Merge pull request 'tracking machine install state' (#4803) from feat/machine-install-state into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4803
This commit is contained in:
@@ -255,6 +255,16 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
installedAt = lib.mkOption {
|
||||
type = types.nullOr types.int;
|
||||
default = null;
|
||||
description = ''
|
||||
Indicates when the machine was first installed.
|
||||
|
||||
Timestamp is in unix time (seconds since epoch).
|
||||
'';
|
||||
};
|
||||
|
||||
tags = lib.mkOption {
|
||||
description = ''
|
||||
List of tags for the machine.
|
||||
|
||||
6223
pkgs/clan-app/ui/api/Inventory.ts
Normal file
6223
pkgs/clan-app/ui/api/Inventory.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
span.machine-status {
|
||||
@apply flex items-center gap-1;
|
||||
@apply flex items-center gap-1.5;
|
||||
|
||||
.indicator {
|
||||
@apply w-1.5 h-1.5 rounded-full m-1.5;
|
||||
@@ -13,7 +13,7 @@ span.machine-status {
|
||||
background-color: theme(colors.fg.semantic.error.1);
|
||||
}
|
||||
|
||||
&.installed > .indicator {
|
||||
&.out-of-sync > .indicator {
|
||||
background-color: theme(colors.fg.inv.3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,27 +20,38 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<MachineStatusProps>;
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Online: Story = {
|
||||
args: {
|
||||
status: "Online",
|
||||
status: "online",
|
||||
},
|
||||
};
|
||||
|
||||
export const Offline: Story = {
|
||||
args: {
|
||||
status: "Offline",
|
||||
status: "offline",
|
||||
},
|
||||
};
|
||||
|
||||
export const Installed: Story = {
|
||||
export const OutOfSync: Story = {
|
||||
args: {
|
||||
status: "Installed",
|
||||
status: "out_of_sync",
|
||||
},
|
||||
};
|
||||
|
||||
export const NotInstalled: Story = {
|
||||
args: {
|
||||
status: "Not Installed",
|
||||
status: "not_installed",
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingWithLabel: Story = {
|
||||
args: {
|
||||
...Loading.args,
|
||||
label: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -60,7 +71,7 @@ export const OfflineWithLabel: Story = {
|
||||
|
||||
export const InstalledWithLabel: Story = {
|
||||
args: {
|
||||
...Installed.args,
|
||||
...OutOfSync.args,
|
||||
label: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,41 +2,58 @@ import "./MachineStatus.css";
|
||||
|
||||
import { Badge } from "@kobalte/core/badge";
|
||||
import cx from "classnames";
|
||||
import { Show } from "solid-js";
|
||||
import { Match, Show, Switch } from "solid-js";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
|
||||
export type MachineStatus =
|
||||
| "Online"
|
||||
| "Offline"
|
||||
| "Installed"
|
||||
| "Not Installed";
|
||||
import { MachineStatus as MachineStatusModel } from "@/src/hooks/queries";
|
||||
import { Loader } from "../Loader/Loader";
|
||||
|
||||
export interface MachineStatusProps {
|
||||
label?: boolean;
|
||||
status: MachineStatus;
|
||||
status?: MachineStatusModel;
|
||||
}
|
||||
|
||||
export const MachineStatus = (props: MachineStatusProps) => (
|
||||
<Badge
|
||||
class={cx("machine-status", {
|
||||
online: props.status == "Online",
|
||||
offline: props.status == "Offline",
|
||||
installed: props.status == "Installed",
|
||||
"not-installed": props.status == "Not Installed",
|
||||
})}
|
||||
textValue={props.status}
|
||||
>
|
||||
{props.label && (
|
||||
<Typography hierarchy="label" size="xs" weight="medium" inverted={true}>
|
||||
{props.status}
|
||||
</Typography>
|
||||
)}
|
||||
<Show
|
||||
when={props.status == "Not Installed"}
|
||||
fallback={<div class="indicator" />}
|
||||
>
|
||||
<Icon icon="Offline" inverted={true} />
|
||||
</Show>
|
||||
</Badge>
|
||||
);
|
||||
export const MachineStatus = (props: MachineStatusProps) => {
|
||||
const status = () => props.status;
|
||||
|
||||
// remove the '_' from the enum
|
||||
// we will use css transform in the typography component to capitalize
|
||||
const statusText = () => props.status?.replaceAll("_", " ");
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={!status()}>
|
||||
<Loader />
|
||||
</Match>
|
||||
<Match when={status()}>
|
||||
<Badge
|
||||
class={cx("machine-status", {
|
||||
online: status() == "online",
|
||||
offline: status() == "offline",
|
||||
"out-of-sync": status() == "out_of_sync",
|
||||
"not-installed": status() == "not_installed",
|
||||
})}
|
||||
textValue={status()}
|
||||
>
|
||||
{props.label && (
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={true}
|
||||
transform="capitalize"
|
||||
>
|
||||
{statusText()}
|
||||
</Typography>
|
||||
)}
|
||||
<Show
|
||||
when={status() != "not_installed"}
|
||||
fallback={<Icon icon="Offline" inverted={true} />}
|
||||
>
|
||||
<div class="indicator" />
|
||||
</Show>
|
||||
</Badge>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
div.sidebar {
|
||||
.sidebar {
|
||||
@apply w-60 border-none z-10;
|
||||
|
||||
& > div.header {
|
||||
}
|
||||
|
||||
& > div.body {
|
||||
.body {
|
||||
@apply pt-4 pb-3 px-2;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import "./Sidebar.css";
|
||||
import styles from "./Sidebar.module.css";
|
||||
import { SidebarHeader } from "@/src/components/Sidebar/SidebarHeader";
|
||||
import { SidebarBody } from "@/src/components/Sidebar/SidebarBody";
|
||||
import cx from "classnames";
|
||||
|
||||
export interface LinkProps {
|
||||
path: string;
|
||||
@@ -13,16 +14,15 @@ export interface SectionProps {
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
class?: string;
|
||||
staticSections?: SectionProps[];
|
||||
}
|
||||
|
||||
export const Sidebar = (props: SidebarProps) => {
|
||||
return (
|
||||
<>
|
||||
<div class="sidebar">
|
||||
<SidebarHeader />
|
||||
<SidebarBody {...props} />
|
||||
</div>
|
||||
</>
|
||||
<div class={cx(styles.sidebar, props.class)}>
|
||||
<SidebarHeader />
|
||||
<SidebarBody class={cx(styles.body)} {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,47 +6,53 @@ import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { For } from "solid-js";
|
||||
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
||||
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
|
||||
import { useMachinesQuery } from "@/src/hooks/queries";
|
||||
import { useMachinesQuery, useMachineStateQuery } from "@/src/hooks/queries";
|
||||
import { SidebarProps } from "./Sidebar";
|
||||
|
||||
interface MachineProps {
|
||||
clanURI: string;
|
||||
machineID: string;
|
||||
name: string;
|
||||
status: MachineStatus;
|
||||
serviceCount: number;
|
||||
}
|
||||
|
||||
const MachineRoute = (props: MachineProps) => (
|
||||
<A href={buildMachinePath(props.clanURI, props.machineID)}>
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
inverted={true}
|
||||
>
|
||||
{props.name}
|
||||
</Typography>
|
||||
<MachineStatus status={props.status} />
|
||||
const MachineRoute = (props: MachineProps) => {
|
||||
const statusQuery = useMachineStateQuery(props.clanURI, props.machineID);
|
||||
|
||||
const status = () =>
|
||||
statusQuery.isSuccess ? statusQuery.data.status : undefined;
|
||||
|
||||
return (
|
||||
<A href={buildMachinePath(props.clanURI, props.machineID)}>
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
inverted={true}
|
||||
>
|
||||
{props.name}
|
||||
</Typography>
|
||||
<MachineStatus status={status()} />
|
||||
</div>
|
||||
<div class="flex w-full flex-row items-center gap-1">
|
||||
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="s"
|
||||
inverted={true}
|
||||
color="primary"
|
||||
>
|
||||
{props.serviceCount}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row items-center gap-1">
|
||||
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="s"
|
||||
inverted={true}
|
||||
color="primary"
|
||||
>
|
||||
{props.serviceCount}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</A>
|
||||
);
|
||||
</A>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarBody = (props: SidebarProps) => {
|
||||
const clanURI = useClanURI();
|
||||
@@ -96,7 +102,6 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
clanURI={clanURI}
|
||||
machineID={id}
|
||||
name={machine.name || id}
|
||||
status="Not Installed"
|
||||
serviceCount={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.machineStatus {
|
||||
@apply flex flex-col gap-2 w-full;
|
||||
|
||||
.summary {
|
||||
@apply flex flex-row justify-between items-center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import styles from "./SidebarMachineStatus.module.css";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { useMachineStateQuery } from "@/src/hooks/queries";
|
||||
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
||||
|
||||
export interface SidebarMachineStatusProps {
|
||||
class?: string;
|
||||
clanURI: string;
|
||||
machineName: string;
|
||||
}
|
||||
|
||||
export const SidebarMachineStatus = (props: SidebarMachineStatusProps) => {
|
||||
const query = useMachineStateQuery(props.clanURI, props.machineName);
|
||||
|
||||
return (
|
||||
<div class={styles.machineStatus}>
|
||||
<div class={styles.summary}>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={true}
|
||||
color="tertiary"
|
||||
>
|
||||
Status
|
||||
</Typography>
|
||||
<MachineStatus
|
||||
label
|
||||
status={query.isSuccess ? query.data.status : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ div.sidebar-pane {
|
||||
animation: sidebarPaneHide 250ms ease-out 300ms forwards;
|
||||
|
||||
& > div.header > *,
|
||||
& > div.sub-header > *,
|
||||
& > div.body > * {
|
||||
animation: sidebarFadeOut 250ms ease-out forwards;
|
||||
}
|
||||
@@ -35,6 +36,25 @@ div.sidebar-pane {
|
||||
}
|
||||
}
|
||||
|
||||
& > div.sub-header {
|
||||
@apply px-3 py-1;
|
||||
@apply border-b-[1px] border-b-bg-inv-4;
|
||||
@apply border-r-[1px] border-r-bg-inv-3 border-l-[1px] border-l-bg-inv-3;
|
||||
|
||||
background:
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.08) 0%, rgba(0, 0, 0, 0.08) 100%),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
theme(colors.bg.inv.3) 0%,
|
||||
theme(colors.bg.inv.4) 100%
|
||||
);
|
||||
|
||||
& > * {
|
||||
@apply opacity-0;
|
||||
animation: sidebarFadeIn 250ms ease-in 250ms forwards;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.body {
|
||||
@apply flex flex-col gap-4 px-2 pt-4 pb-3 w-full h-full;
|
||||
@apply backdrop-blur-md;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, JSX, onMount } from "solid-js";
|
||||
import { createSignal, JSX, onMount, Show } from "solid-js";
|
||||
import "./SidebarPane.css";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import Icon from "../Icon/Icon";
|
||||
@@ -6,8 +6,10 @@ import { Button as KButton } from "@kobalte/core/button";
|
||||
import cx from "classnames";
|
||||
|
||||
export interface SidebarPaneProps {
|
||||
class?: string;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
subHeader?: () => JSX.Element;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
@@ -26,7 +28,12 @@ export const SidebarPane = (props: SidebarPaneProps) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={cx("sidebar-pane", { closing: closing(), open: open() })}>
|
||||
<div
|
||||
class={cx("sidebar-pane", props.class, {
|
||||
closing: closing(),
|
||||
open: open(),
|
||||
})}
|
||||
>
|
||||
<div class="header">
|
||||
<Typography hierarchy="body" size="s" weight="bold" inverted={true}>
|
||||
{props.title}
|
||||
@@ -35,6 +42,9 @@ export const SidebarPane = (props: SidebarPaneProps) => {
|
||||
<Icon icon="Close" color="primary" size="0.75rem" inverted={true} />
|
||||
</KButton>
|
||||
</div>
|
||||
<Show when={props.subHeader}>
|
||||
<div class="sub-header">{props.subHeader!()}</div>
|
||||
</Show>
|
||||
<div class="body">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.install {
|
||||
@apply flex flex-col gap-4 w-full justify-center items-center;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { InstallModal } from "@/src/workflows/Install/install";
|
||||
import { useMachineName } from "@/src/hooks/clan";
|
||||
import { useMachineStateQuery } from "@/src/hooks/queries";
|
||||
import styles from "./SidebarSectionInstall.module.css";
|
||||
import { Alert } from "../Alert/Alert";
|
||||
|
||||
export interface SidebarSectionInstallProps {
|
||||
clanURI: string;
|
||||
machineName: string;
|
||||
}
|
||||
|
||||
export const SidebarSectionInstall = (props: SidebarSectionInstallProps) => {
|
||||
const query = useMachineStateQuery(props.clanURI, props.machineName);
|
||||
|
||||
const [showInstall, setShowModal] = createSignal(false);
|
||||
|
||||
return (
|
||||
<Show when={query.isSuccess && query.data.status == "not_installed"}>
|
||||
<div class={styles.install}>
|
||||
<Alert
|
||||
type="warning"
|
||||
size="s"
|
||||
title="Your machine is not installed yet"
|
||||
description="Start the process by clicking the button below."
|
||||
></Alert>
|
||||
<Button hierarchy="primary" size="s" onClick={() => setShowModal(true)}>
|
||||
Install machine
|
||||
</Button>
|
||||
<Show when={showInstall()}>
|
||||
<InstallModal
|
||||
open={showInstall()}
|
||||
machineName={useMachineName()}
|
||||
onClose={() => setShowModal(false)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,10 @@ export type ClanDetailsWithURI = ClanDetails & { uri: string };
|
||||
|
||||
export type Tags = SuccessData<"list_tags">;
|
||||
export type Machine = SuccessData<"get_machine">;
|
||||
|
||||
export type MachineState = SuccessData<"get_machine_state">;
|
||||
export type MachineStatus = MachineState["status"];
|
||||
|
||||
export type ListMachines = SuccessData<"list_machines">;
|
||||
export type MachineDetails = SuccessData<"get_machine_details">;
|
||||
|
||||
@@ -94,6 +98,33 @@ export const useMachineQuery = (clanURI: string, machineName: string) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const useMachineStateQuery = (clanURI: string, machineName: string) => {
|
||||
const client = useApiClient();
|
||||
return useQuery<MachineState>(() => ({
|
||||
queryKey: ["clans", encodeBase64(clanURI), "machine", machineName, "state"],
|
||||
refetchInterval: 1000 * 60, // poll every 60 seconds
|
||||
queryFn: async () => {
|
||||
const apiCall = client.fetch("get_machine_state", {
|
||||
machine: {
|
||||
name: machineName,
|
||||
flake: {
|
||||
identifier: clanURI,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await apiCall.result;
|
||||
if (result.status === "error") {
|
||||
throw new Error(
|
||||
"Error fetching machine status: " + result.errors[0].message,
|
||||
);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const useMachineDetailsQuery = (
|
||||
clanURI: string,
|
||||
machineName: string,
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
.fade-out {
|
||||
.fadeOut {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.create-backdrop {
|
||||
.createBackdrop {
|
||||
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.create-modal {
|
||||
.createModal {
|
||||
@apply min-w-96;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
}
|
||||
.sidebar {
|
||||
@apply absolute left-4 top-10 w-60;
|
||||
@apply min-h-96;
|
||||
|
||||
div.sidebar {
|
||||
@apply absolute top-10 bottom-20 left-4 w-60;
|
||||
}
|
||||
|
||||
div.sidebar-pane {
|
||||
@apply absolute top-12 bottom-20 left-[16.5rem] w-64;
|
||||
height: calc(100vh - 8rem);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import { produce } from "solid-js/store";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Splash } from "@/src/scene/splash";
|
||||
import cx from "classnames";
|
||||
import "./Clan.css";
|
||||
import styles from "./Clan.module.css";
|
||||
import { Modal } from "@/src/components/Modal/Modal";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { createForm, FieldValues, reset } from "@modular-forms/solid";
|
||||
@@ -37,7 +37,7 @@ import { useNavigate } from "@solidjs/router";
|
||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Sidebar />
|
||||
<Sidebar class={cx(styles.sidebar)} />
|
||||
{props.children}
|
||||
<ClanSceneController {...props} />
|
||||
</>
|
||||
@@ -59,7 +59,7 @@ const MockCreateMachine = (props: MockProps) => {
|
||||
const [form, { Form, Field, FieldArray }] = createForm<CreateFormValues>();
|
||||
|
||||
return (
|
||||
<div ref={(el) => (container = el)} class="create-backdrop">
|
||||
<div ref={(el) => (container = el)} class={cx(styles.createBackdrop)}>
|
||||
<Modal
|
||||
open={true}
|
||||
mount={container!}
|
||||
@@ -67,7 +67,7 @@ const MockCreateMachine = (props: MockProps) => {
|
||||
reset(form);
|
||||
props.onClose();
|
||||
}}
|
||||
class="create-modal"
|
||||
class={cx(styles.createModal)}
|
||||
title="Create Machine"
|
||||
>
|
||||
{() => (
|
||||
@@ -234,7 +234,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
||||
</Button>
|
||||
<div
|
||||
class={cx({
|
||||
"fade-out": !machinesQuery.isLoading && loadingCooldown(),
|
||||
[styles.fadeOut]: !machinesQuery.isLoading && loadingCooldown(),
|
||||
})}
|
||||
>
|
||||
<Splash />
|
||||
|
||||
6
pkgs/clan-app/ui/src/routes/Machine/Machine.module.css
Normal file
6
pkgs/clan-app/ui/src/routes/Machine/Machine.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.sidebarPane {
|
||||
@apply absolute left-[16.5rem] top-12 w-64;
|
||||
@apply min-h-96;
|
||||
|
||||
height: calc(100vh - 10rem);
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import { SidebarPane } from "@/src/components/Sidebar/SidebarPane";
|
||||
import { navigateToClan, useClanURI, useMachineName } from "@/src/hooks/clan";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { Show } from "solid-js";
|
||||
import { SectionGeneral } from "./SectionGeneral";
|
||||
import { InstallModal } from "@/src/workflows/Install/install";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Machine as MachineModel, useMachineQuery } from "@/src/hooks/queries";
|
||||
import { SectionTags } from "@/src/routes/Machine/SectionTags";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { SidebarMachineStatus } from "@/src/components/Sidebar/SidebarMachineStatus";
|
||||
import { SidebarSectionInstall } from "@/src/components/Sidebar/SidebarSectionInstall";
|
||||
|
||||
import cx from "classnames";
|
||||
import styles from "./Machine.module.css";
|
||||
|
||||
export const Machine = (props: RouteSectionProps) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -18,10 +21,6 @@ export const Machine = (props: RouteSectionProps) => {
|
||||
navigateToClan(navigate, clanURI);
|
||||
};
|
||||
|
||||
const [showInstall, setShowModal] = createSignal(false);
|
||||
|
||||
let container: Node;
|
||||
|
||||
const sidebarPane = (machineName: string) => {
|
||||
const machineQuery = useMachineQuery(clanURI, machineName);
|
||||
|
||||
@@ -53,7 +52,15 @@ export const Machine = (props: RouteSectionProps) => {
|
||||
const sectionProps = { clanURI, machineName, onSubmit, machineQuery };
|
||||
|
||||
return (
|
||||
<SidebarPane title={machineName} onClose={onClose}>
|
||||
<SidebarPane
|
||||
class={cx(styles.sidebarPane)}
|
||||
title={machineName}
|
||||
onClose={onClose}
|
||||
subHeader={() => (
|
||||
<SidebarMachineStatus clanURI={clanURI} machineName={machineName} />
|
||||
)}
|
||||
>
|
||||
<SidebarSectionInstall clanURI={clanURI} machineName={machineName} />
|
||||
<SectionGeneral {...sectionProps} />
|
||||
<SectionTags {...sectionProps} />
|
||||
</SidebarPane>
|
||||
@@ -62,22 +69,6 @@ export const Machine = (props: RouteSectionProps) => {
|
||||
|
||||
return (
|
||||
<Show when={useMachineName()} keyed>
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
onClick={() => setShowModal(true)}
|
||||
class="absolute right-0 top-0 m-4"
|
||||
>
|
||||
Install me!
|
||||
</Button>
|
||||
{/* Unmount the whole component to destroy the store and form values */}
|
||||
<Show when={showInstall()}>
|
||||
<InstallModal
|
||||
open={showInstall()}
|
||||
machineName={useMachineName()}
|
||||
mount={container!}
|
||||
onClose={() => setShowModal(false)}
|
||||
/>
|
||||
</Show>
|
||||
{sidebarPane(useMachineName())}
|
||||
</Show>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
@apply absolute bottom-8 z-10 w-full;
|
||||
@apply absolute bottom-10 z-10 w-full;
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
/nix/store/q012qk78pwldxl3qjy09nwrx9jlamivm-nixpkgs
|
||||
/nix/store/apspgd56g9qy6fca8d44qnhdaiqrdf2c-nixpkgs
|
||||
@@ -1,3 +1,4 @@
|
||||
from enum import StrEnum
|
||||
from typing import TypedDict
|
||||
|
||||
from clan_lib.api import API
|
||||
@@ -25,6 +26,18 @@ class ListOptions(TypedDict):
|
||||
filter: MachineFilter
|
||||
|
||||
|
||||
class MachineStatus(StrEnum):
|
||||
NOT_INSTALLED = "not_installed"
|
||||
OFFLINE = "offline"
|
||||
OUT_OF_SYNC = "out_of_sync"
|
||||
ONLINE = "online"
|
||||
|
||||
|
||||
class MachineState(TypedDict):
|
||||
status: MachineStatus
|
||||
# add more info later when retrieving remote state
|
||||
|
||||
|
||||
@API.register
|
||||
def list_machines(
|
||||
flake: Flake, opts: ListOptions | None = None
|
||||
@@ -154,3 +167,47 @@ def get_machine_fields_schema(machine: Machine) -> dict[str, FieldSchema]:
|
||||
}
|
||||
for field in field_names
|
||||
}
|
||||
|
||||
|
||||
@API.register
|
||||
def list_machine_state(flake: Flake) -> dict[str, MachineState]:
|
||||
"""
|
||||
Retrieve the current state of all machines in the clan.
|
||||
|
||||
Args:
|
||||
flake (Flake): The flake object representing the configuration source.
|
||||
"""
|
||||
inventory_store = InventoryStore(flake=flake)
|
||||
inventory = inventory_store.read()
|
||||
|
||||
# todo integrate with remote state when implementing https://git.clan.lol/clan/clan-core/issues/4748
|
||||
machines = inventory.get("machines", {})
|
||||
|
||||
return {
|
||||
machine_name: MachineState(
|
||||
status=MachineStatus.OFFLINE
|
||||
if get_value_by_path(machine, "installedAt", None)
|
||||
else MachineStatus.NOT_INSTALLED
|
||||
)
|
||||
for machine_name, machine in machines.items()
|
||||
}
|
||||
|
||||
|
||||
@API.register
|
||||
def get_machine_state(machine: Machine) -> MachineState:
|
||||
"""
|
||||
Retrieve the current state of the machine.
|
||||
|
||||
Args:
|
||||
machine (Machine): The machine object for which we want to retrieve the latest state.
|
||||
"""
|
||||
inventory_store = InventoryStore(flake=machine.flake)
|
||||
inventory = inventory_store.read()
|
||||
|
||||
# todo integrate with remote state when implementing https://git.clan.lol/clan/clan-core/issues/4748
|
||||
|
||||
return MachineState(
|
||||
status=MachineStatus.OFFLINE
|
||||
if get_value_by_path(inventory, f"machines.{machine.name}.installedAt", None)
|
||||
else MachineStatus.NOT_INSTALLED
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import cast
|
||||
from unittest.mock import ANY, patch
|
||||
@@ -12,7 +13,16 @@ from clan_lib.nix_models.clan import Clan, InventoryMachine, Unknown
|
||||
from clan_lib.persist.inventory_store import InventoryStore
|
||||
from clan_lib.persist.util import get_value_by_path, set_value_by_path
|
||||
|
||||
from .actions import get_machine, get_machine_fields_schema, list_machines, set_machine
|
||||
from .actions import (
|
||||
MachineState,
|
||||
MachineStatus,
|
||||
get_machine,
|
||||
get_machine_fields_schema,
|
||||
get_machine_state,
|
||||
list_machine_state,
|
||||
list_machines,
|
||||
set_machine,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
@@ -219,7 +229,44 @@ def test_get_machine_writeability(clan_flake: Callable[..., Flake]) -> None:
|
||||
"deploy.buildHost",
|
||||
"description",
|
||||
"icon",
|
||||
"installedAt",
|
||||
}
|
||||
assert read_only_fields == {"machineClass", "name"}
|
||||
|
||||
assert write_info["tags"]["readonly_members"] == ["nix1", "all", "nixos"]
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_machine_state(clan_flake: Callable[..., Flake]) -> None:
|
||||
now = int(time.time())
|
||||
yesterday = now - 86400
|
||||
last_week = now - 604800
|
||||
|
||||
flake = clan_flake(
|
||||
# clan.nix, cannot be changed
|
||||
clan={
|
||||
"inventory": {
|
||||
"machines": {
|
||||
"jon": {},
|
||||
"sara": {"installedAt": yesterday},
|
||||
"bob": {"installedAt": last_week},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert list_machine_state(flake) == {
|
||||
"jon": MachineState(status=MachineStatus.NOT_INSTALLED),
|
||||
"sara": MachineState(status=MachineStatus.OFFLINE),
|
||||
"bob": MachineState(status=MachineStatus.OFFLINE),
|
||||
}
|
||||
|
||||
assert get_machine_state(Machine("jon", flake)) == MachineState(
|
||||
status=MachineStatus.NOT_INSTALLED
|
||||
)
|
||||
assert get_machine_state(Machine("sara", flake)) == MachineState(
|
||||
status=MachineStatus.OFFLINE
|
||||
)
|
||||
assert get_machine_state(Machine("bob", flake)) == MachineState(
|
||||
status=MachineStatus.OFFLINE
|
||||
)
|
||||
|
||||
@@ -72,6 +72,7 @@ class InventoryMachineDeploy(TypedDict):
|
||||
InventoryMachineDeployType = InventoryMachineDeploy
|
||||
InventoryMachineDescriptionType = str | None
|
||||
InventoryMachineIconType = str | None
|
||||
InventoryMachineInstalledatType = int | None
|
||||
InventoryMachineMachineclassType = Literal["nixos", "darwin"]
|
||||
InventoryMachineNameType = str
|
||||
InventoryMachineTagsType = list[str]
|
||||
@@ -80,6 +81,7 @@ class InventoryMachine(TypedDict):
|
||||
deploy: NotRequired[InventoryMachineDeployType]
|
||||
description: NotRequired[InventoryMachineDescriptionType]
|
||||
icon: NotRequired[InventoryMachineIconType]
|
||||
installedAt: NotRequired[InventoryMachineInstalledatType]
|
||||
machineClass: NotRequired[InventoryMachineMachineclassType]
|
||||
name: NotRequired[InventoryMachineNameType]
|
||||
tags: NotRequired[InventoryMachineTagsType]
|
||||
|
||||
Reference in New Issue
Block a user