Merge pull request 'ui/services: add more features to components' (#4988) from search into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4988
This commit is contained in:
@@ -66,11 +66,11 @@
|
|||||||
border: 1px solid #2b4647;
|
border: 1px solid #2b4647;
|
||||||
|
|
||||||
background:
|
background:
|
||||||
linear-gradient(0deg, rgba(0, 0, 0, 0.18) 0%, rgba(0, 0, 0, 0.18) 100%),
|
linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
var(--clr-bg-inv-3, rgba(43, 70, 71, 0.79)) 0%,
|
theme(colors.bg.inv.2) 0%,
|
||||||
var(--clr-bg-inv-4, rgba(32, 54, 55, 0.79)) 100%
|
theme(colors.bg.inv.3) 100%
|
||||||
);
|
);
|
||||||
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -80,8 +80,7 @@
|
|||||||
|
|
||||||
.searchContent {
|
.searchContent {
|
||||||
@apply px-3;
|
@apply px-3;
|
||||||
height: var(--container-height, 14.5rem);
|
height: calc(var(--container-height, 14.5rem) - 4rem);
|
||||||
padding-bottom: 4rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes contentHide {
|
@keyframes contentHide {
|
||||||
|
|||||||
@@ -117,6 +117,27 @@ export const Default: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: {
|
||||||
|
// Test with lots of modules
|
||||||
|
loading: true,
|
||||||
|
options: [],
|
||||||
|
renderItem: () => <span></span>,
|
||||||
|
},
|
||||||
|
render: (args: SearchProps<Module>) => {
|
||||||
|
return (
|
||||||
|
<div class="absolute bottom-1/3 w-3/4 px-3">
|
||||||
|
<Search<Module>
|
||||||
|
{...args}
|
||||||
|
onChange={(module) => {
|
||||||
|
// Go to the module configuration
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
type MachineOrTag =
|
type MachineOrTag =
|
||||||
| {
|
| {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import Icon from "../Icon/Icon";
|
|||||||
import { Button } from "../Button/Button";
|
import { Button } from "../Button/Button";
|
||||||
import styles from "./Search.module.css";
|
import styles from "./Search.module.css";
|
||||||
import { Combobox } from "@kobalte/core/combobox";
|
import { Combobox } from "@kobalte/core/combobox";
|
||||||
import { createMemo, createSignal, For, JSX } from "solid-js";
|
import { createMemo, createSignal, For, JSX, Match, Switch } from "solid-js";
|
||||||
import { createVirtualizer } from "@tanstack/solid-virtual";
|
import { createVirtualizer } from "@tanstack/solid-virtual";
|
||||||
import { CollectionNode } from "@kobalte/core/*";
|
import { CollectionNode } from "@kobalte/core/*";
|
||||||
|
import { Loader } from "../Loader/Loader";
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -15,6 +16,8 @@ export interface SearchProps<T> {
|
|||||||
onChange: (value: T | null) => void;
|
onChange: (value: T | null) => void;
|
||||||
options: T[];
|
options: T[];
|
||||||
renderItem: (item: T) => JSX.Element;
|
renderItem: (item: T) => JSX.Element;
|
||||||
|
loading?: boolean;
|
||||||
|
loadingComponent?: JSX.Element;
|
||||||
}
|
}
|
||||||
export function Search<T extends Option>(props: SearchProps<T>) {
|
export function Search<T extends Option>(props: SearchProps<T>) {
|
||||||
// Controlled input value, to allow resetting the input itself
|
// Controlled input value, to allow resetting the input itself
|
||||||
@@ -136,41 +139,55 @@ export function Search<T extends Option>(props: SearchProps<T>) {
|
|||||||
setComboboxItems(arr);
|
setComboboxItems(arr);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Switch>
|
||||||
style={{
|
<Match when={props.loading}>
|
||||||
height: `${virtualizer().getTotalSize()}px`,
|
{props.loadingComponent ?? (
|
||||||
width: "100%",
|
<div class="flex w-full justify-center py-2">
|
||||||
position: "relative",
|
<Loader />
|
||||||
}}
|
</div>
|
||||||
>
|
)}
|
||||||
<For each={virtualizer().getVirtualItems()}>
|
</Match>
|
||||||
{(virtualRow) => {
|
<Match when={!props.loading}>
|
||||||
const item: CollectionNode<T> | undefined =
|
<div
|
||||||
items().getItem(virtualRow.key as string);
|
style={{
|
||||||
|
height: `${virtualizer().getTotalSize()}px`,
|
||||||
|
width: "100%",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={virtualizer().getVirtualItems()}>
|
||||||
|
{(virtualRow) => {
|
||||||
|
const item: CollectionNode<T> | undefined =
|
||||||
|
items().getItem(virtualRow.key as string);
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
console.warn("Item not found for key:", virtualRow.key);
|
console.warn(
|
||||||
return null;
|
"Item not found for key:",
|
||||||
}
|
virtualRow.key,
|
||||||
return (
|
);
|
||||||
<Combobox.Item
|
return null;
|
||||||
item={item}
|
}
|
||||||
class={styles.searchItem}
|
return (
|
||||||
style={{
|
<Combobox.Item
|
||||||
position: "absolute",
|
item={item}
|
||||||
top: 0,
|
class={styles.searchItem}
|
||||||
left: 0,
|
style={{
|
||||||
width: "100%",
|
position: "absolute",
|
||||||
height: `${virtualRow.size}px`,
|
top: 0,
|
||||||
transform: `translateY(${virtualRow.start}px)`,
|
left: 0,
|
||||||
}}
|
width: "100%",
|
||||||
>
|
height: `${virtualRow.size}px`,
|
||||||
{props.renderItem(item.rawValue)}
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
</Combobox.Item>
|
}}
|
||||||
);
|
>
|
||||||
}}
|
{props.renderItem(item.rawValue)}
|
||||||
</For>
|
</Combobox.Item>
|
||||||
</div>
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Combobox.Listbox>
|
</Combobox.Listbox>
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #2e4a4b;
|
border: 1px solid #2e4a4b;
|
||||||
background:
|
background:
|
||||||
linear-gradient(0deg, rgba(0, 0, 0, 0.18) 0%, rgba(0, 0, 0, 0.18) 100%),
|
linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
var(--clr-bg-inv-3, rgba(46, 74, 75, 0.79)) 0%,
|
theme(colors.bg.inv.2) 0%,
|
||||||
var(--clr-bg-inv-4, rgba(32, 54, 55, 0.79)) 100%
|
theme(colors.bg.inv.3) 100%
|
||||||
);
|
);
|
||||||
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
|||||||
@@ -457,3 +457,28 @@ export const useMachineGenerators = (
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ServiceModulesQuery = ReturnType<typeof useServiceModules>;
|
||||||
|
export type ServiceModules = SuccessData<"list_service_modules">;
|
||||||
|
export const useServiceModules = (clanUri: string) => {
|
||||||
|
const client = useApiClient();
|
||||||
|
return useQuery(() => ({
|
||||||
|
queryKey: ["clans", encodeBase64(clanUri), "service_modules"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const call = client.fetch("list_service_modules", {
|
||||||
|
flake: {
|
||||||
|
identifier: clanUri,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await call.result;
|
||||||
|
|
||||||
|
if (result.status === "error") {
|
||||||
|
// todo should we create some specific error types?
|
||||||
|
console.error("Error fetching clan details:", result.errors);
|
||||||
|
throw new Error(result.errors[0].message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|||||||
140
pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx
Normal file
140
pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||||
|
import { ServiceWorkflow } from "./Service";
|
||||||
|
import {
|
||||||
|
createMemoryHistory,
|
||||||
|
MemoryRouter,
|
||||||
|
RouteDefinition,
|
||||||
|
} from "@solidjs/router";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||||
|
import { ApiClientProvider, Fetcher } from "@/src/hooks/ApiClient";
|
||||||
|
import {
|
||||||
|
ApiCall,
|
||||||
|
OperationNames,
|
||||||
|
OperationResponse,
|
||||||
|
SuccessQuery,
|
||||||
|
} from "@/src/hooks/api";
|
||||||
|
|
||||||
|
type ResultDataMap = {
|
||||||
|
[K in OperationNames]: SuccessQuery<K>["data"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFetcher: Fetcher = <K extends OperationNames>(
|
||||||
|
name: K,
|
||||||
|
_args: unknown,
|
||||||
|
): ApiCall<K> => {
|
||||||
|
// TODO: Make this configurable for every story
|
||||||
|
const resultData: Partial<ResultDataMap> = {
|
||||||
|
list_service_modules: [
|
||||||
|
{
|
||||||
|
module: { name: "Module A", input: "Input A" },
|
||||||
|
info: {
|
||||||
|
manifest: {
|
||||||
|
name: "Module A",
|
||||||
|
description: "This is module A",
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
peer: null,
|
||||||
|
server: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module: { name: "Module B", input: "Input B" },
|
||||||
|
info: {
|
||||||
|
manifest: {
|
||||||
|
name: "Module B",
|
||||||
|
description: "This is module B",
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module: { name: "Module C", input: "Input B" },
|
||||||
|
info: {
|
||||||
|
manifest: {
|
||||||
|
name: "Module B",
|
||||||
|
description: "This is module B",
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module: { name: "Module B", input: "Input A" },
|
||||||
|
info: {
|
||||||
|
manifest: {
|
||||||
|
name: "Module B",
|
||||||
|
description: "This is module B",
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
uuid: "mock",
|
||||||
|
cancel: () => Promise.resolve(),
|
||||||
|
result: new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
op_key: "1",
|
||||||
|
status: "success",
|
||||||
|
data: resultData[name],
|
||||||
|
} as OperationResponse<K>);
|
||||||
|
}, 1500);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: Meta<typeof ServiceWorkflow> = {
|
||||||
|
title: "workflows/service",
|
||||||
|
component: ServiceWorkflow,
|
||||||
|
decorators: [
|
||||||
|
(Story: StoryObj, context: StoryContext) => {
|
||||||
|
const Routes: RouteDefinition[] = [
|
||||||
|
{
|
||||||
|
path: "/clans/:clanURI",
|
||||||
|
component: () => (
|
||||||
|
<div>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const history = createMemoryHistory();
|
||||||
|
history.set({ value: "/clans/dGVzdA==", replace: true });
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ApiClientProvider client={{ fetch: mockFetcher }}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<MemoryRouter
|
||||||
|
root={(props) => {
|
||||||
|
console.debug("Rendering MemoryRouter root with props:", props);
|
||||||
|
return props.children;
|
||||||
|
}}
|
||||||
|
history={history}
|
||||||
|
>
|
||||||
|
{Routes}
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</ApiClientProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof ServiceWorkflow>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
124
pkgs/clan-app/ui/src/workflows/Service/Service.tsx
Normal file
124
pkgs/clan-app/ui/src/workflows/Service/Service.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
createStepper,
|
||||||
|
getStepStore,
|
||||||
|
StepperProvider,
|
||||||
|
useStepper,
|
||||||
|
} from "@/src/hooks/stepper";
|
||||||
|
import { BackButton, NextButton } from "../Steps";
|
||||||
|
import { useClanURI } from "@/src/hooks/clan";
|
||||||
|
import { ServiceModules, useServiceModules } from "@/src/hooks/queries";
|
||||||
|
import { createEffect, createSignal } from "solid-js";
|
||||||
|
import { Search } from "@/src/components/Search/Search";
|
||||||
|
import Icon from "@/src/components/Icon/Icon";
|
||||||
|
import { Combobox } from "@kobalte/core/combobox";
|
||||||
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
|
|
||||||
|
type ModuleItem = ServiceModules[number];
|
||||||
|
|
||||||
|
interface Module {
|
||||||
|
value: string;
|
||||||
|
input: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
raw: ModuleItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectService = () => {
|
||||||
|
const clanURI = useClanURI();
|
||||||
|
const stepper = useStepper<ServiceSteps>();
|
||||||
|
|
||||||
|
const serviceModulesQuery = useServiceModules(clanURI);
|
||||||
|
|
||||||
|
const [moduleOptions, setModuleOptions] = createSignal<Module[]>([]);
|
||||||
|
createEffect(() => {
|
||||||
|
if (serviceModulesQuery.data) {
|
||||||
|
setModuleOptions(
|
||||||
|
serviceModulesQuery.data.map((m) => ({
|
||||||
|
value: `${m.module.name}:${m.module.input}`,
|
||||||
|
label: m.module.name,
|
||||||
|
description: m.info.manifest.description,
|
||||||
|
input: m.module.input || "clan-core",
|
||||||
|
raw: m,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [store, set] = getStepStore<ServiceStoreType>(stepper);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Search<Module>
|
||||||
|
loading={serviceModulesQuery.isLoading}
|
||||||
|
onChange={(module) => {
|
||||||
|
if (!module) return;
|
||||||
|
|
||||||
|
console.log("Module selected");
|
||||||
|
set("module", {
|
||||||
|
name: module.raw.module.name,
|
||||||
|
input: module.raw.module.input,
|
||||||
|
});
|
||||||
|
stepper.next();
|
||||||
|
}}
|
||||||
|
options={moduleOptions()}
|
||||||
|
renderItem={(item) => {
|
||||||
|
return (
|
||||||
|
<div class="flex items-center justify-between gap-2 rounded-md px-2 py-1 pr-4">
|
||||||
|
<div class="flex size-8 items-center justify-center rounded-md bg-white">
|
||||||
|
<Icon icon="Code" />
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-col">
|
||||||
|
<Combobox.ItemLabel class="flex">
|
||||||
|
<Typography hierarchy="body" size="s" weight="medium" inverted>
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
</Combobox.ItemLabel>
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="xxs"
|
||||||
|
weight="normal"
|
||||||
|
color="quaternary"
|
||||||
|
inverted
|
||||||
|
class="flex justify-between"
|
||||||
|
>
|
||||||
|
<span>{item.description}</span>
|
||||||
|
<span>by {item.input}</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
id: "select:service",
|
||||||
|
content: SelectService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "select:members",
|
||||||
|
content: () => <div>Configure your service here.</div>,
|
||||||
|
},
|
||||||
|
{ id: "settings", content: () => <div>Adjust settings here.</div> },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ServiceSteps = typeof steps;
|
||||||
|
|
||||||
|
export interface ServiceStoreType {
|
||||||
|
module: {
|
||||||
|
name: string;
|
||||||
|
input: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServiceWorkflow = () => {
|
||||||
|
const stepper = createStepper({ steps }, { initialStep: "select:service" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StepperProvider stepper={stepper}>
|
||||||
|
<BackButton />
|
||||||
|
{stepper.currentStep().content()}
|
||||||
|
<NextButton onClick={() => stepper.next()} />
|
||||||
|
</StepperProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ from clan_lib.errors import ClanError
|
|||||||
from clan_lib.flake.flake import Flake
|
from clan_lib.flake.flake import Flake
|
||||||
from clan_lib.nix_models.clan import (
|
from clan_lib.nix_models.clan import (
|
||||||
InventoryInstance,
|
InventoryInstance,
|
||||||
|
InventoryInstanceModule,
|
||||||
InventoryInstanceModuleType,
|
InventoryInstanceModuleType,
|
||||||
InventoryInstanceRolesType,
|
InventoryInstanceRolesType,
|
||||||
)
|
)
|
||||||
@@ -151,27 +152,32 @@ class ModuleInfo(TypedDict):
|
|||||||
roles: dict[str, None]
|
roles: dict[str, None]
|
||||||
|
|
||||||
|
|
||||||
class ModuleList(TypedDict):
|
class Module(TypedDict):
|
||||||
modules: dict[str, dict[str, ModuleInfo]]
|
module: InventoryInstanceModule
|
||||||
|
info: ModuleInfo
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def list_service_modules(flake: Flake) -> ModuleList:
|
def list_service_modules(flake: Flake) -> list[Module]:
|
||||||
"""Show information about a module"""
|
"""Show information about a module"""
|
||||||
modules = flake.select("clanInternals.inventoryClass.modulesPerSource")
|
modules = flake.select("clanInternals.inventoryClass.modulesPerSource")
|
||||||
|
|
||||||
res: dict[str, dict[str, ModuleInfo]] = {}
|
res: list[Module] = []
|
||||||
for input_name, module_set in modules.items():
|
for input_name, module_set in modules.items():
|
||||||
res[input_name] = {}
|
|
||||||
|
|
||||||
for module_name, module_info in module_set.items():
|
for module_name, module_info in module_set.items():
|
||||||
# breakpoint()
|
res.append(
|
||||||
res[input_name][module_name] = ModuleInfo(
|
Module(
|
||||||
manifest=ModuleManifest.from_dict(module_info.get("manifest")),
|
module={"name": module_name, "input": input_name},
|
||||||
roles=module_info.get("roles", {}),
|
info=ModuleInfo(
|
||||||
|
manifest=ModuleManifest.from_dict(
|
||||||
|
module_info.get("manifest"),
|
||||||
|
),
|
||||||
|
roles=module_info.get("roles", {}),
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return ModuleList(modules=res)
|
return res
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
@@ -188,19 +194,21 @@ def get_service_module(
|
|||||||
input_name, module_name = check_service_module_ref(flake, module_ref)
|
input_name, module_name = check_service_module_ref(flake, module_ref)
|
||||||
|
|
||||||
avilable_modules = list_service_modules(flake)
|
avilable_modules = list_service_modules(flake)
|
||||||
module_set = avilable_modules.get("modules", {}).get(input_name)
|
module_set: list[Module] = [
|
||||||
|
m for m in avilable_modules if m["module"].get("input", None) == input_name
|
||||||
|
]
|
||||||
|
|
||||||
if module_set is None:
|
if not module_set:
|
||||||
msg = f"Module set for input '{input_name}' not found"
|
msg = f"Module set for input '{input_name}' not found"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
module = module_set.get(module_name)
|
module = next((m for m in module_set if m["module"]["name"] == module_name), None)
|
||||||
|
|
||||||
if module is None:
|
if module is None:
|
||||||
msg = f"Module '{module_name}' not found in input '{input_name}'"
|
msg = f"Module '{module_name}' not found in input '{input_name}'"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
return module
|
return module["info"]
|
||||||
|
|
||||||
|
|
||||||
def check_service_module_ref(
|
def check_service_module_ref(
|
||||||
@@ -219,18 +227,21 @@ def check_service_module_ref(
|
|||||||
msg = "Setting module_ref.input is currently required"
|
msg = "Setting module_ref.input is currently required"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
module_set = avilable_modules.get("modules", {}).get(input_ref)
|
module_set = [
|
||||||
|
m for m in avilable_modules if m["module"].get("input", None) == input_ref
|
||||||
|
]
|
||||||
|
|
||||||
if module_set is None:
|
if module_set is None:
|
||||||
|
inputs = {m["module"].get("input") for m in avilable_modules}
|
||||||
msg = f"module set for input '{input_ref}' not found"
|
msg = f"module set for input '{input_ref}' not found"
|
||||||
msg += f"\nAvilable input_refs: {avilable_modules.get('modules', {}).keys()}"
|
msg += f"\nAvilable input_refs: {inputs}"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
module_name = module_ref.get("name")
|
module_name = module_ref.get("name")
|
||||||
if not module_name:
|
if not module_name:
|
||||||
msg = "Module name is required in module_ref"
|
msg = "Module name is required in module_ref"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
module = module_set.get(module_name)
|
module = next((m for m in module_set if m["module"]["name"] == module_name), None)
|
||||||
if module is None:
|
if module is None:
|
||||||
msg = f"module with name '{module_name}' not found"
|
msg = f"module with name '{module_name}' not found"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|||||||
@@ -215,9 +215,9 @@ def test_clan_create_api(
|
|||||||
inventory = store.read()
|
inventory = store.read()
|
||||||
|
|
||||||
modules = list_service_modules(clan_dir_flake)
|
modules = list_service_modules(clan_dir_flake)
|
||||||
assert (
|
|
||||||
modules["modules"]["clan-core"]["admin"]["manifest"].name == "clan-core/admin"
|
admin_module = next(m for m in modules if m["module"]["name"] == "admin")
|
||||||
)
|
assert admin_module["info"]["manifest"].name == "clan-core/admin"
|
||||||
|
|
||||||
set_value_by_path(inventory, "instances", inventory_conf.instances)
|
set_value_by_path(inventory, "instances", inventory_conf.instances)
|
||||||
store.write(
|
store.write(
|
||||||
|
|||||||
Reference in New Issue
Block a user