Skip to content

Commit

Permalink
feat: imlpement the UI for configuring extensions during cluster create
Browse files Browse the repository at this point in the history
Fixes: #207

The UI allows selecting installed extensions before adding machines to a
cluster during cluster creation or scaling.

Signed-off-by: Artem Chernyshev <[email protected]>
  • Loading branch information
Unix4ever committed May 8, 2024
1 parent f6cd840 commit 5b8c130
Show file tree
Hide file tree
Showing 16 changed files with 323 additions and 74 deletions.
2 changes: 2 additions & 0 deletions frontend/src/components/common/Icon/TIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ const icons = {
"link-down": defineAsyncComponent(() => import("../../icons/IconLinkDown.vue")),
"settings-toggle": defineAsyncComponent(() => import("../../icons/IconSettingsToggle.vue")),
"rollback": defineAsyncComponent(() => import("../../icons/IconRollback.vue")),
"extensions": defineAsyncComponent(() => import("../../icons/IconExtensions.vue")),
"extensions-toggle": defineAsyncComponent(() => import("../../icons/IconExtensionsToggle.vue")),
"document": DocumentIcon,
"power": PowerIcon,
"users": UsersIcon,
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/components/icons/IconExtensions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!--
Copyright (c) 2024 Sidero Labs, Inc.

Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<template>
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M10 3.5a1.5 1.5 0 013 0V4a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-.5a1.5 1.5 0 000 3h.5a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-.5a1.5 1.5 0 00-3 0v.5a1 1 0 01-1 1H6a1 1 0 01-1-1v-3a1 1 0 00-1-1h-.5a1.5 1.5 0 010-3H4a1 1 0 001-1V6a1 1 0 011-1h3a1 1 0 001-1v-.5z" />
</svg>
</template>

17 changes: 17 additions & 0 deletions frontend/src/components/icons/IconExtensionsToggle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!--
Copyright (c) 2024 Sidero Labs, Inc.

Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<template>
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<mask id="toggle2">
<rect id="cover" x="-5%" y="-5%" width="110%" height="110%" fill="white"/>
<circle cx="17" cy="3" r="4" fill="black"/>
</mask>
<path mask="url(#toggle2)" d="M10 3.5a1.5 1.5 0 013 0V4a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-.5a1.5 1.5 0 000 3h.5a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-.5a1.5 1.5 0 00-3 0v.5a1 1 0 01-1 1H6a1 1 0 01-1-1v-3a1 1 0 00-1-1h-.5a1.5 1.5 0 010-3H4a1 1 0 001-1V6a1 1 0 011-1h3a1 1 0 001-1v-.5z" />
<circle cx="17" cy="3" r="2.5" fill="#FF5F2A"/>
</svg>
</template>

2 changes: 1 addition & 1 deletion frontend/src/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ export const showModal = (component: Component, props: any) => {

export const closeModal = () => {
modal.value = null;
};
};
46 changes: 44 additions & 2 deletions frontend/src/states/cluster-management/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// MachineSet is the state used in cluster creation flow.

import { Resource, ResourceService } from "@/api/grpc"
import { ClusterSpec, ConfigPatchSpec, MachineSetSpecBootstrapSpec, MachineSetSpecUpdateStrategy, EtcdBackupConf } from "@/api/omni/specs/omni.pb"
import { ClusterSpec, ConfigPatchSpec, MachineSetSpecBootstrapSpec, MachineSetSpecUpdateStrategy, EtcdBackupConf, ExtensionsConfigurationSpec } from "@/api/omni/specs/omni.pb"

import { MachineSetNodeSpec, MachineSetSpec, MachineSetSpecMachineClassAllocationType } from "@/api/omni/specs/omni.pb";
import {
Expand All @@ -15,6 +15,7 @@ import {
DefaultKubernetesVersion,
DefaultNamespace,
DefaultTalosVersion,
ExtensionsConfigurationType,
LabelCluster,
LabelClusterMachine,
LabelControlPlaneRole,
Expand Down Expand Up @@ -89,6 +90,7 @@ export interface MachineSet {

export interface MachineSetNode {
patches: Record<string, ConfigPatch>
systemExtensions?: string[]
}

export type Cluster = {
Expand Down Expand Up @@ -380,6 +382,25 @@ export class State {
spec: {}
}

const systemExtensions = machineSet.machines[id].systemExtensions;

if (systemExtensions) {
resources.push({
metadata: {
id: `schematic-${id}`,
namespace: DefaultNamespace,
type: ExtensionsConfigurationType,
labels: {
[LabelCluster]: this.cluster.name,
[LabelClusterMachine]: id,
}
},
spec: {
extensions: systemExtensions
}
})
}

if (!machineSet.machineClass)
resources.push(msn);

Expand Down Expand Up @@ -531,6 +552,19 @@ export const populateExisting = async (clusterName: string) => {
patchesByID[patch.metadata.id!] = patch;
}

const systemExtensions: Resource<ExtensionsConfigurationSpec>[] = await ResourceService.List({
type: ExtensionsConfigurationType,
namespace: DefaultNamespace,
}, withRuntime(Runtime.Omni), withSelectors([`${LabelCluster}=${clusterName}`]));

const systemExtensionsByID: Record<string, Resource<ExtensionsConfigurationSpec>> = {};

for (const configuration of systemExtensions) {
systemExtensionsByID[configuration.metadata.id!] = configuration;

resources.push(configuration);
}

// get cluster
const cluster: Resource<ClusterSpec> = await ResourceService.Get({
type: ClusterType,
Expand Down Expand Up @@ -653,7 +687,10 @@ export const populateExisting = async (clusterName: string) => {
continue;
}

const machineSetNode = {
const machineSetNode: {
patches: Record<string, ConfigPatch>,
systemExtensions?: string[]
} = {
patches: {},
};

Expand All @@ -677,6 +714,11 @@ export const populateExisting = async (clusterName: string) => {
resources.push(configPatch);
}

const systemExtensions = systemExtensionsByID[`schematic-${msn.metadata.id!}`];
if (systemExtensions) {
machineSetNode.systemExtensions = systemExtensions.spec.extensions ?? [];
}

if (!msn.metadata.owner) {
resources.push(msn);
}
Expand Down
30 changes: 27 additions & 3 deletions frontend/src/views/omni/Clusters/Management/ClusterMachineItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@ included in the LICENSE file.
<div>
<machine-set-picker :options="options" :machine-set-index="machineSetIndex" @update:machineSetIndex="value => machineSetIndex = value"/>
</div>
<div class="flex items-center">
<div class="flex items-center gap-1">
<icon-button
class="text-naturals-N14 my-auto"
@click="openExtensionConfig"
:id="machineSetIndex !== undefined ? `extensions-${options?.[machineSetIndex]?.id}` : undefined"
:disabled="machineSetIndex === undefined || options?.[machineSetIndex]?.disabled"
:icon="systemExtensions ? 'extensions-toggle' : 'extensions'"/>
<icon-button
class="text-naturals-N14 my-auto"
@click="openPatchConfig"
:id="machineSetIndex !== undefined ? options?.[machineSetIndex]?.id : undefined"
:disabled="machineSetIndex === undefined || options?.[machineSetIndex]?.disabled"
:icon="machineSetNode.patches[machinePatchID] && machineSetIndex ? 'settings-toggle': 'settings'"/>
:icon="machineSetNode.patches[machinePatchID] && machineSetIndex !== undefined ? 'settings-toggle': 'settings'"/>
</div>
</div>
</div>
Expand All @@ -55,7 +61,7 @@ included in the LICENSE file.
<div class="mb-2 mt-4">Network Interfaces</div>
<div>
<div v-for="(processor, index) in item?.spec?.hardware?.processors" :key="index">
{{ processor.frequency / 1000 }} GHz, {{ processor.core_count }} {{ pluralize("core", processor.core_count) }}, {{ processor.description }}
{{ (processor.frequency ?? 0) / 1000 }} GHz, {{ processor.core_count }} {{ pluralize("core", processor.core_count) }}, {{ processor.description }}
</div>
</div>
<div>
Expand Down Expand Up @@ -110,6 +116,7 @@ import { MachineSet, MachineSetNode, state, PatchID } from "@/states/cluster-man
import MachineSetPicker, { PickerOption } from "./MachineSetPicker.vue";

import yaml from "js-yaml";
import CreateExtensions from "../../Modals/CreateExtensions.vue";

type MemModule = {
size_mb?: number,
Expand Down Expand Up @@ -272,6 +279,23 @@ const setInstallDisk = (value: string) => {
};
};

const systemExtensions = ref<string[]>();

const openExtensionConfig = () => {
showModal(
CreateExtensions,
{
machine: item.value.metadata.id!,
modelValue: systemExtensions.value,
onSave(extensions?: string[]) {
machineSetNode.value.systemExtensions = extensions;

systemExtensions.value = extensions;
}
},
)
};

const openPatchConfig = () => {
showModal(
ConfigPatchEdit,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/omni/Modals/ConfigPatchEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,4 @@ const openDocs = () => {
.heading {
@apply text-xl text-naturals-N14;
}
</style>
</style>
140 changes: 140 additions & 0 deletions frontend/src/views/omni/Modals/CreateExtensions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!--
Copyright (c) 2024 Sidero Labs, Inc.

Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<template>
<div class="modal-window flex flex-col gap-4" style="height: 90%;">
<div class="heading">
<h3 class="text-base text-naturals-N14">
Set Extensions
</h3>
<close-button @click="close" />
</div>

<div v-if="machineStatus" class="flex flex-col gap-4 flex-1 overflow-hidden">
<extensions-picker
v-model="requestedExtensions"
:talos-version="machineStatus?.spec.talos_version!.slice(1)" class="flex-1"
/>

<div class="flex justify-between gap-4">
<t-button @click="close" type="secondary">
Cancel
</t-button>
<div class="flex gap-4">
<t-button @click="() => updateExtensions()" icon="reset" :disabled="modelValue === undefined">
Revert
</t-button>
<t-button @click="() => updateExtensions(requestedExtensions)" type="highlighted">
Save
</t-button>
</div>
</div>

</div>
<div v-else class="flex items-center justify-center">
<t-spinner class="w-6 h-6"/>
</div>
</div>
</template>

<script setup lang="ts">
import CloseButton from "@/views/omni/Modals/CloseButton.vue";
import TButton from "@/components/common/Button/TButton.vue";
import ExtensionsPicker from "@/views/omni/Extensions/ExtensionsPicker.vue";
import { computed, ref, toRefs, watch } from "vue";
import Watch from "@/api/watch";
import { Resource } from "@/api/grpc";
import { MachineStatusSpec } from "@/api/omni/specs/omni.pb";
import { DefaultNamespace, MachineStatusType } from "@/api/resources";
import { Runtime } from "@/api/common/omni.pb";
import TSpinner from "@/components/common/Spinner/TSpinner.vue";
import { closeModal } from "@/modal";

const props = defineProps<{
machine: string
modelValue?: string[]
onSave: (e?: string[]) => void
}>();

const {
machine,
modelValue,
} = toRefs(props);

const close = () => {
closeModal();
};

const requestedExtensions = ref<Record<string, boolean>>({});

if (modelValue.value) {
for (const key of modelValue.value) {
requestedExtensions.value[key] = true;
}
}

const machineStatus = ref<Resource<MachineStatusSpec>>();
const machineStatusWatch = new Watch(machineStatus);

watch(machineStatus, () => {
if (modelValue.value !== undefined) {
return;
}

const extensions = machineStatus.value?.spec.schematic?.extensions;
if (!extensions) {
return;
}

requestedExtensions.value = {};

for (const extension of extensions) {
requestedExtensions.value[extension] = true;
}
});

machineStatusWatch.setup(computed(() => {
return {
resource: {
id: machine.value,
namespace: DefaultNamespace,
type: MachineStatusType,
},
runtime: Runtime.Omni
}
}));

const updateExtensions = (extensions?: Record<string, boolean>) => {
if (extensions === undefined) {
props.onSave();
} else {
const list: string[] = [];
for (const key in extensions) {
if (!extensions[key]) {
continue;
}

list.push(key);
}

list.sort();

props.onSave(list);
}

close();
}
</script>

<style scoped>
.modal-window {
@apply w-1/2 h-auto p-8;
}

.heading {
@apply flex justify-between items-center text-xl text-naturals-N14;
}
</style>
12 changes: 6 additions & 6 deletions frontend/src/views/omni/Modals/UpdateExtensions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ included in the LICENSE file.
<close-button @click="close" />
</div>

<div v-if="machineExtensionsStatus" class="flex flex-col gap-2 flex-1 overflow-hidden">
<div v-if="machineExtensionsStatus" class="flex flex-col gap-4 flex-1 overflow-hidden">
<extensions-picker
v-model="enabledExtensions"
:talos-version="machineExtensionsStatus?.spec.talos_version!.slice(1)" class="flex-1"
:indeterminate="indeterminate"
:immutable-extensions="immutableExtensions"
/>

<div class="flex justify-end gap-4">
<t-button @click="close" class="w-32 h-9">
<div class="flex justify-between gap-4">
<t-button @click="close" type="secondary">
Cancel
</t-button>
<t-button @click="updateExtensions" class="w-32 h-9" type="highlighted">
<t-button @click="updateExtensions" type="highlighted">
Update
</t-button>
</div>
Expand Down Expand Up @@ -146,7 +146,7 @@ const updateExtensionsConfig = async () => {

const extensionsConfiguration: Resource<ExtensionsConfigurationSpec> = {
metadata: {
id: machineExtensionsStatus.value.metadata.id!,
id: `schematic-${machineExtensionsStatus.value.metadata.id!}`,
namespace: DefaultNamespace,
type: ExtensionsConfigurationType,
labels: {
Expand All @@ -161,7 +161,7 @@ const updateExtensionsConfig = async () => {

try {
const existing: Resource<ExtensionsConfigurationSpec> = await ResourceService.Get({
id: extensionsConfiguration.metadata.id,
id: `schematic-${extensionsConfiguration.metadata.id}`,
namespace: extensionsConfiguration.metadata.namespace,
type: extensionsConfiguration.metadata.type,
}, withRuntime(Runtime.Omni));
Expand Down
Loading

0 comments on commit 5b8c130

Please sign in to comment.