Merge pull request 'ui/integrate-clan-tags-machine-detail' (#4716) from ui/integrate-clan-tags-machine-detail into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4716
This commit is contained in:
brianmcgee
2025-08-12 14:20:27 +00:00
4 changed files with 54 additions and 26 deletions

View File

@@ -11,7 +11,7 @@ import { Label } from "@/src/components/Form/Label";
import { Orienter } from "@/src/components/Form/Orienter"; import { Orienter } from "@/src/components/Form/Orienter";
import { CollectionNode } from "@kobalte/core"; import { CollectionNode } from "@kobalte/core";
interface MachineTag { export interface MachineTag {
value: string; value: string;
disabled?: boolean; disabled?: boolean;
new?: boolean; new?: boolean;
@@ -24,11 +24,10 @@ export type MachineTagsProps = FieldProps & {
disabled?: boolean; disabled?: boolean;
required?: boolean; required?: boolean;
defaultValue?: string[]; defaultValue?: string[];
defaultOptions?: string[];
readonlyOptions?: string[];
}; };
// tags which are applied to all machines and cannot be removed
const staticOptions = [{ value: "all", disabled: true }];
const uniqueOptions = (options: MachineTag[]) => { const uniqueOptions = (options: MachineTag[]) => {
const record: Record<string, MachineTag> = {}; const record: Record<string, MachineTag> = {};
options.forEach((option) => { options.forEach((option) => {
@@ -39,13 +38,8 @@ const uniqueOptions = (options: MachineTag[]) => {
return Object.values(record); return Object.values(record);
}; };
const sortedOptions = (options: MachineTag[]) => { const sortedOptions = (options: MachineTag[]) =>
return options.sort((a, b) => { options.sort((a, b) => a.value.localeCompare(b.value));
if (a.new && !b.new) return -1;
if (a.disabled && !b.disabled) return -1;
return a.value.localeCompare(b.value);
});
};
const sortedAndUniqueOptions = (options: MachineTag[]) => const sortedAndUniqueOptions = (options: MachineTag[]) =>
sortedOptions(uniqueOptions(options)); sortedOptions(uniqueOptions(options));
@@ -72,9 +66,15 @@ export const MachineTags = (props: MachineTagsProps) => {
(props.defaultValue || []).map((value) => ({ value })), (props.defaultValue || []).map((value) => ({ value })),
); );
// todo this should be the superset of tags used across the entire clan and be passed in via a prop // convert default options string[] into MachineTag[]
const [availableOptions, setAvailableOptions] = createSignal<MachineTag[]>( const [availableOptions, setAvailableOptions] = createSignal<MachineTag[]>(
sortedAndUniqueOptions([...staticOptions, ...defaultValue]), sortedAndUniqueOptions([
...(props.readonlyOptions || []).map((value) => ({
value,
disabled: true,
})),
...(props.defaultOptions || []).map((value) => ({ value })),
]),
); );
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {

View File

@@ -1,9 +1,9 @@
import { FieldSchema } from "@/src/hooks/queries"; import { SuccessData } from "@/src/hooks/api";
import { Maybe } from "@modular-forms/solid"; import { Maybe } from "@modular-forms/solid";
export const tooltipText = <T extends object, K extends keyof T>( export const tooltipText = (
name: K, name: string,
schema: FieldSchema<T>, schema: SuccessData<"get_machine_fields_schema">,
staticValue: Maybe<string> = undefined, staticValue: Maybe<string> = undefined,
): Maybe<string> => { ): Maybe<string> => {
const entry = schema[name]; const entry = schema[name];

View File

@@ -6,20 +6,15 @@ import { useApiClient } from "./ApiClient";
export type ClanDetails = SuccessData<"get_clan_details">; export type ClanDetails = SuccessData<"get_clan_details">;
export type ClanDetailsWithURI = ClanDetails & { uri: string }; export type ClanDetailsWithURI = ClanDetails & { uri: string };
export type FieldSchema<T> = { export type Tags = SuccessData<"list_tags">;
[K in keyof T]: {
readonly: boolean;
reason?: string;
};
};
export type Machine = SuccessData<"get_machine">; export type Machine = SuccessData<"get_machine">;
export type ListMachines = SuccessData<"list_machines">; export type ListMachines = SuccessData<"list_machines">;
export type MachineDetails = SuccessData<"get_machine_details">; export type MachineDetails = SuccessData<"get_machine_details">;
export interface MachineDetail { export interface MachineDetail {
tags: Tags;
machine: Machine; machine: Machine;
fieldsSchema: FieldSchema<Machine>; fieldsSchema: SuccessData<"get_machine_fields_schema">;
} }
export type MachinesQueryResult = UseQueryResult<ListMachines>; export type MachinesQueryResult = UseQueryResult<ListMachines>;
@@ -50,7 +45,12 @@ export const useMachineQuery = (clanURI: string, machineName: string) => {
return useQuery<MachineDetail>(() => ({ return useQuery<MachineDetail>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "machine", machineName], queryKey: ["clans", encodeBase64(clanURI), "machine", machineName],
queryFn: async () => { queryFn: async () => {
const [machineCall, schemaCall] = await Promise.all([ const [tagsCall, machineCall, schemaCall] = await Promise.all([
client.fetch("list_tags", {
flake: {
identifier: clanURI,
},
}),
client.fetch("get_machine", { client.fetch("get_machine", {
name: machineName, name: machineName,
flake: { flake: {
@@ -67,6 +67,11 @@ export const useMachineQuery = (clanURI: string, machineName: string) => {
}), }),
]); ]);
const tags = await tagsCall.result;
if (tags.status === "error") {
throw new Error("Error fetching tags: " + tags.errors[0].message);
}
const machine = await machineCall.result; const machine = await machineCall.result;
if (machine.status === "error") { if (machine.status === "error") {
throw new Error("Error fetching machine: " + machine.errors[0].message); throw new Error("Error fetching machine: " + machine.errors[0].message);
@@ -81,6 +86,7 @@ export const useMachineQuery = (clanURI: string, machineName: string) => {
} }
return { return {
tags: tags.data,
machine: machine.data, machine: machine.data,
fieldsSchema: writeSchema.data, fieldsSchema: writeSchema.data,
}; };

View File

@@ -30,6 +30,26 @@ export const SectionTags = (props: SectionTags) => {
return pick(machineQuery.data.machine, ["tags"]) satisfies FormValues; return pick(machineQuery.data.machine, ["tags"]) satisfies FormValues;
}; };
const options = () => {
if (!machineQuery.isSuccess) {
return [[], []];
}
// these are static values or values which have been configured in nix and
// cannot be modified in the UI
const readonlyOptions =
machineQuery.data.fieldsSchema.tags?.readonly_members || [];
// filter out the read-only options from the superset of clan-wide options
const readonlySet = new Set(readonlyOptions);
const defaultOptions = (machineQuery.data.tags.options || []).filter(
(tag) => !readonlySet.has(tag),
);
return [defaultOptions, readonlyOptions];
};
return ( return (
<Show when={machineQuery.isSuccess}> <Show when={machineQuery.isSuccess}>
<SidebarSectionForm <SidebarSectionForm
@@ -50,6 +70,8 @@ export const SectionTags = (props: SectionTags) => {
readOnly={!editing} readOnly={!editing}
orientation="horizontal" orientation="horizontal"
defaultValue={field.value} defaultValue={field.value}
defaultOptions={options()[0]}
readonlyOptions={options()[1]}
input={input} input={input}
/> />
)} )}