Compare commits

..
Author SHA1 Message Date
mbecker20 15eaeab68d deploy 2.0.0-dev-120 2026-02-26 15:06:15 -08:00
mbecker20 89ea5ad5a4 stack and deployment state color include update available. Ensure server version mismatch color applied everywhere 2026-02-26 15:02:37 -08:00
mbecker20 c05dde3678 add close button to update / alert details drawer 2026-02-26 14:43:24 -08:00
mbecker20 a17a1005c7 smaller more consistent gaps 2026-02-26 14:38:36 -08:00
mbecker20 d13314eb98 stack stopped and deployment exited warning instead of critical 2026-02-26 14:25:31 -08:00
mbecker20 f41a61116a fix batch executions width 2026-02-26 14:24:58 -08:00
mbecker20 13fbcae105 edit swarm join command 2026-02-26 14:09:01 -08:00
mbecker20 71c437962f ensure section headers consistent spacing 2026-02-26 14:06:57 -08:00
mbecker20 e4147ccdaf rename ConfigSwitch onChange -> onCheckedChange 2026-02-26 13:41:51 -08:00
mbecker20 5089375211 improve tab styling 2026-02-26 13:38:07 -08:00
mbecker20 e489812af4 Fix config input issue 2026-02-26 13:37:32 -08:00
mbecker20 7373ff8d0e execution buttons disabled when loading 2026-02-26 12:53:58 -08:00
mbecker20 4e1cd32f3f Select Template 2026-02-25 19:31:28 -08:00
mbecker20 57211746f1 hide actions when none 2026-02-25 19:21:48 -08:00
mbecker20 5e153fb02b dashboard active styling 2026-02-25 19:21:22 -08:00
mbecker20 b28a413005 procedure / action webhook branch mobile style 2026-02-25 17:46:22 -08:00
mbecker20 691671abbd Load Average is first historical stat 2026-02-25 17:43:54 -08:00
mbecker20 504e81c2f7 improve login no auth configured and passkey pending 2026-02-25 17:36:02 -08:00
66 changed files with 634 additions and 511 deletions
Generated
+13 -13
View File
@@ -1149,7 +1149,7 @@ dependencies = [
[[package]]
name = "command"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"komodo_client",
"shlex",
@@ -1489,7 +1489,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "database"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"async-compression",
@@ -1759,7 +1759,7 @@ dependencies = [
[[package]]
name = "encoding"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"bytes",
@@ -1801,7 +1801,7 @@ dependencies = [
[[package]]
name = "environment"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"formatting",
@@ -1930,7 +1930,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"mogh_error",
]
@@ -2109,7 +2109,7 @@ dependencies = [
[[package]]
name = "git"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"command",
@@ -2709,7 +2709,7 @@ dependencies = [
[[package]]
name = "interpolate"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"komodo_client",
@@ -2835,7 +2835,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"chrono",
@@ -2863,7 +2863,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2902,7 +2902,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"arc-swap",
@@ -2975,7 +2975,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"arc-swap",
@@ -4030,7 +4030,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "periphery_client"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"encoding",
@@ -5983,7 +5983,7 @@ dependencies = [
[[package]]
name = "transport"
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
dependencies = [
"anyhow",
"axum",
+2 -2
View File
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "2.0.0-dev-118"
version = "2.0.0-dev-120"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -77,7 +77,7 @@ indexmap = { version = "2.13.0", features = ["serde"] }
serde = { version = "1.0.227", features = ["derive"] }
strum = { version = "0.28.0", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
toml = "0.9.11+spec-1.1.0"
toml = "0.9.11"
serde_yaml_ng = "0.10.0"
serde_json = "1.0.149"
serde_qs = "1.0.0"
+8 -8
View File
@@ -3,14 +3,14 @@ module.exports = {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
"mantine-breakpoint-xl2": "104em",
"mantine-breakpoint-xl3": "120em",
"mantine-breakpoint-xl4": "136em",
"mantine-breakpoint-xs": "40em",
"mantine-breakpoint-sm": "54em",
"mantine-breakpoint-md": "68em",
"mantine-breakpoint-lg": "82em",
"mantine-breakpoint-xl": "96em",
"mantine-breakpoint-xl2": "110em",
"mantine-breakpoint-xl3": "126em",
"mantine-breakpoint-xl4": "142em",
},
},
},
+7 -2
View File
@@ -5,7 +5,7 @@ import {
} from "@/lib/formatting";
import { useInvalidate, useRead, useUser, useWrite } from "@/lib/hooks";
import { ResourceComponents, UsableResource } from "@/resources";
import { Drawer, Group, Stack, Text } from "@mantine/core";
import { ActionIcon, Drawer, Group, Stack, Text } from "@mantine/core";
import { ICONS } from "@/theme/icons";
import { Clock, Link2 } from "lucide-react";
import CopyButton from "@/ui/copy-button";
@@ -92,7 +92,12 @@ export function AlertDetailsContent({
return (
<Stack gap="xl" m="md">
{/** HEADER */}
<Text fz="h1">{fmtUpperCamelcase(alert.data.type)}</Text>
<Group justify="space-between">
<Text fz="h1">{fmtUpperCamelcase(alert.data.type)}</Text>
<ActionIcon size="lg" variant="filled" color="red" onClick={close}>
<ICONS.Clear size="1.3rem" />
</ActionIcon>
</Group>
{/** DETAILS */}
<Stack gap="sm">
+1 -1
View File
@@ -47,7 +47,7 @@ export default function ApiKeysSection({
isPending={isPending}
title="Api Keys"
titleFz="h3"
titleMb="0"
icon={<ICONS.Key size="1.2rem" />}
titleRight={
<Box ml="md">
+7 -7
View File
@@ -15,6 +15,7 @@ import {
Stack,
Text,
TextInput,
useMatches,
} from "@mantine/core";
import { Types } from "komodo_client";
import { ChevronDown } from "lucide-react";
@@ -62,19 +63,18 @@ function BatchExecutionsDropdownMenu<T extends Request>({
onSelect: (item: T) => void;
disabled: boolean;
}) {
const width = useMatches({
base: "target",
xs: 250,
});
return (
<Menu
position="bottom-start"
offset={16}
disabled={disabled}
width="target"
>
<Menu position="bottom-start" offset={16} disabled={disabled} width={width}>
<Menu.Target>
<Button
leftSection={<ICONS.Execution size="1rem" />}
rightSection={<ChevronDown size="1rem" />}
disabled={disabled}
w={{ base: "100%", xs: "fit-content" }}
w={{ base: "100%", sm: "fit-content" }}
>
Execute
</Button>
@@ -48,6 +48,7 @@ export default function ContainersSection({
<Section
titleOther={titleOther}
title={!titleOther ? "Containers" : undefined}
icon={!titleOther ? <ICONS.Container size="1.3rem" /> : undefined}
actions={
(pruneButton && !allRunning) || _search || setShow ? (
@@ -37,6 +37,7 @@ export default function DockerLabelsSection({
<SearchInput value={search} onSearch={setSearch} />
</Box>
}
{...props}
>
<DockerOptions options={filtered} {...groupProps} />
@@ -84,7 +84,7 @@ export default function BasePermissionsSection({
}
title="Base Permissions on Resource Types"
titleFz="h3"
titleMb="0"
actions={
<Group>
<SearchInput value={search} onSearch={setSearch} />
@@ -51,7 +51,7 @@ export default function SpecificPermissionsSection({
<Section
title="Per Resource Permissions"
titleFz="h3"
titleMb="0"
actions={
<Group>
<SearchInput value={search} onSearch={setSearch} />
+11 -5
View File
@@ -8,7 +8,7 @@ import { useRead } from "@/lib/hooks";
import { useWebsocketMessages } from "@/lib/socket";
import { updateLogToHtml, versionIsNone } from "@/lib/utils";
import { ResourceComponents, UsableResource } from "@/resources";
import { Code, Drawer, Group, Stack, Text } from "@mantine/core";
import { ActionIcon, Code, Drawer, Group, Stack, Text } from "@mantine/core";
import UserAvatar from "@/components/user-avatar";
import { ICONS } from "@/theme/icons";
import { Clock, Link2, SquarePen } from "lucide-react";
@@ -62,6 +62,7 @@ export default function UpdateDetails({
}
export function UpdateDetailsContent({ id }: { id: string }) {
const { close } = useUpdateDetails();
const { data: update, refetch } = useRead("GetUpdate", { id });
// Listen for updates on the update id and refetch
useWebsocketMessages("update-details", (update) => {
@@ -80,10 +81,15 @@ export function UpdateDetailsContent({ id }: { id: string }) {
return (
<Stack gap="xl" m="md">
{/** HEADER */}
<Text fz="h2">
{fmtOperation(update.operation)}{" "}
{!versionIsNone(update.version) && fmtVersion(update.version)}
</Text>
<Group justify="space-between">
<Text fz="h2">
{fmtOperation(update.operation)}{" "}
{!versionIsNone(update.version) && fmtVersion(update.version)}
</Text>
<ActionIcon size="lg" variant="filled" color="red" onClick={close}>
<ICONS.Clear size="1.3rem" />
</ActionIcon>
</Group>
{/** DETAILS */}
<Stack gap="sm">
+1 -1
View File
@@ -15,7 +15,7 @@ export default function UpdatesSection({
return (
<Section
title="Updates"
titleMb="0"
icon={<ICONS.Update size="1.3rem" />}
actions={
link && (
+11 -13
View File
@@ -141,13 +141,13 @@ export const swarmTaskStateIntention: (
};
export const serverStateIntention: (
state?: Types.ServerState,
hasVersionMismatch?: boolean,
) => ColorIntention = (state, hasVersionMismatch) => {
state: Types.ServerState | undefined,
versionMismatch: boolean,
) => ColorIntention = (state, versionMismatch) => {
switch (state) {
case Types.ServerState.Ok:
// If there's a version mismatch and the server is "Ok", show warning instead
return hasVersionMismatch ? "Warning" : "Good";
return versionMismatch ? "Warning" : "Good";
case Types.ServerState.NotOk:
return "Critical";
case Types.ServerState.Disabled:
@@ -157,18 +157,17 @@ export const serverStateIntention: (
}
};
export const stackStateIntention = (state?: Types.StackState) => {
export const stackStateIntention = (
state: Types.StackState | undefined,
updateAvailable: boolean | undefined,
) => {
switch (state) {
case undefined:
return "None";
case Types.StackState.Deploying:
return "Warning";
case Types.StackState.Running:
return "Good";
case Types.StackState.Paused:
return "Warning";
case Types.StackState.Stopped:
return "Critical";
return updateAvailable ? "Warning" : "Good";
case Types.StackState.Restarting:
return "Critical";
case Types.StackState.Down:
@@ -181,7 +180,8 @@ export const stackStateIntention = (state?: Types.StackState) => {
};
export const deploymentStateIntention: (
state?: Types.DeploymentState,
state: Types.DeploymentState | undefined,
updateAvailable: boolean | undefined,
) => ColorIntention = (state) => {
switch (state) {
case undefined:
@@ -192,8 +192,6 @@ export const deploymentStateIntention: (
return "Good";
case Types.DeploymentState.NotDeployed:
return "Neutral";
case Types.DeploymentState.Paused:
return "Warning";
case Types.DeploymentState.Unknown:
return "Unknown";
default:
+1 -1
View File
@@ -69,7 +69,7 @@ export default function Alerts() {
icon={ICONS.Alert}
description="View historical alerts"
>
<Stack gap="sm">
<Stack>
{/* QUERY */}
<Group>
{/* RESOURCE TYPE */}
+1 -1
View File
@@ -51,7 +51,7 @@ export default function Containers() {
icon={ICONS.Container}
description="See all containers across all servers."
>
<Stack gap="xs">
<Stack>
<Group justify="space-between">
<MultiSelect
placeholder="Filter by Servers"
+12 -2
View File
@@ -75,7 +75,13 @@ export default function DashboardActiveResources() {
state: (
<StatusBadge
text={stack.info.state}
intent={stackStateIntention(stack.info.state)}
intent={stackStateIntention(
stack.info.state,
stack.info.services &&
!stack.info.services.every(
(service) => !service.update_available,
),
)}
/>
),
})),
@@ -85,7 +91,10 @@ export default function DashboardActiveResources() {
state: (
<StatusBadge
text={deployment.info.state}
intent={deploymentStateIntention(deployment.info.state)}
intent={deploymentStateIntention(
deployment.info.state,
deployment.info.update_available,
)}
/>
),
})),
@@ -147,6 +156,7 @@ export default function DashboardActiveResources() {
return (
<Section
title="Active"
mb="xl"
icon={
<Circle
size="1rem"
+1 -1
View File
@@ -27,7 +27,7 @@ import ServerStatsCard from "@/resources/server/stats-card";
export default function DashboardRecents() {
return (
<Stack gap="xl">
<Stack>
<DashboardNoResources />
{SIDEBAR_RESOURCES.map((type) => (
<RecentRow key={type} type={type} />
+108 -108
View File
@@ -8,9 +8,10 @@ import ResourceSubPage from "@/resources/sub-page";
import { ICONS } from "@/theme/icons";
import ConfirmButton from "@/ui/confirm-button";
import { DataTable } from "@/ui/data-table";
import PageGuard from "@/ui/page-guard";
import Section from "@/ui/section";
import ShowHideButton from "@/ui/show-hide-button";
import { Box, Center, Group, Loader, Text } from "@mantine/core";
import { Box, Center, Group, Text } from "@mantine/core";
import { Types } from "komodo_client";
import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
@@ -79,132 +80,131 @@ function ImageInner({
},
);
if (isPending) {
return (
<Center h="30vh">
<Loader size="xl" />
</Center>
);
}
if (isError) {
return (
<Center h="30vh">
<Text>Failed to inspect image.</Text>
</Center>
);
}
if (!image) {
return (
<Center h="30vh">
<Text>No image found with given name: {imageName}</Text>
</Center>
);
}
const unused = containers && containers.length === 0 ? true : false;
const intention = unused ? "Critical" : "Good";
return (
<ResourceSubPage
entityTypeName="Image"
parentType="Server"
parentId={serverId}
name={imageName}
icon={ICONS.Image}
intent={intention}
state={unused ? "Unused" : "In Use"}
info={
image.Id && (
<Group gap="xs">
<Text>Id:</Text>
<Text title={image.Id} maw={150} className="text-ellipsis">
{image.Id}
</Text>
</Group>
)
}
executions={
unused && (
<ConfirmButton
variant="filled"
color="red"
icon={<ICONS.Delete size="1rem" />}
loading={deletePending}
onClick={() => deleteImage({ server: serverId, name: imageName })}
>
Delete Image
</ConfirmButton>
)
<PageGuard
isPending={isPending}
error={
isError
? "Failed to inspect image."
: !image
? "No image found with name: " + imageName
: undefined
}
>
{containers && containers.length > 0 && (
<ContainersSection serverId={serverId} containers={containers} />
)}
{/* TOP LEVEL IMAGE INFO */}
<Section title="Details" icon={<ICONS.Info size="1.3rem" />}>
<DataTable
tableKey="image-info"
data={[image]}
columns={[
{
accessorKey: "Architecture",
header: "Architecture",
},
{
accessorKey: "Os",
header: "Os",
},
{
accessorKey: "Size",
header: "Size",
cell: ({ row }) =>
row.original.Size ? fmtSizeBytes(row.original.Size) : "Unknown",
},
]}
/>
</Section>
{history && history.length > 0 && (
<Section
title="History"
icon={<ICONS.History size="1.3rem" />}
titleRight={
<Box pl="md">
<ShowHideButton show={showHistory} setShow={setShowHistory} />
</Box>
{image && (
<ResourceSubPage
entityTypeName="Image"
parentType="Server"
parentId={serverId}
name={imageName}
icon={ICONS.Image}
intent={intention}
state={unused ? "Unused" : "In Use"}
info={
image.Id && (
<Group gap="xs">
<Text>Id:</Text>
<Text title={image.Id} maw={150} className="text-ellipsis">
{image.Id}
</Text>
</Group>
)
}
executions={
unused && (
<ConfirmButton
variant="filled"
color="red"
icon={<ICONS.Delete size="1rem" />}
loading={deletePending}
onClick={() =>
deleteImage({ server: serverId, name: imageName })
}
>
Delete Image
</ConfirmButton>
)
}
>
{showHistory && (
{containers && containers.length > 0 && (
<ContainersSection serverId={serverId} containers={containers} />
)}
{/* TOP LEVEL IMAGE INFO */}
<Section
title="Details"
icon={<ICONS.Info size="1.3rem" />}
>
<DataTable
tableKey="image-history"
data={history.toReversed()}
tableKey="image-info"
data={[image]}
columns={[
{
accessorKey: "CreatedBy",
header: "Created By",
size: 400,
accessorKey: "Architecture",
header: "Architecture",
},
{
accessorKey: "Created",
header: "Timestamp",
accessorKey: "Os",
header: "Os",
},
{
accessorKey: "Size",
header: "Size",
cell: ({ row }) =>
fmtDateWithMinutes(new Date(row.original.Created * 1000)),
size: 200,
row.original.Size
? fmtSizeBytes(row.original.Size)
: "Unknown",
},
]}
/>
</Section>
{history && history.length > 0 && (
<Section
title="History"
icon={<ICONS.History size="1.3rem" />}
titleRight={
<Box pl="md">
<ShowHideButton show={showHistory} setShow={setShowHistory} />
</Box>
}
>
{showHistory && (
<DataTable
tableKey="image-history"
data={history.toReversed()}
columns={[
{
accessorKey: "CreatedBy",
header: "Created By",
size: 400,
},
{
accessorKey: "Created",
header: "Timestamp",
cell: ({ row }) =>
fmtDateWithMinutes(
new Date(row.original.Created * 1000),
),
size: 200,
},
]}
/>
)}
</Section>
)}
</Section>
)}
{specific.includes(Types.SpecificPermission.Inspect) && (
<InspectSection json={image} showToggle />
)}
{specific.includes(Types.SpecificPermission.Inspect) && (
<InspectSection json={image} showToggle />
)}
<DockerLabelsSection labels={image?.Config?.Labels} />
</ResourceSubPage>
<DockerLabelsSection labels={image?.Config?.Labels} />
</ResourceSubPage>
)}
</PageGuard>
);
}
+61
View File
@@ -0,0 +1,61 @@
import { komodo_client, useLoginOptions } from "@/lib/hooks";
import { Button, Group, Stack, Text } from "@mantine/core";
import { MoghAuth } from "komodo_client";
import { KeyRound } from "lucide-react";
export default function LoginHeader({
secondFactorPending,
}: {
secondFactorPending: boolean;
}) {
const options = useLoginOptions().data;
return (
<Group gap="4rem" justify="space-between">
<Group gap="sm">
<img src="/mogh-512x512.png" width={42} height={42} alt="moghtech" />
<Stack gap="0">
<Text fz="h2" fw="450" lts="0.1rem">
KOMODO
</Text>
<Text size="md" opacity={0.6} mt={-8}>
Log In
</Text>
</Stack>
</Group>
<Group gap="sm">
{(
[
[options?.oidc, "Oidc"],
[options?.github, "Github"],
[options?.google, "Google"],
] as Array<
[boolean | undefined, MoghAuth.Types.ExternalLoginProvider]
>
).map(
([enabled, provider]) =>
enabled && (
<Button
key={provider}
onClick={() => komodo_client().auth.externalLogin(provider)}
leftSection={
provider === "Oidc" ? (
<KeyRound size="1rem" />
) : (
<img
src={`/icons/${provider.toLowerCase()}.svg`}
alt={provider}
style={{ width: "1rem", height: "1rem" }}
/>
)
}
w={110}
disabled={secondFactorPending}
>
{provider}
</Button>
),
)}
</Group>
</Group>
);
}
@@ -1,9 +1,4 @@
import {
komodo_client,
useLogin,
useLoginOptions,
useUserInvalidate,
} from "@/lib/hooks";
import { useLogin, useLoginOptions, useUserInvalidate } from "@/lib/hooks";
import { sanitizeQuery } from "@/lib/utils";
import {
Button,
@@ -12,15 +7,15 @@ import {
Group,
Loader,
PasswordInput,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { notifications } from "@mantine/notifications";
import { MoghAuth } from "komodo_client";
import { KeyRound } from "lucide-react";
import { AlertTriangle, KeyRound } from "lucide-react";
import { useState } from "react";
import LoginHeader from "./header";
export default function Login({
passkeyIsPending: _passkeyIsPending,
@@ -101,11 +96,11 @@ export default function Login({
},
);
// const noAuthConfigured =
// options !== undefined &&
// Object.values(options).every((value) => value === false);
const noAuthConfigured =
options !== undefined &&
Object.values(options).every((value) => value === false);
// const showSignUp = options !== undefined && !options.registration_disabled;
const showSignUp = options !== undefined && !options.registration_disabled;
const localForm = useForm({
mode: "uncontrolled",
@@ -131,67 +126,10 @@ export default function Login({
},
});
const registration_disabled = options?.registration_disabled ?? true;
return (
<Center h="80vh">
<Fieldset
legend={
<Group gap="4rem" justify="space-between">
<Group gap="sm">
<img
src="/mogh-512x512.png"
width={42}
height={42}
alt="moghtech"
/>
<Stack gap="0">
<Text fz="h2" fw="450" lts="0.1rem">
KOMODO
</Text>
<Text size="md" opacity={0.6} mt={-8}>
Log In
</Text>
</Stack>
</Group>
<Group gap="sm">
{(
[
[options?.oidc, "Oidc"],
[options?.github, "Github"],
[options?.google, "Google"],
] as Array<
[boolean | undefined, MoghAuth.Types.ExternalLoginProvider]
>
).map(
([enabled, provider]) =>
enabled && (
<Button
key={provider}
onClick={() =>
komodo_client().auth.externalLogin(provider)
}
leftSection={
provider === "Oidc" ? (
<KeyRound size="1rem" />
) : (
<img
src={`/icons/${provider.toLowerCase()}.svg`}
alt={provider}
style={{ width: "1rem", height: "1rem" }}
/>
)
}
w={110}
disabled={secondFactorPending}
>
{provider}
</Button>
),
)}
</Group>
</Group>
}
legend={<LoginHeader secondFactorPending={secondFactorPending} />}
component="form"
onSubmit={
totpIsPending
@@ -218,7 +156,7 @@ export default function Login({
key={localForm.key("password")}
/>
<Group mt="sm" justify="end">
{!registration_disabled && (
{showSignUp && (
<Button
variant="outline"
w={110}
@@ -228,11 +166,7 @@ export default function Login({
Sign Up
</Button>
)}
<Button
w={110}
type="submit"
loading={loginPending}
>
<Button w={110} type="submit" loading={loginPending}>
Log In
</Button>
</Group>
@@ -240,13 +174,11 @@ export default function Login({
)}
{passkeyIsPending && (
<>
<Group justify="center" my="lg">
<KeyRound size="1.5rem" />
<Group>
<Loader />
<Text size="lg">Provide your passkey to finish login</Text>
</Group>
</>
<Text size="lg">Provide your passkey to finish login...</Text>
<Loader />
</Group>
)}
{totpIsPending && (
@@ -276,6 +208,25 @@ export default function Login({
</Group>
</>
)}
{noAuthConfigured && (
<Group my="lg">
<AlertTriangle size="2rem" />
<Text>
No login methods have been configured. <br />
See the{" "}
<a
href="https://github.com/moghtech/komodo/blob/main/config/core.config.toml"
target="_blank"
rel="noreferrer"
className="hover-underline"
>
<b>example config</b>
</a>{" "}
for information on configuring auth.
</Text>
</Group>
)}
</Fieldset>
</Center>
);
+2 -2
View File
@@ -65,7 +65,7 @@ export default function Profile() {
<Section
title="Login"
titleFz="h3"
titleMb="0"
icon={<ICONS.Key size="1.2rem" />}
withBorder
>
@@ -115,7 +115,7 @@ export default function Profile() {
<Section
title="2FA"
titleFz="h3"
titleMb="0"
icon={<ICONS.Key size="1.2rem" />}
withBorder
>
+1 -1
View File
@@ -95,7 +95,7 @@ export const LinkedLogins = ({
<Section
title="Providers"
titleFz="h3"
titleMb="0"
icon={<ICONS.Provider size="1.2rem" />}
withBorder
>
+1 -1
View File
@@ -92,7 +92,7 @@ function ResourceInner({ type, id }: { type: UsableResource; id: string }) {
{canExecute && Object.keys(RC.Executions).length > 0 && (
<Section
title="Execute"
titleMb="0"
icon={<ICONS.Execution size="1rem" />}
my="md"
>
+24 -22
View File
@@ -11,7 +11,7 @@ import {
import { ResourceComponents, UsableResource } from "@/resources";
import { Types } from "komodo_client";
import Page from "@/ui/page";
import { Group } from "@mantine/core";
import { Group, Stack } from "@mantine/core";
import TableSkeleton from "@/ui/table-skeleton";
import TemplateQuerySelector from "@/components/template-query-selector";
import TagsFilter from "@/components/tags/filter";
@@ -72,29 +72,31 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
</Group>
}
>
<Group justify="space-between" w="100%">
<Group w={{ base: "100%", xs: "fit-content" }}>
{(is_admin || !disable_non_admin_create) && <RC.New />}
<RC.BatchExecutions />
<Stack>
<Group justify="space-between" w="100%">
<Group w={{ base: "100%", xs: "fit-content" }}>
{(is_admin || !disable_non_admin_create) && <RC.New />}
<RC.BatchExecutions />
</Group>
<Group w={{ base: "100%", xs: "fit-content" }}>
{(type === "Stack" || type === "Deployment") && (
<LabelledSwitch
label="Pending Update"
checked={filterUpdateAvailable}
onCheckedChange={toggleFilterUpdateAvailable}
opacity={0.7}
fz="sm"
/>
)}
<TemplateQuerySelector />
<TagsFilter />
<SearchInput value={search} onSearch={setSearch} />
</Group>
</Group>
<Group w={{ base: "100%", xs: "fit-content" }}>
{(type === "Stack" || type === "Deployment") && (
<LabelledSwitch
label="Pending Update"
checked={filterUpdateAvailable}
onCheckedChange={toggleFilterUpdateAvailable}
opacity={0.7}
fz="sm"
/>
)}
<TemplateQuerySelector />
<TagsFilter />
<SearchInput value={search} onSearch={setSearch} />
</Group>
</Group>
{filtered ? <RC.Table resources={filtered ?? []} /> : <TableSkeleton />}
{filtered ? <RC.Table resources={filtered ?? []} /> : <TableSkeleton />}
</Stack>
</Page>
);
}
+73 -71
View File
@@ -14,7 +14,7 @@ import { ICONS } from "@/theme/icons";
import { DataTable, SortableHeader } from "@/ui/data-table";
import Page from "@/ui/page";
import SearchInput from "@/ui/search-input";
import { Group, Switch } from "@mantine/core";
import { Group, Stack, Switch } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useState } from "react";
@@ -31,78 +31,80 @@ export default function Schedules() {
title="Schedules"
description="See an overview of your scheduled Actions and Procedures."
>
<Group justify="end">
<TagsFilter />
<SearchInput value={search} onSearch={setSearch} />
</Group>
<Stack>
<Group justify="end">
<TagsFilter />
<SearchInput value={search} onSearch={setSearch} />
</Group>
<DataTable
tableKey="schedules"
data={filtered}
columns={[
{
size: 200,
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Target" />
),
cell: ({ row }) => (
<ResourceLink
type={row.original.target.type as UsableResource}
id={row.original.target.id}
/>
),
},
{
size: 200,
accessorKey: "schedule",
header: ({ column }) => (
<SortableHeader column={column} title="Schedule" />
),
},
{
size: 200,
accessorKey: "next_scheduled_run",
header: ({ column }) => (
<SortableHeader column={column} title="Next Run" />
),
sortingFn: (a, b) => {
const sa = a.original.next_scheduled_run;
const sb = b.original.next_scheduled_run;
if (!sa && !sb) return 0;
if (!sa) return 1;
if (!sb) return -1;
if (sa > sb) return 1;
else if (sa < sb) return -1;
else return 0;
<DataTable
tableKey="schedules"
data={filtered}
columns={[
{
size: 200,
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Target" />
),
cell: ({ row }) => (
<ResourceLink
type={row.original.target.type as UsableResource}
id={row.original.target.id}
/>
),
},
cell: ({ row }) =>
row.original.next_scheduled_run
? new Date(row.original.next_scheduled_run).toLocaleString()
: "Not Scheduled",
},
{
size: 100,
accessorKey: "enabled",
header: ({ column }) => (
<SortableHeader column={column} title="Enabled" />
),
cell: ({ row: { original: schedule } }) => (
<ScheduleEnableSwitch
type={schedule.target.type as UsableResource}
id={schedule.target.id}
enabled={schedule.enabled}
/>
),
},
{
header: "Tags",
cell: ({ row }) => <TableTags tagIds={row.original.tags} />,
},
]}
/>
{
size: 200,
accessorKey: "schedule",
header: ({ column }) => (
<SortableHeader column={column} title="Schedule" />
),
},
{
size: 200,
accessorKey: "next_scheduled_run",
header: ({ column }) => (
<SortableHeader column={column} title="Next Run" />
),
sortingFn: (a, b) => {
const sa = a.original.next_scheduled_run;
const sb = b.original.next_scheduled_run;
if (!sa && !sb) return 0;
if (!sa) return 1;
if (!sb) return -1;
if (sa > sb) return 1;
else if (sa < sb) return -1;
else return 0;
},
cell: ({ row }) =>
row.original.next_scheduled_run
? new Date(row.original.next_scheduled_run).toLocaleString()
: "Not Scheduled",
},
{
size: 100,
accessorKey: "enabled",
header: ({ column }) => (
<SortableHeader column={column} title="Enabled" />
),
cell: ({ row: { original: schedule } }) => (
<ScheduleEnableSwitch
type={schedule.target.type as UsableResource}
id={schedule.target.id}
enabled={schedule.enabled}
/>
),
},
{
header: "Tags",
cell: ({ row }) => <TableTags tagIds={row.original.tags} />,
},
]}
/>
</Stack>
</Page>
);
}
+1 -1
View File
@@ -23,7 +23,7 @@ export default function SettingsUserGroups({
return (
<Section
title="User Groups"
titleMb="0"
icon={<ICONS.Users size="1.3rem" />}
titleRight={
<Box ml="md">
+1 -1
View File
@@ -40,7 +40,7 @@ export default function SettingsUsers() {
<Section
title="Users"
titleMb="0"
icon={<ICONS.User size="1.3rem" />}
titleRight={
<Group ml="md">
+1 -1
View File
@@ -83,7 +83,7 @@ export default function SwarmTaskTabs({
}
return (
<Tabs color={colorByIntention(intent)} value={view}>
<Tabs color={colorByIntention(intent)} value={view} mt="lg">
{View}
</Tabs>
);
+96 -94
View File
@@ -5,7 +5,7 @@ import { ICONS } from "@/theme/icons";
import { filterBySplit, terminalLink } from "@/lib/utils";
import { DataTable, SortableHeader } from "@/ui/data-table";
import Page from "@/ui/page";
import { Group, Text } from "@mantine/core";
import { Group, Stack, Text } from "@mantine/core";
import { useState } from "react";
import { Link } from "react-router-dom";
import DeleteTerminal from "./delete";
@@ -24,101 +24,103 @@ export default function Terminals() {
icon={ICONS.Terminal}
description="Manage Terminals across all your Servers."
>
<Group justify="space-between">
<Group>
<CreateTerminal />
<BatchDeleteAllTerminals
refetch={refetch}
noTerminals={!terminals?.length}
/>
<Stack>
<Group justify="space-between">
<Group>
<CreateTerminal />
<BatchDeleteAllTerminals
refetch={refetch}
noTerminals={!terminals?.length}
/>
</Group>
<SearchInput value={search} onSearch={setSearch} />
</Group>
<SearchInput value={search} onSearch={setSearch} />
</Group>
<DataTable
tableKey="terminals"
data={filtered}
loading={isPending}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<Link
to={terminalLink(row.original)}
onClick={(e) => {
e.stopPropagation();
}}
>
<Group className="hover-underline" fz="md" wrap="nowrap">
<ICONS.Terminal size="1rem" />
{row.original.name}
</Group>
</Link>
),
},
{
size: 200,
accessorKey: "target",
header: ({ column }) => (
<SortableHeader column={column} title="Target" />
),
cell: ({ row }) => (
<TerminalTargetLink target={row.original.target} />
),
},
{
accessorKey: "command",
header: ({ column }) => (
<SortableHeader column={column} title="Command" />
),
cell: ({ row }) => (
<Text ff="monospace" fz="sm">
{row.original.command}
</Text>
),
},
{
size: 100,
accessorKey: "size",
header: ({ column }) => (
<SortableHeader column={column} title="Size" />
),
cell: ({
row: {
original: { stored_size_kb },
},
}) => (
<span className="font-mono px-2 py-1 bg-secondary rounded-md">
{stored_size_kb.toFixed()} KiB
</span>
),
},
{
accessorKey: "created_at",
header: ({ column }) => (
<SortableHeader column={column} title="Created" />
),
cell: ({
row: {
original: { created_at },
},
}) => fmtDateWithMinutes(new Date(created_at)),
},
{
header: "Delete",
cell: ({ row }) => (
<DeleteTerminal
target={row.original.target}
terminal={row.original.name}
refetch={refetch}
/>
),
},
]}
/>
<DataTable
tableKey="terminals"
data={filtered}
loading={isPending}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<Link
to={terminalLink(row.original)}
onClick={(e) => {
e.stopPropagation();
}}
>
<Group className="hover-underline" fz="md" wrap="nowrap">
<ICONS.Terminal size="1rem" />
{row.original.name}
</Group>
</Link>
),
},
{
size: 200,
accessorKey: "target",
header: ({ column }) => (
<SortableHeader column={column} title="Target" />
),
cell: ({ row }) => (
<TerminalTargetLink target={row.original.target} />
),
},
{
accessorKey: "command",
header: ({ column }) => (
<SortableHeader column={column} title="Command" />
),
cell: ({ row }) => (
<Text ff="monospace" fz="sm">
{row.original.command}
</Text>
),
},
{
size: 100,
accessorKey: "size",
header: ({ column }) => (
<SortableHeader column={column} title="Size" />
),
cell: ({
row: {
original: { stored_size_kb },
},
}) => (
<span className="font-mono px-2 py-1 bg-secondary rounded-md">
{stored_size_kb.toFixed()} KiB
</span>
),
},
{
accessorKey: "created_at",
header: ({ column }) => (
<SortableHeader column={column} title="Created" />
),
cell: ({
row: {
original: { created_at },
},
}) => fmtDateWithMinutes(new Date(created_at)),
},
{
header: "Delete",
cell: ({ row }) => (
<DeleteTerminal
target={row.original.target}
terminal={row.original.name}
refetch={refetch}
/>
),
},
]}
/>
</Stack>
</Page>
);
}
+1 -1
View File
@@ -49,7 +49,7 @@ export default function Updates() {
icon={ICONS.Update}
description="View historical updates"
>
<Stack gap="sm">
<Stack>
{/* QUERY */}
<Group>
{/* RESOURCE TYPE */}
+1 -1
View File
@@ -97,7 +97,7 @@ export default function UserGroup() {
<Section
title="Users"
titleFz="h3"
titleMb="0"
icon={<ICONS.User size="1.2rem" />}
titleRight={
<Group ml="md">
+1 -1
View File
@@ -56,7 +56,7 @@ export default function User() {
<Section
title="User Permissions"
titleFz="h3"
titleMb="0"
withBorder
>
<Group>
+1 -1
View File
@@ -29,7 +29,7 @@ export default function UserMemberGroups({ userId }: { userId: string }) {
title="Groups"
icon={<ICONS.UserGroup size="1.2rem" />}
titleFz="h3"
titleMb="0"
titleRight={
<Box ml="md">
<UserAddUserGroup userId={userId} />
+2 -2
View File
@@ -161,7 +161,7 @@ export default function ActionConfig({ id }: { id: string }) {
: false
}
disabled={disabled || !(update.schedule ?? config.schedule)}
onChange={(schedule_enabled) => set({ schedule_enabled })}
onCheckedChange={(schedule_enabled) => set({ schedule_enabled })}
/>
),
schedule_format: (schedule_format, set) => (
@@ -262,7 +262,7 @@ export default function ActionConfig({ id }: { id: string }) {
placeholder="Branch"
value={branch}
onChange={(e) => setBranch(e.target.value)}
w={200}
w={{ base: "100%", sm: 200 }}
disabled={branch === "__ANY__"}
/>
<LabelledSwitch
-1
View File
@@ -62,7 +62,6 @@ export default function BuildTabs({ id }: { id: string }) {
tabs={tabsNoContent}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);
+7 -1
View File
@@ -75,8 +75,14 @@ export const BuilderComponents: RequiredResourceComponents<
? builder.info.instance_type
: undefined,
);
const coreVersion = useRead("GetVersion", {}).data?.version;
const intent = server?.info.state
? serverStateIntention(server.info.state)
? serverStateIntention(
server.info.state,
!!coreVersion &&
!!server.info.version &&
coreVersion !== server.info.version,
)
: "Neutral";
return (
<ResourceHeader
+1 -1
View File
@@ -284,7 +284,7 @@ export default function DeploymentConfig({
label="Poll for Updates"
description="Check for updates to the image during Global Auto Update."
value={autoUpdate || poll}
onChange={(poll_for_updates) => set({ poll_for_updates })}
onCheckedChange={(poll_for_updates) => set({ poll_for_updates })}
disabled={disabled || autoUpdate}
/>
);
+16 -6
View File
@@ -103,11 +103,15 @@ export const DeploymentComponents: RequiredResourceComponents<
Table: DeploymentTable,
Icon: ({ id, size = "1rem", noColor }) => {
const state = useRead("ListDeployments", {}).data?.find((r) => r.id === id)
?.info.state;
const info = useRead("ListDeployments", {}).data?.find(
(r) => r.id === id,
)?.info;
const color = noColor
? undefined
: state && hexColorByIntention(deploymentStateIntention(state));
: info &&
hexColorByIntention(
deploymentStateIntention(info.state, info.update_available),
);
return <ICONS.Deployment size={size} color={color} />;
},
@@ -118,7 +122,10 @@ export const DeploymentComponents: RequiredResourceComponents<
type="Deployment"
id={id}
resource={deployment}
intent={deploymentStateIntention(deployment?.info.state)}
intent={deploymentStateIntention(
deployment?.info.state,
deployment?.info.update_available,
)}
icon={ICONS.Deployment}
name={deployment?.name}
state={deployment?.info.state}
@@ -128,9 +135,12 @@ export const DeploymentComponents: RequiredResourceComponents<
},
State: ({ id }) => {
let state = useDeployment(id)?.info.state;
let info = useDeployment(id)?.info;
return (
<StatusBadge text={state} intent={deploymentStateIntention(state)} />
<StatusBadge
text={info?.state}
intent={deploymentStateIntention(info?.state, info?.update_available)}
/>
);
},
Info: {
+3 -2
View File
@@ -91,7 +91,6 @@ export default function DeploymentTabs({ id }: { id: string }) {
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);
@@ -130,7 +129,9 @@ export default function DeploymentTabs({ id }: { id: string }) {
return (
<Tabs
color={colorByIntention(deploymentStateIntention(state))}
color={colorByIntention(
deploymentStateIntention(state, info?.update_available),
)}
value={view}
>
{View}
+1 -1
View File
@@ -115,7 +115,7 @@ export default function NewResource<Config>({
}
: undefined
}
placeholder="Template (Optional)"
placeholder="Select Template"
targetProps={{ w: "100%", maw: "100%" }}
width="target"
position="bottom"
+2 -2
View File
@@ -194,7 +194,7 @@ export default function ProcedureConfig({ id }: { id: string }) {
: false
}
disabled={disabled || !(update.schedule ?? config.schedule)}
onChange={(schedule_enabled) => set({ schedule_enabled })}
onCheckedChange={(schedule_enabled) => set({ schedule_enabled })}
/>
),
schedule_format: (schedule_format, set) => (
@@ -276,7 +276,7 @@ export default function ProcedureConfig({ id }: { id: string }) {
placeholder="Branch"
value={branch}
onChange={(e) => setBranch(e.target.value)}
w={200}
w={{ base: "100%", sm: 200 }}
disabled={branch === "__ANY__"}
/>
<LabelledSwitch
-1
View File
@@ -58,7 +58,6 @@ export default function RepoTabs({ id }: { id: string }) {
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);
+13 -2
View File
@@ -15,6 +15,7 @@ import ServerNetworks from "./networks";
import { ICONS } from "@/theme/icons";
import ServerVolumes from "./volumes";
import ServerImages from "./images";
import { useRead } from "@/lib/hooks";
type ServerDockerView = "Containers" | "Networks" | "Volumes" | "Images";
@@ -30,7 +31,9 @@ export default function ServerDockerResources({
id: string;
titleOther: ReactNode;
}) {
const state = useServer(id)?.info.state ?? Types.ServerState.NotOk;
const coreVersion = useRead("GetVersion", {}).data?.version;
const info = useServer(id)?.info;
const state = info?.state ?? Types.ServerState.NotOk;
const [view, setView] = useLocalStorage<ServerDockerView>({
key: "server-info-view-v1",
defaultValue: "Containers",
@@ -98,7 +101,15 @@ export default function ServerDockerResources({
return (
<Section titleOther={titleOther}>
<Tabs color={colorByIntention(serverStateIntention(state))} value={view}>
<Tabs
color={colorByIntention(
serverStateIntention(
state,
!!coreVersion && !!info?.version && coreVersion !== info.version,
),
)}
value={view}
>
{View}
</Tabs>
</Section>
+11 -2
View File
@@ -133,8 +133,17 @@ export const ServerComponents: RequiredResourceComponents<
},
State: ({ id }) => {
let state = useServer(id)?.info.state;
return <StatusBadge text={state} intent={serverStateIntention(state)} />;
const coreVersion = useRead("GetVersion", {}).data?.version;
const info = useServer(id)?.info;
return (
<StatusBadge
text={info?.state}
intent={serverStateIntention(
info?.state,
!!coreVersion && !!info?.version && coreVersion !== info.version,
)}
/>
);
},
Info: {
ServerVersion,
+2 -2
View File
@@ -39,10 +39,10 @@ type StatType =
type StatDatapoint = { date: number; value: number };
const STAT_TYPES: [StatType, ReactNode][] = [
["Load Average", <ICONS.LoadAvg size="1.1rem" />],
["Cpu", <ICONS.Cpu size="1.1rem" />],
["Memory", <ICONS.Memory size="1.1rem" />],
["Disk", <ICONS.Disk size="1.1rem" />],
["Load Average", <ICONS.LoadAvg size="1.1rem" />],
["Network Ingress", <Download size="1.1rem" />],
["Network Egress", <Upload size="1.1rem" />],
];
@@ -204,7 +204,7 @@ function StatChart({
return (
<Stack gap={4}>
<Group c="dimmed" gap="xs">
<Group gap="xs">
{icon}
<Text fz="xl" fw={500}>
{type}
+9 -2
View File
@@ -26,6 +26,7 @@ export default function ServerTabs({ id }: { id: string }) {
const { specificTerminal } = usePermissions({ type: "Server", id });
const coreVersion = useRead("GetVersion", {}).data?.version;
const serverInfo = useServer(id)?.info;
const notReachable = serverInfo?.state !== Types.ServerState.Ok;
const terminalDisabled =
@@ -83,7 +84,6 @@ export default function ServerTabs({ id }: { id: string }) {
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);
@@ -126,7 +126,14 @@ export default function ServerTabs({ id }: { id: string }) {
return (
<Tabs
color={colorByIntention(serverStateIntention(serverInfo?.state))}
color={colorByIntention(
serverStateIntention(
serverInfo?.state,
!!coreVersion &&
!!serverInfo?.version &&
coreVersion !== serverInfo.version,
),
)}
value={view}
>
{View}
+2 -2
View File
@@ -371,7 +371,7 @@ export default function StackConfig({
label="Poll for Updates"
description="Check for updates to the image during Global Auto Update."
value={auto_update || poll}
onChange={(poll_for_updates) => set({ poll_for_updates })}
onCheckedChange={(poll_for_updates) => set({ poll_for_updates })}
disabled={disabled || auto_update}
/>
);
@@ -385,7 +385,7 @@ export default function StackConfig({
label="Full Stack Auto Update"
description="Always redeploy full stack instead of just specific services with update."
value={value}
onChange={(auto_update_all_services) =>
onCheckedChange={(auto_update_all_services) =>
set({ auto_update_all_services })
}
disabled={disabled || !auto_update}
+25 -6
View File
@@ -109,11 +109,17 @@ export const StackComponents: RequiredResourceComponents<
Table: StackTable,
Icon: ({ id, size = "1rem", noColor }) => {
const state = useRead("ListStacks", {}).data?.find((r) => r.id === id)?.info
.state;
const info = useRead("ListStacks", {}).data?.find((r) => r.id === id)?.info;
const color = noColor
? undefined
: state && hexColorByIntention(stackStateIntention(state));
: info &&
hexColorByIntention(
stackStateIntention(
info.state,
info.services &&
!info.services.every((service) => !service.update_available),
),
);
return <ICONS.Stack size={size} color={color} />;
},
@@ -124,7 +130,11 @@ export const StackComponents: RequiredResourceComponents<
type="Stack"
id={id}
resource={stack}
intent={stackStateIntention(stack?.info.state)}
intent={stackStateIntention(
stack?.info.state,
stack?.info.services &&
!stack?.info.services.every((service) => !service.update_available),
)}
icon={ICONS.Stack}
name={stack?.name}
state={stack?.info.state}
@@ -138,8 +148,17 @@ export const StackComponents: RequiredResourceComponents<
},
State: ({ id }) => {
let state = useStack(id)?.info.state;
return <StatusBadge text={state} intent={stackStateIntention(state)} />;
let info = useStack(id)?.info;
return (
<StatusBadge
text={info?.state}
intent={stackStateIntention(
info?.state,
info?.services &&
!info.services.every((service) => !service.update_available),
)}
/>
);
},
Info: {
DeployTarget: ({ id }) => {
+10 -2
View File
@@ -95,7 +95,6 @@ export default function StackTabs({ id }: { id: string }) {
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);
@@ -135,7 +134,16 @@ export default function StackTabs({ id }: { id: string }) {
}
return (
<Tabs color={colorByIntention(stackStateIntention(state))} value={view}>
<Tabs
color={colorByIntention(
stackStateIntention(
state,
info?.services &&
!info.services.every((service) => !service.update_available),
),
)}
value={view}
>
{View}
</Tabs>
);
+1 -1
View File
@@ -73,7 +73,7 @@ export default function ResourceSubPage({
{canExecute && executions && (
<Section
title="Execute"
titleMb="0"
icon={<ICONS.Execution size="1.3rem" />}
my="md"
>
+1
View File
@@ -76,6 +76,7 @@ export default function SwarmConfig({
}
disabled={disabled}
targetProps={{ w: "90%", maw: "" }}
clearable={false}
/>
{!disabled && (
<ActionIcon variant="filled" color="red">
+1 -2
View File
@@ -21,8 +21,7 @@ export default function JoinSwarmCommands({
return (
<Stack>
<Text c="dimmed">
Copy a command below and run it on the target host to join it to the
swarm.
Copy a command below and run it on the target host to join the swarm.
</Text>
{addr && tokens ? (
-1
View File
@@ -67,7 +67,6 @@ export default function SwarmTabs({ id }: { id: string }) {
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);
+1 -1
View File
@@ -145,7 +145,7 @@ export default function ResourceSyncConfig({
label="Delete Unmatched Resources"
description="Executions will delete any resources not found in the resource files. Only use this when using one sync for everything."
value={managed || delete_mode}
onChange={(delete_mode) => set({ delete: delete_mode })}
onCheckedChange={(delete_mode) => set({ delete: delete_mode })}
disabled={disabled || managed}
/>
);
-1
View File
@@ -44,7 +44,6 @@ export default function ResourceSyncTabs({ id }: { id: string }) {
tabs={tabsNoContent}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);
+9 -5
View File
@@ -1,4 +1,4 @@
import { Dispatch, Fragment, SetStateAction } from "react";
import { Fragment } from "react";
import { ConfigFieldArgs, ConfigGroupArgs } from ".";
import { ConfigInput, ConfigSwitch } from "./item";
import { Group } from "@mantine/core";
@@ -13,7 +13,7 @@ export default function ConfigGroup<T>({
}: {
config: T;
update: Partial<T>;
setUpdate: Dispatch<SetStateAction<Partial<T>>>;
setUpdate: (update: Partial<T>) => void;
disabled: boolean;
fields: ConfigGroupArgs<T>["fields"];
}) {
@@ -38,7 +38,9 @@ export default function ConfigGroup<T>({
key={key}
label={args?.label ?? key}
value={value}
onChange={(value) => setUpdate({ [key]: value } as Partial<T>)}
onValueChange={(value) =>
setUpdate({ [key]: value } as Partial<T>)
}
disabled={args?.disabled || disabled}
placeholder={args?.placeholder}
description={args?.description}
@@ -52,7 +54,7 @@ export default function ConfigGroup<T>({
key={key}
label={args?.label ?? key}
value={Number(value)}
onChange={(value) =>
onValueChange={(value) =>
setUpdate({ [key]: Number(value) } as Partial<T>)
}
disabled={args?.disabled || disabled}
@@ -68,7 +70,9 @@ export default function ConfigGroup<T>({
key={key}
label={args?.label ?? key}
value={value}
onChange={(value) => setUpdate({ [key]: value } as Partial<T>)}
onCheckedChange={(value) =>
setUpdate({ [key]: value } as Partial<T>)
}
disabled={args?.disabled || disabled}
description={args?.description}
boldLabel={args?.boldLabel}
+4 -4
View File
@@ -96,7 +96,7 @@ export default function Config<T>({
</Text>
)}
<Stack gap="xl">
<Stack>
{(groupArgs as ConfigGroupArgs<T>[])
.filter(({ hidden }) => !hidden)
.map(
@@ -189,10 +189,10 @@ export default function Config<T>({
const SaveOrResetComponent = changesMade && (
<>
<Group visibleFrom="lg" justify="flex-end">
<Group visibleFrom="xs" justify="flex-end">
<SaveOrReset unsavedIndicator />
</Group>
<Stack hiddenFrom="lg">
<Stack hiddenFrom="xs">
<SaveOrReset unsavedIndicator fullWidth />
</Stack>
</>
@@ -209,7 +209,7 @@ export default function Config<T>({
{!disableSidebar && (
<Flex
w="100%"
gap={{ base: "md", lg: "xl" }}
gap="md"
direction={{ base: "column", lg: "row" }}
>
{/** SIDEBAR (LG) */}
+4 -4
View File
@@ -119,20 +119,20 @@ export function ConfigInput({
export function ConfigSwitch({
value,
disabled,
onChange,
onCheckedChange,
switchProps,
...itemProps
}: {
value: boolean | undefined;
disabled: boolean;
onChange: (value: boolean) => void;
onCheckedChange: (value: boolean) => void;
switchProps?: SwitchProps;
} & Omit<Omit<ConfigItemProps, "children">, "onChange">) {
} & Omit<ConfigItemProps, "children">) {
return (
<ConfigItem {...itemProps}>
<EnableSwitch
checked={value}
onCheckedChange={onChange}
onCheckedChange={onCheckedChange}
disabled={disabled}
{...switchProps}
/>
+12 -1
View File
@@ -26,7 +26,17 @@ export interface ConfirmButtonProps extends ButtonProps {
const ConfirmButton = createPolymorphicComponent<"button", ConfirmButtonProps>(
forwardRef<HTMLButtonElement, ConfirmButtonProps>(
(
{ icon, rightSection, children, onClick, onBlur, miw, loading, ...props },
{
icon,
rightSection,
children,
onClick,
onBlur,
miw,
loading,
disabled,
...props
},
ref,
) => {
const [clickedOnce, setClickedOnce] = useState(false);
@@ -65,6 +75,7 @@ const ConfirmButton = createPolymorphicComponent<"button", ConfirmButtonProps>(
(rightSection ?? icon ?? <ICONS.Unknown size="1rem" />)
)
}
disabled={disabled || loading}
{...props}
ref={ref}
>
+2 -1
View File
@@ -23,7 +23,7 @@ export interface ConfirmIconProps extends ActionIconProps {
const ConfirmIcon = createPolymorphicComponent<"button", ConfirmIconProps>(
forwardRef<HTMLButtonElement, ConfirmIconProps>(
({ children, onClick, onBlur, miw, loading, ...props }, ref) => {
({ children, onClick, onBlur, miw, loading, disabled, ...props }, ref) => {
const [clickedOnce, setClickedOnce] = useState(false);
useEffect(() => {
if (clickedOnce) {
@@ -49,6 +49,7 @@ const ConfirmIcon = createPolymorphicComponent<"button", ConfirmIconProps>(
onBlur?.(e);
}}
onPointerDown={(e) => e.stopPropagation()}
disabled={disabled || loading}
{...props}
ref={ref}
>
+1 -1
View File
@@ -131,7 +131,6 @@ export default function ConfirmModal({
</Modal>
<Button
disabled={disabled}
onClick={(e) => {
e.stopPropagation();
open();
@@ -147,6 +146,7 @@ export default function ConfirmModal({
)
}
loading={targetNoIcon ? loading : undefined}
disabled={disabled || loading}
{...targetProps}
>
{children}
+2 -2
View File
@@ -14,8 +14,8 @@ export default function EntityPage({
...props
}: EntityPageProps) {
return (
<Stack gap="xl" mb="50vh" {...props}>
<Group justify="space-between" mb="lg">
<Stack mb="50vh" {...props}>
<Group justify="space-between">
<BackButton to={backTo} />
{actions && <Group wrap="nowrap">{actions}</Group>}
</Group>
+1 -1
View File
@@ -33,7 +33,7 @@ export default function LabelledSwitch({
}}
className="bordered-light"
px="xs"
py={6}
py={4}
bdrs="sm"
style={{ cursor: "pointer" }}
justify="space-between"
+3 -1
View File
@@ -77,12 +77,13 @@ export function MobileFriendlyTabsSelector({
actions,
value,
onValueChange,
changeAt = "md",
changeAt: _changeAt,
fullIconSize = "1.3rem",
mobileIconSize = "1rem",
tabProps,
}: MobileFriendlyTabsSelectorProps) {
const tabs = _tabs.filter((t) => !t.hidden);
const changeAt = _changeAt ?? (tabs.length > 4 ? "lg" : "md");
const SelectedIcon = tabs.find((tab) => tab.value === value)?.icon;
return (
<>
@@ -102,6 +103,7 @@ export function MobileFriendlyTabsSelector({
fz="h3"
justify="center"
c={tabValue === value ? undefined : "dimmed"}
w={tabProps?.w ?? 140}
{...tabProps}
>
{Icon && <Icon size={fullIconSize} />}
+1 -2
View File
@@ -50,7 +50,6 @@ const Page = createPolymorphicComponent<"div", PageProps>(
w={{ base: "100%", xs: "fit-content" }}
px="xl"
py="xs"
mb="lg"
style={{
border: "1px solid var(--mantine-color-accent-border-0)",
}}
@@ -80,7 +79,7 @@ const Page = createPolymorphicComponent<"div", PageProps>(
</Paper>
);
return (
<Stack gap="xl" {...stackProps} ref={ref}>
<Stack {...stackProps} ref={ref}>
{aboveTitle}
{oppositeTitle ? (
<Group justify="space-between" align="start">
+1 -1
View File
@@ -35,7 +35,7 @@ const Section = createPolymorphicComponent<"div", SectionProps>(
(
{
titleFz = "h2",
titleMb = { base: "0", lg: "sm" },
titleMb,
title,
titleDimmed,
titleNode,