Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/app/settings/server/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { PathUtils } from "@/server/utils/path.utils";
import { FsUtils } from "@/server/utils/fs.utils";
import fs from "fs";
import { z } from "zod";
import { revalidateTag } from "next/cache";
import { Tags } from "@/server/utils/cache-tag-generator.utils";


export const updateIngressSettings = async (prevState: any, inputData: QsIngressSettingsModel) =>
Expand Down Expand Up @@ -108,6 +110,7 @@ export const updateQuickstack = async () =>
simpleAction(async () => {
await getAdminUserSession();
const useCaranyChannel = await paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false);
revalidateTag(Tags.quickStackVersionInfo());
await quickStackService.updateQuickStack(useCaranyChannel);
return new SuccessActionResult(undefined, 'QuickStack will be updated, refresh the page in a few seconds.');
});
Expand Down
49 changes: 36 additions & 13 deletions src/app/settings/server/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import podService from "@/server/services/pod.service";
import quickStackService from "@/server/services/qs.service";
import { ServerSettingsTabs } from "./server-settings-tabs";
import { Settings, Network, HardDrive, Rocket, Wrench } from "lucide-react";
import quickStackUpdateService from "@/server/services/qs-update.service";

export default async function ProjectPage({
searchParams
Expand All @@ -29,18 +30,40 @@ export default async function ProjectPage({
}) {

const session = await getAdminUserSession();
const serverUrl = await paramService.getString(ParamService.QS_SERVER_HOSTNAME, '');
const disableNodePortAccess = await paramService.getBoolean(ParamService.DISABLE_NODEPORT_ACCESS, false);
const letsEncryptMail = await paramService.getString(ParamService.LETS_ENCRYPT_MAIL, session.email);
const regitryStorageLocation = await paramService.getString(ParamService.REGISTRY_SOTRAGE_LOCATION, Constants.INTERNAL_REGISTRY_LOCATION);
const ipv4Address = await paramService.getString(ParamService.PUBLIC_IPV4_ADDRESS);
const systemBackupLocation = await paramService.getString(ParamService.QS_SYSTEM_BACKUP_LOCATION, Constants.QS_SYSTEM_BACKUP_DEACTIVATED);
const s3Targets = await s3TargetService.getAll();
const traefikStatus = await traefikService.getStatus();
const useCanaryChannel = await paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false);
const qsPodInfos = await podService.getPodsForApp(Constants.QS_NAMESPACE, Constants.QS_APP_NAME);

const [
serverUrl,
disableNodePortAccess,
letsEncryptMail,
regitryStorageLocation,
ipv4Address,
systemBackupLocation,
useCanaryChannel
] = await Promise.all([
paramService.getString(ParamService.QS_SERVER_HOSTNAME, ''),
paramService.getBoolean(ParamService.DISABLE_NODEPORT_ACCESS, false),
paramService.getString(ParamService.LETS_ENCRYPT_MAIL, session.email),
paramService.getString(ParamService.REGISTRY_SOTRAGE_LOCATION, Constants.INTERNAL_REGISTRY_LOCATION),
paramService.getString(ParamService.PUBLIC_IPV4_ADDRESS),
paramService.getString(ParamService.QS_SYSTEM_BACKUP_LOCATION, Constants.QS_SYSTEM_BACKUP_DEACTIVATED),
paramService.getBoolean(ParamService.USE_CANARY_CHANNEL, false)
]);

const [
s3Targets,
traefikStatus,
qsPodInfos,
currentVersion,
newVersionInfo
] = await Promise.all([
s3TargetService.getAll(),
traefikService.getStatus(),
podService.getPodsForApp(Constants.QS_NAMESPACE, Constants.QS_APP_NAME),
quickStackService.getVersionOfCurrentQuickstackInstance(),
quickStackUpdateService.getNewVersionInfo()
]);

const qsPodInfo = qsPodInfos.find(p => !!p);
const currentVersion = await quickStackService.getVersionOfCurrentQuickstackInstance();
const defaultTab = typeof searchParams?.tab === 'string' ? searchParams.tab : 'general';

return (
Expand All @@ -63,7 +86,7 @@ export default async function ProjectPage({
<TabsTrigger value="general"><Settings className="mr-2 h-4 w-4" />General</TabsTrigger>
<TabsTrigger value="networking"><Network className="mr-2 h-4 w-4" />Networking / Traefik</TabsTrigger>
<TabsTrigger value="storage"><HardDrive className="mr-2 h-4 w-4" />Storage & Backups</TabsTrigger>
<TabsTrigger value="updates"><Rocket className="mr-2 h-4 w-4" />Updates</TabsTrigger>
<TabsTrigger value="updates"><Rocket className="mr-2 h-4 w-4" />Updates {newVersionInfo && <div className="h-2 w-2 ml-2 rounded-full bg-orange-500 animate-pulse" />}</TabsTrigger>
<TabsTrigger value="maintenance"><Wrench className="mr-2 h-4 w-4" />Maintenance</TabsTrigger>
</TabsList>

Expand All @@ -90,7 +113,7 @@ export default async function ProjectPage({

<TabsContent value="updates" className="space-y-4">
<div className="grid gap-6">
<QuickStackVersionInfo currentVersion={currentVersion} useCanaryChannel={useCanaryChannel!} />
<QuickStackVersionInfo newVersionInfo={newVersionInfo} currentVersion={currentVersion} useCanaryChannel={useCanaryChannel!} />
</div>
</TabsContent>
<TabsContent value="maintenance" className="space-y-4">
Expand Down
128 changes: 99 additions & 29 deletions src/app/settings/server/qs-version-info.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,133 @@
'use client';

import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { purgeRegistryImages, setCanaryChannel, updateQuickstack, updateRegistry } from "./actions";
import { setCanaryChannel, updateQuickstack } from "./actions";
import { Button } from "@/components/ui/button";
import { Toast } from "@/frontend/utils/toast.utils";
import { useConfirmDialog } from "@/frontend/states/zustand.states";
import { LogsDialog } from "@/components/custom/logs-overlay";
import { Constants } from "@/shared/utils/constants";
import { Rocket, RotateCcw, SquareTerminal, Trash } from "lucide-react";
import { Rocket, ExternalLink } from "lucide-react";
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import React from "react";
import { GithubReleaseInfo } from "@/server/adapter/github.adapter";
import Link from "next/link";

export default function QuickStackVersionInfo({
useCanaryChannel,
currentVersion
currentVersion,
newVersionInfo
}: {
useCanaryChannel: boolean;
currentVersion?: string;
newVersionInfo?: GithubReleaseInfo
}) {

const useConfirm = useConfirmDialog();
const [loading, setLoading] = React.useState(false);

const handleUpdate = async () => {
if (await useConfirm.openConfirmDialog({
title: 'Update QuickStack',
description: 'This action will restart the QuickStack service and installs the latest version. It may take a few minutes to complete.',
okButton: "Update QuickStack",
})) {
Toast.fromAction(() => updateQuickstack());
}
};

return <>
<Card>
<CardHeader>
<CardTitle>QuickStack Version</CardTitle>
<CardDescription>Update your QuickStack cluster or change to the experimental Canary version.</CardDescription>
<CardTitle className="flex items-center gap-2">
QuickStack Version
</CardTitle>
<CardDescription>Manage your QuickStack version and update channel preferences</CardDescription>
</CardHeader>
<CardContent className="space-y-6">


<div className="flex items-center space-x-2 pl-1">
<Switch id="canary-channel-mode" disabled={loading} checked={useCanaryChannel} onCheckedChange={(checked) => {
try {
setLoading(true);
Toast.fromAction(() => setCanaryChannel(checked));
} finally {
setLoading(false);
}
}} />
<Label htmlFor="canary-channel-mode">Use Canary Channel for Updates</Label>
<div className="rounded-lg border bg-muted/50 p-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">Current Version</p>
<p className="text-2xl font-bold">{currentVersion ?? 'unknown'}</p>
</div>
{newVersionInfo && (
<div className="flex flex-col items-end gap-2">
<div className="flex items-center gap-2 rounded-full bg-primary/10 px-3 py-1 cursor-pointer" onClick={handleUpdate}>
<div className="h-2 w-2 rounded-full bg-orange-500 animate-pulse" />
<span className="text-xs font-medium text-primary">Update Available</span>
</div>
<div className="text-sm text-muted-foreground flex gap-1">
<span>Version {newVersionInfo.version} | </span>
<Link href={newVersionInfo.url} target="_blank" className="flex gap-1 items-center hover:underline">
<ExternalLink className=" h-4 w-4" />
View Release Notes
</Link>
</div>
</div>
)}
</div>
</div>

<div className="flex items-center gap-4">
<Button variant="secondary" disabled={loading} onClick={async () => {
if (await useConfirm.openConfirmDialog({
title: 'Update QuickStack',
description: 'This action will restart the QuickStack service and installs the lastest version. It may take a few minutes to complete.',
okButton: "Update QuickStack",
})) {
Toast.fromAction(() => updateQuickstack());
}
}}><Rocket /> Update QuickStack</Button>
<p className="text-slate-500 text-sm flex-1 text-right">Installed: {currentVersion ?? 'unknown'}</p>
<div className="space-y-3">
<div className="flex items-center justify-between rounded-lg border p-4 hover:bg-accent/50 transition-colors">
<div className="space-y-0.5">
<Label htmlFor="canary-channel-mode" className="text-base cursor-pointer">
Canary Channel
</Label>
<p className="text-sm text-muted-foreground">
Get early access to experimental features and updates (not recommended for production environments).
</p>
</div>
<Switch
id="canary-channel-mode"
disabled={loading}
checked={useCanaryChannel}
onCheckedChange={async (checked) => {
// Show warning when enabling canary channel
if (checked) {
const confirmed = await useConfirm.openConfirmDialog({
title: 'Enable Canary Channel',
description: 'Canary channel provides early access to experimental features and updates. These versions may contain bugs, make your QuickStack cluster unusable and are not recommended for production environments. Are you sure you want to continue?',
okButton: "Enable Canary Channel",
});

if (!confirmed) {
return;
}
}

try {
setLoading(true);
Toast.fromAction(() => setCanaryChannel(checked));
} finally {
setLoading(false);
}
}}
/>
</div>
</div>


</CardContent>
<CardFooter className="flex justify-between items-center border-t pt-6">
{useCanaryChannel ?
<p className="text-sm text-muted-foreground">
Cannot check for updates while on the canary channel.
</p> :
<p className="text-sm text-muted-foreground">
{newVersionInfo ? 'Update to the latest version' : 'You are up to date'}
</p>}
<Button
disabled={loading}
onClick={handleUpdate}
size="lg"
className="gap-2"
>
<Rocket className="h-4 w-4" />
Update QuickStack
</Button>
</CardFooter>
</Card >
</>;
}
68 changes: 35 additions & 33 deletions src/app/sidebar-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,51 +32,53 @@ import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import QuickStackLogo from "@/components/custom/quickstack-logo"
import { UserGroupUtils } from "@/shared/utils/role.utils"


const settingsMenu = [
{
title: "Profile",
url: "/settings/profile",
icon: User,
},
{
title: "Users & Groups",
url: "/settings/users",
icon: User2,
adminOnly: true,
},
{
title: "S3 Targets",
url: "/settings/s3-targets",
icon: Settings,
adminOnly: true,
},
{
title: "Cluster",
url: "/settings/cluster",
adminOnly: true,
},
{
title: "QuickStack Settings",
url: "/settings/server",
adminOnly: true,
},
]
import { GithubReleaseInfo } from "@/server/adapter/github.adapter"

export function SidebarCient({
projects,
session
session,
newVersionInfo
}: {
projects: (Project & { apps: App[] })[];
session: UserSession;
newVersionInfo?: GithubReleaseInfo;
}) {

const path = usePathname();

const [currentlySelectedProjectId, setCurrentlySelectedProjectId] = useState<string | null>(null);
const [currentlySelectedAppId, setCurrentlySelectedAppId] = useState<string | null>(null);

const settingsMenu = [
{
title: "Profile",
url: "/settings/profile",
icon: User,
},
{
title: "Users & Groups",
url: "/settings/users",
icon: User2,
adminOnly: true,
},
{
title: "S3 Targets",
url: "/settings/s3-targets",
icon: Settings,
adminOnly: true,
},
{
title: "Cluster",
url: "/settings/cluster",
adminOnly: true,
},
{
title: <span className="flex items-center gap-2">QuickStack Settings {newVersionInfo && <div className="h-2 w-2 rounded-full bg-orange-500 animate-pulse" />}</span>,
url: "/settings/server",
adminOnly: true,
},
]

useEffect(() => {
if (path.startsWith('/project/app/')) {
const appId = path.split('/')[3];
Expand Down Expand Up @@ -265,7 +267,7 @@ export function SidebarCient({
<SidebarMenuSub>
{(UserGroupUtils.isAdmin(session) ? settingsMenu :
settingsMenu.filter(x => !x.adminOnly)).map((item) => (
<SidebarMenuSubItem key={item.title}>
<SidebarMenuSubItem key={item.url}>
<SidebarMenuButton asChild>
<Link href={item.url}>
<span>{item.title}</span>
Expand Down
5 changes: 3 additions & 2 deletions src/app/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import projectService from "@/server/services/project.service"
import { getUserSession } from "@/server/utils/action-wrapper.utils"
import { SidebarCient } from "./sidebar-client"
import { UserGroupUtils } from "@/shared/utils/role.utils";
import quickStackUpdateService from "@/server/services/qs-update.service";

export async function AppSidebar() {

Expand All @@ -12,12 +13,12 @@ export async function AppSidebar() {
}

const projects = await projectService.getAllProjects();

const newVersionInfo = await quickStackUpdateService.getNewVersionInfo();
const relevantProjectsForUser = projects.filter((project) =>
UserGroupUtils.sessionHasReadAccessToProject(session, project.id));
for (const project of relevantProjectsForUser) {
project.apps = project.apps.filter((app) => UserGroupUtils.sessionHasReadAccessForApp(session, app.id));
}

return <SidebarCient projects={relevantProjectsForUser} session={session} />
return <SidebarCient newVersionInfo={newVersionInfo} projects={relevantProjectsForUser} session={session} />
}
Loading