From 22bd24cb1c17a245fcba4545b565fda4e5eeba81 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 28 May 2024 07:49:52 -0400 Subject: [PATCH 1/2] frontend: Accept given YAML/JSON in EditorDialog Signed-off-by: Evangelos Skopelitis --- .../common/Resource/CreateButton.tsx | 3 +- .../common/Resource/EditorDialog.tsx | 64 +++++++++++-------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/common/Resource/CreateButton.tsx b/frontend/src/components/common/Resource/CreateButton.tsx index 4bd579fdb7..bd7c466bdb 100644 --- a/frontend/src/components/common/Resource/CreateButton.tsx +++ b/frontend/src/components/common/Resource/CreateButton.tsx @@ -36,6 +36,7 @@ export default function CreateButton(props: CreateButtonProps) { const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE); const clusters = useClusterGroup(); const [targetCluster, setTargetCluster] = React.useState(clusters[0] || ''); + const itemRef = React.useRef({}); // When the clusters in the group change, we want to reset the target cluster // if it's not in the new list of clusters. @@ -148,7 +149,7 @@ export default function CreateButton(props: CreateButtonProps) { )} setOpenDialog(false)} onSave={handleSave} diff --git a/frontend/src/components/common/Resource/EditorDialog.tsx b/frontend/src/components/common/Resource/EditorDialog.tsx index 58bd07b2e6..983c30cd50 100644 --- a/frontend/src/components/common/Resource/EditorDialog.tsx +++ b/frontend/src/components/common/Resource/EditorDialog.tsx @@ -49,8 +49,8 @@ import SimpleEditor from './SimpleEditor'; type KubeObjectIsh = Partial; export interface EditorDialogProps extends DialogProps { - /** The object to edit, or null to make the dialog be in "loading mode". Pass it an empty object if no contents are to be shown when the dialog is first open. */ - item: KubeObjectIsh | null; + /** The object(s) to edit, or null to make the dialog be in "loading mode". Pass it an empty object if no contents are to be shown when the dialog is first open. */ + item: KubeObjectIsh | object | object[] | string | null; /** Called when the dialog is closed. */ onClose: () => void; /** Called when the user clicks the save button. */ @@ -88,11 +88,14 @@ export default function EditorDialog(props: EditorDialogProps) { const [lang, setLang] = React.useState(i18n.language); const themeName = getThemeName(); - const originalCodeRef = React.useRef({ code: '', format: item ? 'yaml' : '' }); + const initialCode = typeof item === 'string' ? item : yaml.dump(item || {}); + const originalCodeRef = React.useRef({ code: initialCode, format: item ? 'yaml' : '' }); const [code, setCode] = React.useState(originalCodeRef.current); const codeRef = React.useRef(code); const lastCodeCheckHandler = React.useRef(0); - const previousVersionRef = React.useRef(item?.metadata?.resourceVersion || ''); + const previousVersionRef = React.useRef( + isKubeObjectIsh(item) ? item?.metadata?.resourceVersion || '' : '' + ); const [error, setError] = React.useState(''); const [docSpecs, setDocSpecs] = React.useState< KubeObjectInterface | KubeObjectInterface[] | null @@ -109,36 +112,45 @@ export default function EditorDialog(props: EditorDialogProps) { setUseSimpleEditorState(data); } + function isKubeObjectIsh(item: any): item is KubeObjectIsh { + return item && typeof item === 'object' && !Array.isArray(item) && 'metadata' in item; + } + // Update the code when the item changes, but only if the code hasn't been touched. React.useEffect(() => { if (!item || Object.keys(item || {}).length === 0) { + const defaultCode = '# Enter your YAML or JSON here'; + originalCodeRef.current = { code: defaultCode, format: 'yaml' }; + setCode({ code: defaultCode, format: 'yaml' }); return; } - const originalCode = originalCodeRef.current.code; - const itemCode = - originalCodeRef.current.format === 'json' ? JSON.stringify(item) : yaml.dump(item); - if (itemCode !== originalCodeRef.current.code) { - originalCodeRef.current = { code: itemCode, format: originalCodeRef.current.format }; - } + // Determine the format (YAML or JSON) and serialize to string + const format = looksLikeJson(originalCodeRef.current.code) ? 'json' : 'yaml'; + const itemCode = format === 'json' ? JSON.stringify(item) : yaml.dump(item); - if (!item.metadata) { - return; + // Update the code if the item representation has changed + if (itemCode !== originalCodeRef.current.code) { + originalCodeRef.current = { code: itemCode, format }; + setCode({ code: itemCode, format }); } - const resourceVersionsDiffer = - (previousVersionRef.current || '') !== (item.metadata!.resourceVersion || ''); - // Only change if the code hasn't been touched. - // We use the codeRef in this effect instead of the code, because we need to access the current - // state of the code but we don't want to trigger a re-render when we set the code here. - if (resourceVersionsDiffer || codeRef.current.code === originalCode) { - // Prevent updating to the same code, which would lead to an infinite loop. - if (codeRef.current.code !== itemCode) { - setCode({ code: itemCode, format: originalCodeRef.current.format }); - } + // Additional handling for Kubernetes objects + if (isKubeObjectIsh(item) && item.metadata) { + const resourceVersionsDiffer = + (previousVersionRef.current || '') !== (item.metadata!.resourceVersion || ''); + // Only change if the code hasn't been touched. + // We use the codeRef in this effect instead of the code, because we need to access the current + // state of the code but we don't want to trigger a re-render when we set the code here. + if (resourceVersionsDiffer || codeRef.current.code === originalCodeRef.current.code) { + // Prevent updating to the same code, which would lead to an infinite loop. + if (codeRef.current.code !== itemCode) { + setCode({ code: itemCode, format: originalCodeRef.current.format }); + } - if (resourceVersionsDiffer && !!item.metadata!.resourceVersion) { - previousVersionRef.current = item.metadata!.resourceVersion; + if (resourceVersionsDiffer && !!item.metadata!.resourceVersion) { + previousVersionRef.current = item.metadata!.resourceVersion; + } } } }, [item]); @@ -309,7 +321,9 @@ export default function EditorDialog(props: EditorDialogProps) { const errorLabel = error || errorMessage; let dialogTitle = title; if (!dialogTitle && item) { - const itemName = item.metadata?.name || t('New Object'); + const itemName = isKubeObjectIsh(item) + ? item.metadata?.name || t('New Object') + : t('New Object'); dialogTitle = isReadOnly() ? t('translation|View: {{ itemName }}', { itemName }) : t('translation|Edit: {{ itemName }}', { itemName }); From 9f454b263618cf95101e0012efc2f40f9380273e Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 28 May 2024 07:51:13 -0400 Subject: [PATCH 2/2] frontend: Add create resource UI These changes introduce a new UI feature that allows users to create resources from the associated list view. Clicking the 'Create' button opens up the EditorDialog used in the generic 'Create / Apply' button, now accepting generic YAML/JSON text rather than explicitly expecting an item that looks like a Kubernetes resource. The dialog box also includes a generic template for each resource. The apply logic for this new feature (as well as the original 'Create / Apply' button) has been consolidated in EditorDialog, with a flag allowing external components to utilize their own dispatch functionality. Fixes: #1820 Signed-off-by: Evangelos Skopelitis --- .../common/CreateResourceButton.stories.tsx | 98 +++++++++++++++++++ .../common/CreateResourceButton.tsx | 42 ++++++++ .../common/Resource/CreateButton.tsx | 94 +----------------- .../common/Resource/EditorDialog.stories.tsx | 11 +++ .../common/Resource/EditorDialog.tsx | 87 ++++++++++++++-- .../common/Resource/ResourceListView.tsx | 10 +- .../common/Resource/ViewButton.stories.tsx | 11 +++ ...rceButton.ConfigMapStory.stories.storyshot | 1 + ...ceButton.InvalidResource.stories.storyshot | 13 +++ ...urceButton.ValidResource.stories.storyshot | 13 +++ frontend/src/components/common/index.test.ts | 1 + frontend/src/components/common/index.ts | 1 + .../List.Items.stories.storyshot | 14 ++- .../src/components/crd/CustomResourceList.tsx | 8 +- .../CustomResourceList.List.stories.storyshot | 14 ++- .../List.DaemonSets.stories.storyshot | 14 ++- .../EndpointList.Items.stories.storyshot | 14 ++- .../HPAList.Items.stories.storyshot | 14 ++- .../ClassList.Items.stories.storyshot | 14 ++- .../List.Items.stories.storyshot | 14 ++- .../List.Items.stories.storyshot | 14 ++- .../List.Nodes.stories.storyshot | 14 ++- .../pdbList.Items.stories.storyshot | 14 ++- .../priorityClassList.Items.stories.storyshot | 14 ++- .../List.ReplicaSets.stories.storyshot | 14 ++- .../List.Items.stories.storyshot | 14 ++- .../List.Items.stories.storyshot | 14 ++- .../ClaimList.Items.stories.storyshot | 14 ++- .../ClassList.Items.stories.storyshot | 14 ++- .../VolumeList.Items.stories.storyshot | 14 ++- .../VPAList.List.stories.storyshot | 14 ++- ...gWebhookConfigList.Items.stories.storyshot | 14 ++- ...gWebhookConfigList.Items.stories.storyshot | 14 ++- frontend/src/i18n/locales/de/translation.json | 15 ++- frontend/src/i18n/locales/en/translation.json | 15 ++- frontend/src/i18n/locales/es/translation.json | 15 ++- frontend/src/i18n/locales/fr/translation.json | 15 ++- frontend/src/i18n/locales/pt/translation.json | 15 ++- .../src/i18n/locales/zh-tw/translation.json | 15 ++- frontend/src/lib/k8s/KubeObject.ts | 12 +++ frontend/src/lib/k8s/configMap.ts | 6 ++ frontend/src/lib/k8s/cronJob.ts | 31 ++++++ frontend/src/lib/k8s/daemonSet.ts | 34 ++++++- frontend/src/lib/k8s/deployment.ts | 29 ++++++ frontend/src/lib/k8s/endpoints.ts | 23 +++++ frontend/src/lib/k8s/hpa.ts | 11 +++ frontend/src/lib/k8s/ingress.ts | 33 +++++++ frontend/src/lib/k8s/ingressClass.ts | 6 ++ frontend/src/lib/k8s/lease.ts | 11 +++ frontend/src/lib/k8s/limitRange.tsx | 28 ++++++ .../lib/k8s/mutatingWebhookConfiguration.ts | 26 +++++ frontend/src/lib/k8s/networkpolicy.tsx | 43 ++++++++ frontend/src/lib/k8s/persistentVolume.ts | 19 ++++ frontend/src/lib/k8s/persistentVolumeClaim.ts | 13 +++ frontend/src/lib/k8s/podDisruptionBudget.ts | 6 ++ frontend/src/lib/k8s/priorityClass.ts | 9 ++ frontend/src/lib/k8s/replicaSet.ts | 28 ++++++ frontend/src/lib/k8s/resourceQuota.ts | 6 ++ frontend/src/lib/k8s/runtime.ts | 6 ++ frontend/src/lib/k8s/secret.ts | 6 ++ frontend/src/lib/k8s/service.ts | 20 ++++ frontend/src/lib/k8s/serviceAccount.ts | 10 ++ frontend/src/lib/k8s/statefulSet.ts | 32 +++++- frontend/src/lib/k8s/storageClass.ts | 9 ++ .../lib/k8s/validatingWebhookConfiguration.ts | 26 +++++ frontend/src/lib/k8s/vpa.ts | 12 +++ .../plugin/__snapshots__/pluginLib.snapshot | 1 + 67 files changed, 1085 insertions(+), 171 deletions(-) create mode 100644 frontend/src/components/common/CreateResourceButton.stories.tsx create mode 100644 frontend/src/components/common/CreateResourceButton.tsx create mode 100644 frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMapStory.stories.storyshot create mode 100644 frontend/src/components/common/__snapshots__/CreateResourceButton.InvalidResource.stories.storyshot create mode 100644 frontend/src/components/common/__snapshots__/CreateResourceButton.ValidResource.stories.storyshot diff --git a/frontend/src/components/common/CreateResourceButton.stories.tsx b/frontend/src/components/common/CreateResourceButton.stories.tsx new file mode 100644 index 0000000000..77f3e5b808 --- /dev/null +++ b/frontend/src/components/common/CreateResourceButton.stories.tsx @@ -0,0 +1,98 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, waitFor } from '@storybook/test'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { KubeObjectClass } from '../../lib/k8s/cluster'; +import ConfigMap from '../../lib/k8s/configMap'; +import store from '../../redux/stores/store'; +import { TestContext } from '../../test'; +import { CreateResourceButton, CreateResourceButtonProps } from './CreateResourceButton'; + +export default { + title: 'CreateResourceButton', + component: CreateResourceButton, + parameters: { + storyshots: { + disable: true, + }, + }, + decorators: [ + Story => { + return ( + + + + + + ); + }, + ], +} as Meta; + +type Story = StoryObj; + +export const ValidResource: Story = { + args: { resourceClass: ConfigMap as unknown as KubeObjectClass }, + + play: async ({ args }) => { + await userEvent.click( + screen.getByRole('button', { + name: `Create ${args.resourceClass.getBaseObject().kind}`, + }) + ); + + await waitFor(() => expect(screen.getByRole('textbox')).toBeVisible()); + + await userEvent.click(screen.getByRole('textbox')); + + await userEvent.keyboard('{Control>}a{/Control} {Backspace}'); + await userEvent.keyboard(`apiVersion: v1{Enter}`); + await userEvent.keyboard(`kind: ConfigMap{Enter}`); + await userEvent.keyboard(`metadata:{Enter}`); + await userEvent.keyboard(` name: base-configmap`); + + const button = await screen.findByRole('button', { name: 'Apply' }); + expect(button).toBeVisible(); + }, +}; + +export const InvalidResource: Story = { + args: { resourceClass: ConfigMap as unknown as KubeObjectClass }, + + play: async ({ args }) => { + await userEvent.click( + screen.getByRole('button', { + name: `Create ${args.resourceClass.getBaseObject().kind}`, + }) + ); + + await waitFor(() => expect(screen.getByRole('textbox')).toBeVisible()); + + await userEvent.click(screen.getByRole('textbox')); + + await userEvent.keyboard('{Control>}a{/Control}'); + await userEvent.keyboard(`apiVersion: v1{Enter}`); + await userEvent.keyboard(`kind: ConfigMap{Enter}`); + await userEvent.keyboard(`metadata:{Enter}`); + await userEvent.keyboard(` name: base-configmap{Enter}`); + await userEvent.keyboard(`creationTimestamp: ''`); + + const button = await screen.findByRole('button', { name: 'Apply' }); + expect(button).toBeVisible(); + + await userEvent.click(button); + + await waitFor(() => + userEvent.click( + screen.getByRole('button', { + name: `Create ${args.resourceClass.getBaseObject().kind}`, + }) + ) + ); + + await waitFor(() => expect(screen.getByText(/Failed/)).toBeVisible(), { + timeout: 15000, + }); + }, +}; diff --git a/frontend/src/components/common/CreateResourceButton.tsx b/frontend/src/components/common/CreateResourceButton.tsx new file mode 100644 index 0000000000..ad0fc1effb --- /dev/null +++ b/frontend/src/components/common/CreateResourceButton.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { KubeObjectClass } from '../../lib/k8s/cluster'; +import { ActionButton, AuthVisible, EditorDialog } from '../common'; + +export interface CreateResourceButtonProps { + resourceClass: KubeObjectClass; + resourceName?: string; +} + +export function CreateResourceButton(props: CreateResourceButtonProps) { + const { resourceClass, resourceName } = props; + const { t } = useTranslation(['glossary', 'translation']); + const [openDialog, setOpenDialog] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + + const baseObject = resourceClass.getBaseObject(); + const name = resourceName ?? baseObject.kind; + + return ( + + { + setOpenDialog(true); + }} + /> + setOpenDialog(false)} + saveLabel={t('translation|Apply')} + errorMessage={errorMessage} + onEditorChanged={() => setErrorMessage('')} + title={t('translation|Create {{ name }}', { name })} + /> + + ); +} diff --git a/frontend/src/components/common/Resource/CreateButton.tsx b/frontend/src/components/common/Resource/CreateButton.tsx index bd7c466bdb..f97fc81d62 100644 --- a/frontend/src/components/common/Resource/CreateButton.tsx +++ b/frontend/src/components/common/Resource/CreateButton.tsx @@ -6,18 +6,7 @@ import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; -import { useLocation } from 'react-router-dom'; import { useClusterGroup } from '../../../lib/k8s'; -import { apply } from '../../../lib/k8s/apiProxy'; -import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; -import { clusterAction } from '../../../redux/clusterActionSlice'; -import { - EventStatus, - HeadlampEventType, - useEventCallback, -} from '../../../redux/headlampEventSlice'; -import { AppDispatch } from '../../../redux/stores/store'; import ActionButton from '../ActionButton'; import EditorDialog from './EditorDialog'; @@ -27,15 +16,14 @@ interface CreateButtonProps { export default function CreateButton(props: CreateButtonProps) { const { isNarrow } = props; - const dispatch: AppDispatch = useDispatch(); const [openDialog, setOpenDialog] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); - const location = useLocation(); const { t } = useTranslation(['translation']); - const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE); const clusters = useClusterGroup(); const [targetCluster, setTargetCluster] = React.useState(clusters[0] || ''); + + // We want to avoid resetting the dialog state on close. const itemRef = React.useRef({}); // When the clusters in the group change, we want to reset the target cluster @@ -48,82 +36,6 @@ export default function CreateButton(props: CreateButtonProps) { } }, [clusters]); - const applyFunc = async (newItems: KubeObjectInterface[], clusterName: string) => { - await Promise.allSettled(newItems.map(newItem => apply(newItem, clusterName))).then( - (values: any) => { - values.forEach((value: any, index: number) => { - if (value.status === 'rejected') { - let msg; - const kind = newItems[index].kind; - const name = newItems[index].metadata.name; - const apiVersion = newItems[index].apiVersion; - if (newItems.length === 1) { - msg = t('translation|Failed to create {{ kind }} {{ name }}.', { kind, name }); - } else { - msg = t('translation|Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.', { - kind, - name, - apiVersion, - }); - } - setErrorMessage(msg); - setOpenDialog(true); - throw msg; - } - }); - } - ); - }; - - function handleSave(newItemDefs: KubeObjectInterface[]) { - let massagedNewItemDefs = newItemDefs; - const cancelUrl = location.pathname; - - // check if all yaml objects are valid - for (let i = 0; i < massagedNewItemDefs.length; i++) { - if (massagedNewItemDefs[i].kind === 'List') { - // flatten this List kind with the items that it has which is a list of valid k8s resources - const deletedItem = massagedNewItemDefs.splice(i, 1); - massagedNewItemDefs = massagedNewItemDefs.concat(deletedItem[0].items!); - } - if (!massagedNewItemDefs[i].metadata?.name) { - setErrorMessage( - t(`translation|Invalid: One or more of resources doesn't have a name property`) - ); - return; - } - if (!massagedNewItemDefs[i].kind) { - setErrorMessage(t('translation|Invalid: Please set a kind to the resource')); - return; - } - } - // all resources name - const resourceNames = massagedNewItemDefs.map(newItemDef => newItemDef.metadata.name); - setOpenDialog(false); - - dispatch( - clusterAction(() => applyFunc(massagedNewItemDefs, targetCluster), { - startMessage: t('translation|Applying {{ newItemName }}…', { - newItemName: resourceNames.join(','), - }), - cancelledMessage: t('translation|Cancelled applying {{ newItemName }}.', { - newItemName: resourceNames.join(','), - }), - successMessage: t('translation|Applied {{ newItemName }}.', { - newItemName: resourceNames.join(','), - }), - errorMessage: t('translation|Failed to apply {{ newItemName }}.', { - newItemName: resourceNames.join(','), - }), - cancelUrl, - }) - ); - - dispatchCreateEvent({ - status: EventStatus.CONFIRMED, - }); - } - return ( {isNarrow ? ( @@ -152,7 +64,7 @@ export default function CreateButton(props: CreateButtonProps) { item={itemRef.current} open={openDialog} onClose={() => setOpenDialog(false)} - onSave={handleSave} + setOpen={setOpenDialog} saveLabel={t('translation|Apply')} errorMessage={errorMessage} onEditorChanged={() => setErrorMessage('')} diff --git a/frontend/src/components/common/Resource/EditorDialog.stories.tsx b/frontend/src/components/common/Resource/EditorDialog.stories.tsx index 0e974b6481..3ad4b5c66c 100644 --- a/frontend/src/components/common/Resource/EditorDialog.stories.tsx +++ b/frontend/src/components/common/Resource/EditorDialog.stories.tsx @@ -2,12 +2,23 @@ import FormControlLabel from '@mui/material/FormControlLabel'; import FormGroup from '@mui/material/FormGroup'; import Switch from '@mui/material/Switch'; import { Meta, StoryFn } from '@storybook/react'; +import { Provider } from 'react-redux'; +import store from '../../../redux/stores/store'; import { EditorDialog, EditorDialogProps } from '..'; export default { title: 'Resource/EditorDialog', component: EditorDialog, argTypes: {}, + decorators: [ + Story => { + return ( + + + + ); + }, + ], } as Meta; const Template: StoryFn = args => { diff --git a/frontend/src/components/common/Resource/EditorDialog.tsx b/frontend/src/components/common/Resource/EditorDialog.tsx index 983c30cd50..2c182d6070 100644 --- a/frontend/src/components/common/Resource/EditorDialog.tsx +++ b/frontend/src/components/common/Resource/EditorDialog.tsx @@ -18,9 +18,19 @@ import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { getCluster } from '../../../lib/cluster'; +import { apply } from '../../../lib/k8s/apiProxy'; import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import { getThemeName } from '../../../lib/themes'; import { useId } from '../../../lib/util'; +import { clusterAction } from '../../../redux/clusterActionSlice'; +import { + EventStatus, + HeadlampEventType, + useEventCallback, +} from '../../../redux/headlampEventSlice'; +import { AppDispatch } from '../../../redux/stores/store'; import ConfirmButton from '../ConfirmButton'; import { Dialog, DialogProps } from '../Dialog'; import Loader from '../Loader'; @@ -53,10 +63,12 @@ export interface EditorDialogProps extends DialogProps { item: KubeObjectIsh | object | object[] | string | null; /** Called when the dialog is closed. */ onClose: () => void; - /** Called when the user clicks the save button. */ - onSave: ((...args: any[]) => void) | null; + /** Called by a component for when the user clicks the save button. When set to "default", internal save logic is applied. */ + onSave?: ((...args: any[]) => void) | 'default' | null; /** Called when the editor's contents change. */ onEditorChanged?: ((newValue: string) => void) | null; + /** The function to open the dialog. */ + setOpen?: (open: boolean) => void; /** The label to use for the save button. */ saveLabel?: string; /** The error message to display. */ @@ -71,8 +83,9 @@ export default function EditorDialog(props: EditorDialogProps) { const { item, onClose, - onSave, + onSave = 'default', onEditorChanged, + setOpen, saveLabel, errorMessage, title, @@ -106,6 +119,8 @@ export default function EditorDialog(props: EditorDialogProps) { const localData = localStorage.getItem('useSimpleEditor'); return localData ? JSON.parse(localData) : false; }); + const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE); + const dispatch: AppDispatch = useDispatch(); function setUseSimpleEditor(data: boolean) { localStorage.setItem('useSimpleEditor', JSON.stringify(data)); @@ -269,6 +284,34 @@ export default function EditorDialog(props: EditorDialogProps) { setCode(originalCodeRef.current); } + const applyFunc = async (newItems: KubeObjectInterface[], clusterName: string) => { + await Promise.allSettled(newItems.map(newItem => apply(newItem, clusterName))).then( + (values: any) => { + values.forEach((value: any, index: number) => { + if (value.status === 'rejected') { + let msg; + const kind = newItems[index].kind; + const name = newItems[index].metadata.name; + const apiVersion = newItems[index].apiVersion; + if (newItems.length === 1) { + msg = t('translation|Failed to create {{ kind }} {{ name }}.', { kind, name }); + } else { + msg = t('translation|Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.', { + kind, + name, + apiVersion, + }); + } + setError(msg); + setOpen?.(true); + // throw msg; + throw new Error(msg); + } + }); + } + ); + }; + function handleSave() { // Verify the YAML even means anything before trying to use it. const { obj, format, error } = getObjectsFromCode(code); @@ -285,7 +328,39 @@ export default function EditorDialog(props: EditorDialogProps) { setError(t("Error parsing the code. Please verify it's valid YAML or JSON!")); return; } - onSave!(obj); + + const newItemDefs = obj!; + + if (typeof onSave === 'string' && onSave === 'default') { + const resourceNames = newItemDefs.map(newItemDef => newItemDef.metadata.name); + const clusterName = getCluster() || ''; + + dispatch( + clusterAction(() => applyFunc(newItemDefs, clusterName), { + startMessage: t('translation|Applying {{ newItemName }}…', { + newItemName: resourceNames.join(','), + }), + cancelledMessage: t('translation|Cancelled applying {{ newItemName }}.', { + newItemName: resourceNames.join(','), + }), + successMessage: t('translation|Applied {{ newItemName }}.', { + newItemName: resourceNames.join(','), + }), + errorMessage: t('translation|Failed to apply {{ newItemName }}.', { + newItemName: resourceNames.join(','), + }), + cancelUrl: location.pathname, + }) + ); + + dispatchCreateEvent({ + status: EventStatus.CONFIRMED, + }); + + onClose(); + } else if (typeof onSave === 'function') { + onSave!(obj); + } } function makeEditor() { @@ -321,9 +396,7 @@ export default function EditorDialog(props: EditorDialogProps) { const errorLabel = error || errorMessage; let dialogTitle = title; if (!dialogTitle && item) { - const itemName = isKubeObjectIsh(item) - ? item.metadata?.name || t('New Object') - : t('New Object'); + const itemName = (isKubeObjectIsh(item) && item.metadata?.name) || t('New Object'); dialogTitle = isReadOnly() ? t('translation|View: {{ itemName }}', { itemName }) : t('translation|Edit: {{ itemName }}', { itemName }); diff --git a/frontend/src/components/common/Resource/ResourceListView.tsx b/frontend/src/components/common/Resource/ResourceListView.tsx index 19e6334b23..1157e0c674 100644 --- a/frontend/src/components/common/Resource/ResourceListView.tsx +++ b/frontend/src/components/common/Resource/ResourceListView.tsx @@ -1,6 +1,6 @@ import React, { PropsWithChildren, ReactElement, ReactNode } from 'react'; -import { KubeObject } from '../../../lib/k8s/KubeObject'; -import { KubeObjectClass } from '../../../lib/k8s/KubeObject'; +import { KubeObject, KubeObjectClass } from '../../../lib/k8s/KubeObject'; +import { CreateResourceButton } from '../CreateResourceButton'; import SectionBox from '../SectionBox'; import SectionFilterHeader, { SectionFilterHeaderProps } from '../SectionFilterHeader'; import ResourceTable, { ResourceTableProps } from './ResourceTable'; @@ -30,6 +30,8 @@ export default function ResourceListView( ) { const { title, children, headerProps, ...tableProps } = props; const withNamespaceFilter = 'resourceClass' in props && props.resourceClass?.isNamespaced; + const resourceClass = (props as ResourceListViewWithResourceClassProps) + .resourceClass as KubeObjectClass; return ( ] : undefined) + } {...headerProps} /> ) : ( diff --git a/frontend/src/components/common/Resource/ViewButton.stories.tsx b/frontend/src/components/common/Resource/ViewButton.stories.tsx index 9f4867efcc..b61ab181be 100644 --- a/frontend/src/components/common/Resource/ViewButton.stories.tsx +++ b/frontend/src/components/common/Resource/ViewButton.stories.tsx @@ -1,7 +1,9 @@ import '../../../i18n/config'; import { Meta, StoryFn } from '@storybook/react'; import React from 'react'; +import { Provider } from 'react-redux'; import { KubeObject } from '../../../lib/k8s/KubeObject'; +import store from '../../../redux/stores/store'; import ViewButton from './ViewButton'; import { ViewButtonProps } from './ViewButton'; @@ -9,6 +11,15 @@ export default { title: 'Resource/ViewButton', component: ViewButton, argTypes: {}, + decorators: [ + Story => { + return ( + + + + ); + }, + ], } as Meta; const Template: StoryFn = args => ; diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMapStory.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMapStory.stories.storyshot new file mode 100644 index 0000000000..df46f87231 --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMapStory.stories.storyshot @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.InvalidResource.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.InvalidResource.stories.storyshot new file mode 100644 index 0000000000..895858ca2d --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.InvalidResource.stories.storyshot @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.ValidResource.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.ValidResource.stories.storyshot new file mode 100644 index 0000000000..895858ca2d --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.ValidResource.stories.storyshot @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/index.test.ts b/frontend/src/components/common/index.test.ts index 0af5c688a8..a1d343491c 100644 --- a/frontend/src/components/common/index.test.ts +++ b/frontend/src/components/common/index.test.ts @@ -19,6 +19,7 @@ const checkExports = [ 'Chart', 'ConfirmDialog', 'ConfirmButton', + 'CreateResourceButton', 'Dialog', 'EmptyContent', 'ErrorPage', diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts index 4e35bcc8c7..54e664535d 100644 --- a/frontend/src/components/common/index.ts +++ b/frontend/src/components/common/index.ts @@ -50,3 +50,4 @@ export { default as ConfirmButton } from './ConfirmButton'; export * from './NamespacesAutocomplete'; export * from './Table/Table'; export { default as Table } from './Table'; +export * from './CreateResourceButton'; diff --git a/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot b/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot index ebe816364f..9c475fc2f0 100644 --- a/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot +++ b/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot @@ -19,7 +19,19 @@
+ > + +
, + ]} actions={[ @@ -176,6 +181,7 @@ export function CustomResourceListTable(props: CustomResourceTableProps) { title={title} headerProps={{ noNamespaceFilter: !crd.isNamespaced, + titleSideActions: [], }} resourceClass={CRClass} columns={cols} diff --git a/frontend/src/components/crd/__snapshots__/CustomResourceList.List.stories.storyshot b/frontend/src/components/crd/__snapshots__/CustomResourceList.List.stories.storyshot index 77ffd55c80..e46f8c72eb 100644 --- a/frontend/src/components/crd/__snapshots__/CustomResourceList.List.stories.storyshot +++ b/frontend/src/components/crd/__snapshots__/CustomResourceList.List.stories.storyshot @@ -51,7 +51,19 @@
+ > + +
+ > + +
+ > + +
+ > + +
+ > + +
diff --git a/frontend/src/components/ingress/__snapshots__/List.Items.stories.storyshot b/frontend/src/components/ingress/__snapshots__/List.Items.stories.storyshot index b92471f195..7854e29d42 100644 --- a/frontend/src/components/ingress/__snapshots__/List.Items.stories.storyshot +++ b/frontend/src/components/ingress/__snapshots__/List.Items.stories.storyshot @@ -19,7 +19,19 @@
+ > + +
+ > + +
+ > + +
diff --git a/frontend/src/components/podDisruptionBudget/__snapshots__/pdbList.Items.stories.storyshot b/frontend/src/components/podDisruptionBudget/__snapshots__/pdbList.Items.stories.storyshot index 96a669fcf3..c8e50b1e4e 100644 --- a/frontend/src/components/podDisruptionBudget/__snapshots__/pdbList.Items.stories.storyshot +++ b/frontend/src/components/podDisruptionBudget/__snapshots__/pdbList.Items.stories.storyshot @@ -19,7 +19,19 @@
+ > + +
+ > + +
diff --git a/frontend/src/components/replicaset/__snapshots__/List.ReplicaSets.stories.storyshot b/frontend/src/components/replicaset/__snapshots__/List.ReplicaSets.stories.storyshot index 1e7638b3ad..3da09c1b16 100644 --- a/frontend/src/components/replicaset/__snapshots__/List.ReplicaSets.stories.storyshot +++ b/frontend/src/components/replicaset/__snapshots__/List.ReplicaSets.stories.storyshot @@ -22,7 +22,19 @@
+ > + +
+ > + +
diff --git a/frontend/src/components/secret/__snapshots__/List.Items.stories.storyshot b/frontend/src/components/secret/__snapshots__/List.Items.stories.storyshot index a321d36c1b..7f298e2da5 100644 --- a/frontend/src/components/secret/__snapshots__/List.Items.stories.storyshot +++ b/frontend/src/components/secret/__snapshots__/List.Items.stories.storyshot @@ -19,7 +19,19 @@
+ > + +
+ > + +
+ > + +
diff --git a/frontend/src/components/storage/__snapshots__/VolumeList.Items.stories.storyshot b/frontend/src/components/storage/__snapshots__/VolumeList.Items.stories.storyshot index 08c33911e3..521587f3ff 100644 --- a/frontend/src/components/storage/__snapshots__/VolumeList.Items.stories.storyshot +++ b/frontend/src/components/storage/__snapshots__/VolumeList.Items.stories.storyshot @@ -19,7 +19,19 @@
+ > + +
diff --git a/frontend/src/components/verticalPodAutoscaler/__snapshots__/VPAList.List.stories.storyshot b/frontend/src/components/verticalPodAutoscaler/__snapshots__/VPAList.List.stories.storyshot index 162a07e109..23d009bf5f 100644 --- a/frontend/src/components/verticalPodAutoscaler/__snapshots__/VPAList.List.stories.storyshot +++ b/frontend/src/components/verticalPodAutoscaler/__snapshots__/VPAList.List.stories.storyshot @@ -19,7 +19,19 @@
+ > + +
+ > + +
diff --git a/frontend/src/components/webhookconfiguration/__snapshots__/ValidatingWebhookConfigList.Items.stories.storyshot b/frontend/src/components/webhookconfiguration/__snapshots__/ValidatingWebhookConfigList.Items.stories.storyshot index 05a6c863b3..9783b2ba34 100644 --- a/frontend/src/components/webhookconfiguration/__snapshots__/ValidatingWebhookConfigList.Items.stories.storyshot +++ b/frontend/src/components/webhookconfiguration/__snapshots__/ValidatingWebhookConfigList.Items.stories.storyshot @@ -22,7 +22,19 @@
+ > + +
diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index 3705543014..0fbf20a578 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -146,6 +146,7 @@ "Lost connection to the cluster.": "", "No": "Nein", "Yes": "Ja", + "Create {{ name }}": "", "Toggle fullscreen": "Vollbild ein/aus", "Close": "Schließen", "Head back <1>home.": "Head back <1>home.", @@ -174,14 +175,6 @@ "Read more": "Mehr lesen", "Dismiss": "Schließen", "Install the metrics-server to get usage data.": "Installieren Sie den Metriken-Server, um Nutzungsdaten zu erhalten.", - "Failed to create {{ kind }} {{ name }}.": "Erstellung von {{ kind }} fehlgeschlagen {{ name }}.", - "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Fehler beim Erstellen von {{ kind }} {{ name }} in {{ apiVersion }}.", - "Invalid: One or more of resources doesn't have a name property": "Ungültig: Eine oder mehrere der Ressourcen haben keine Namenseigenschaft", - "Invalid: Please set a kind to the resource": "Ungültig: Bitte geben Sie einen Typ für die Ressource an", - "Applying {{ newItemName }}…": "Anwenden von {{ newItemName }}…", - "Cancelled applying {{ newItemName }}.": "Die Anwendung von {{ newItemName }} wurde abgebrochen.", - "Applied {{ newItemName }}.": "Angewandt {{ newItemName }}.", - "Failed to apply {{ newItemName }}.": "Die Anwendung von {{ newItemName }} ist fehlgeschlagen.", "Create / Apply": "Erstellen / Anwenden", "Create": "Erstellen", "Deleting item {{ itemName }}…": "Lösche Element {{ itemName }} …", @@ -200,8 +193,14 @@ "Edit": "Bearbeiten", "Invalid JSON": "Ungültiges JSON", "Invalid YAML": "Ungültige YAML", + "Failed to create {{ kind }} {{ name }}.": "Erstellung von {{ kind }} fehlgeschlagen {{ name }}.", + "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Fehler beim Erstellen von {{ kind }} {{ name }} in {{ apiVersion }}.", "Error parsing the code: {{error}}": "Fehler beim Parsen des Codes: {{error}}", "Error parsing the code. Please verify it's valid YAML or JSON!": "Fehler beim Parsen des Codes. Bitte überprüfen Sie, ob es sich um gültiges YAML oder JSON handelt!", + "Applying {{ newItemName }}…": "Anwenden von {{ newItemName }}…", + "Cancelled applying {{ newItemName }}.": "Die Anwendung von {{ newItemName }} wurde abgebrochen.", + "Applied {{ newItemName }}.": "Angewandt {{ newItemName }}.", + "Failed to apply {{ newItemName }}.": "Die Anwendung von {{ newItemName }} ist fehlgeschlagen.", "New Object": "Neues Objekt", "View: {{ itemName }}": "Ansicht: {{ itemName }}", "Edit: {{ itemName }}": "Bearbeiten: {{ itemName }}", diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index 6b60b149ef..8972bb0a69 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -146,6 +146,7 @@ "Lost connection to the cluster.": "Lost connection to the cluster.", "No": "No", "Yes": "Yes", + "Create {{ name }}": "Create {{ name }}", "Toggle fullscreen": "Toggle fullscreen", "Close": "Close", "Head back <1>home.": "Head back <1>home.", @@ -174,14 +175,6 @@ "Read more": "Read more", "Dismiss": "Dismiss", "Install the metrics-server to get usage data.": "Install the metrics-server to get usage data.", - "Failed to create {{ kind }} {{ name }}.": "Failed to create {{ kind }} {{ name }}.", - "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.", - "Invalid: One or more of resources doesn't have a name property": "Invalid: One or more of resources doesn't have a name property", - "Invalid: Please set a kind to the resource": "Invalid: Please set a kind to the resource", - "Applying {{ newItemName }}…": "Applying {{ newItemName }}…", - "Cancelled applying {{ newItemName }}.": "Cancelled applying {{ newItemName }}.", - "Applied {{ newItemName }}.": "Applied {{ newItemName }}.", - "Failed to apply {{ newItemName }}.": "Failed to apply {{ newItemName }}.", "Create / Apply": "Create / Apply", "Create": "Create", "Deleting item {{ itemName }}…": "Deleting item {{ itemName }}…", @@ -200,8 +193,14 @@ "Edit": "Edit", "Invalid JSON": "Invalid JSON", "Invalid YAML": "Invalid YAML", + "Failed to create {{ kind }} {{ name }}.": "Failed to create {{ kind }} {{ name }}.", + "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.", "Error parsing the code: {{error}}": "Error parsing the code: {{error}}", "Error parsing the code. Please verify it's valid YAML or JSON!": "Error parsing the code. Please verify it's valid YAML or JSON!", + "Applying {{ newItemName }}…": "Applying {{ newItemName }}…", + "Cancelled applying {{ newItemName }}.": "Cancelled applying {{ newItemName }}.", + "Applied {{ newItemName }}.": "Applied {{ newItemName }}.", + "Failed to apply {{ newItemName }}.": "Failed to apply {{ newItemName }}.", "New Object": "New Object", "View: {{ itemName }}": "View: {{ itemName }}", "Edit: {{ itemName }}": "Edit: {{ itemName }}", diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index 97e95dcc65..25a40ca6a8 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -146,6 +146,7 @@ "Lost connection to the cluster.": "", "No": "No", "Yes": "Sí", + "Create {{ name }}": "", "Toggle fullscreen": "Alternar pantalla completa", "Close": "Cerrar", "Head back <1>home.": "Head back <1>home.", @@ -175,14 +176,6 @@ "Read more": "Leer más", "Dismiss": "Descartar", "Install the metrics-server to get usage data.": "Instale el metrics-server para obtener datos de uso.", - "Failed to create {{ kind }} {{ name }}.": "Fallo al crear {{ kind }} {{ name }}.", - "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Fallo al crear {{ kind }} {{ name }} en {{ apiVersion }}.", - "Invalid: One or more of resources doesn't have a name property": "Inválido: Uno o más recursos no tiene la propriedad \"name\"", - "Invalid: Please set a kind to the resource": "Inválido: Por favor asigne el \"kind\" al recurso.", - "Applying {{ newItemName }}…": "Aplicando {{ newItemName }}…", - "Cancelled applying {{ newItemName }}.": "Se ha cancelado la aplicación de {{ newItemName }}.", - "Applied {{ newItemName }}.": "Se ha aplicado {{ newItemName }}.", - "Failed to apply {{ newItemName }}.": "Fallo al aplicar {{ newItemName }}.", "Create / Apply": "Crear / Aplicar", "Create": "Crear", "Deleting item {{ itemName }}…": "Eliminando item {{ itemName }}…", @@ -201,8 +194,14 @@ "Edit": "Editar", "Invalid JSON": "JSON Inválido", "Invalid YAML": "YAML Inválido", + "Failed to create {{ kind }} {{ name }}.": "Fallo al crear {{ kind }} {{ name }}.", + "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Fallo al crear {{ kind }} {{ name }} en {{ apiVersion }}.", "Error parsing the code: {{error}}": "Error al analizar el código: {{error}}", "Error parsing the code. Please verify it's valid YAML or JSON!": "Error al analizar el código: {{error}}. ¡Por favor verifique que es YAML o JSON válidos!", + "Applying {{ newItemName }}…": "Aplicando {{ newItemName }}…", + "Cancelled applying {{ newItemName }}.": "Se ha cancelado la aplicación de {{ newItemName }}.", + "Applied {{ newItemName }}.": "Se ha aplicado {{ newItemName }}.", + "Failed to apply {{ newItemName }}.": "Fallo al aplicar {{ newItemName }}.", "New Object": "Nuevo Objeto", "View: {{ itemName }}": "Ver: {{ itemName }}", "Edit: {{ itemName }}": "Editar: {{ itemName }}", diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index 45bb73792b..4184de2367 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -146,6 +146,7 @@ "Lost connection to the cluster.": "", "No": "Non", "Yes": "Oui", + "Create {{ name }}": "", "Toggle fullscreen": "Basculer en mode plein écran", "Close": "Fermer", "Head back <1>home.": "Head back <1>home.", @@ -175,14 +176,6 @@ "Read more": "Lire la suite", "Dismiss": "Rejeter", "Install the metrics-server to get usage data.": "Installez le serveur de métriques pour obtenir des données d'utilisation.", - "Failed to create {{ kind }} {{ name }}.": "Échec de la création de {{ kind }} {{ name }}.", - "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Échec de la création de {{ kind }} {{ name }} dans {{ apiVersion }}.", - "Invalid: One or more of resources doesn't have a name property": "Non valide : Une ou plusieurs ressources n'ont pas de propriété nom", - "Invalid: Please set a kind to the resource": "Non valide : Veuillez définir un type pour la ressource", - "Applying {{ newItemName }}…": "Application {{ newItemName }}…", - "Cancelled applying {{ newItemName }}.": "Annulation de l'application {{ newItemName }}.", - "Applied {{ newItemName }}.": "Appliqué {{ newItemName }}.", - "Failed to apply {{ newItemName }}.": "Échec de l'application de {{ newItemName }}.", "Create / Apply": "Créer / Appliquer", "Create": "Créer", "Deleting item {{ itemName }}…": "Suppression de l'élément {{ itemName }}…", @@ -201,8 +194,14 @@ "Edit": "Éditer", "Invalid JSON": "JSON non valide", "Invalid YAML": "YAML non valide", + "Failed to create {{ kind }} {{ name }}.": "Échec de la création de {{ kind }} {{ name }}.", + "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Échec de la création de {{ kind }} {{ name }} dans {{ apiVersion }}.", "Error parsing the code: {{error}}": "Erreur lors de l'analyse du code : {{error}}", "Error parsing the code. Please verify it's valid YAML or JSON!": "Erreur lors de l'analyse du code. Veuillez vérifier qu'il s'agit d'un YAML ou d'un JSON valide !", + "Applying {{ newItemName }}…": "Application {{ newItemName }}…", + "Cancelled applying {{ newItemName }}.": "Annulation de l'application {{ newItemName }}.", + "Applied {{ newItemName }}.": "Appliqué {{ newItemName }}.", + "Failed to apply {{ newItemName }}.": "Échec de l'application de {{ newItemName }}.", "New Object": "Nouvel objet", "View: {{ itemName }}": "Regarder : {{ itemName }}", "Edit: {{ itemName }}": "Modifier : {{ itemName }}", diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index 3f2a88813f..879cb1d98e 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -146,6 +146,7 @@ "Lost connection to the cluster.": "", "No": "Não", "Yes": "Sim", + "Create {{ name }}": "", "Toggle fullscreen": "Alternar ecrã inteiro", "Close": "Fechar", "Head back <1>home.": "Voltar ao <1>início.", @@ -175,14 +176,6 @@ "Read more": "Ler mais", "Dismiss": "Dispensar", "Install the metrics-server to get usage data.": "Instale o metrics-server para obter dados sobre o uso.", - "Failed to create {{ kind }} {{ name }}.": "Falha ao criar {{ kind }} {{ name }}.", - "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Falha ao criar {{ kind }} {{ name }} em {{ apiVersion }}.", - "Invalid: One or more of resources doesn't have a name property": "Inválido: Um ou mais recursos não tem a propriedade \"name\"", - "Invalid: Please set a kind to the resource": "Inválido: Por favor introduza o \"kind\" para este recurso", - "Applying {{ newItemName }}…": "A aplicar {{ newItemName }}…", - "Cancelled applying {{ newItemName }}.": "Cancelou-se a aplicação de {{ newItemNam }}.", - "Applied {{ newItemName }}.": "Aplicou-se {{ newItemName }}", - "Failed to apply {{ newItemName }}.": "Falha ao aplicar {{ newItemName }}.", "Create / Apply": "Criar / Aplicar", "Create": "Criar", "Deleting item {{ itemName }}…": "A eliminar o item {{ itemName }}…", @@ -201,8 +194,14 @@ "Edit": "Editar", "Invalid JSON": "JSON Inválido", "Invalid YAML": "YAML Inválido", + "Failed to create {{ kind }} {{ name }}.": "Falha ao criar {{ kind }} {{ name }}.", + "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "Falha ao criar {{ kind }} {{ name }} em {{ apiVersion }}.", "Error parsing the code: {{error}}": "Erro ao analisar o código: {{error}}", "Error parsing the code. Please verify it's valid YAML or JSON!": "Erro ao analisar o código. Por favor verifique que o YAML ou JSON são válidos!", + "Applying {{ newItemName }}…": "A aplicar {{ newItemName }}…", + "Cancelled applying {{ newItemName }}.": "Cancelou-se a aplicação de {{ newItemNam }}.", + "Applied {{ newItemName }}.": "Aplicou-se {{ newItemName }}", + "Failed to apply {{ newItemName }}.": "Falha ao aplicar {{ newItemName }}.", "New Object": "Novo Objecto", "View: {{ itemName }}": "Ver: {{ itemName }}", "Edit: {{ itemName }}": "Editar: {{ itemName }}", diff --git a/frontend/src/i18n/locales/zh-tw/translation.json b/frontend/src/i18n/locales/zh-tw/translation.json index d3f6241f5c..d071e986e6 100644 --- a/frontend/src/i18n/locales/zh-tw/translation.json +++ b/frontend/src/i18n/locales/zh-tw/translation.json @@ -146,6 +146,7 @@ "Lost connection to the cluster.": "與叢集的連接丟失。", "No": "否", "Yes": "是", + "Create {{ name }}": "", "Toggle fullscreen": "切換全屏", "Close": "關閉", "Head back <1>home.": "返回<1>首頁。", @@ -173,14 +174,6 @@ "Read more": "閱讀更多", "Dismiss": "忽略", "Install the metrics-server to get usage data.": "安裝 metrics-server 以獲取使用數據。", - "Failed to create {{ kind }} {{ name }}.": "新增 {{ kind }} {{ name }} 失敗。", - "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "在 {{ apiVersion }} 中新增 {{ kind }} {{ name }} 失敗。", - "Invalid: One or more of resources doesn't have a name property": "無效:一個或多個資源沒有名稱屬性", - "Invalid: Please set a kind to the resource": "無效:請為資源設置一種類型", - "Applying {{ newItemName }}…": "正在應用 {{ newItemName }}…", - "Cancelled applying {{ newItemName }}.": "取消應用 {{ newItemName }}。", - "Applied {{ newItemName }}.": "已應用 {{ newItemName }}。", - "Failed to apply {{ newItemName }}.": "應用 {{ newItemName }} 失敗。", "Create / Apply": "新增 / 應用", "Create": "新增", "Deleting item {{ itemName }}…": "正在刪除項目 {{ itemName }}…", @@ -199,8 +192,14 @@ "Edit": "編輯", "Invalid JSON": "無效的 JSON", "Invalid YAML": "無效的 YAML", + "Failed to create {{ kind }} {{ name }}.": "新增 {{ kind }} {{ name }} 失敗。", + "Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.": "在 {{ apiVersion }} 中新增 {{ kind }} {{ name }} 失敗。", "Error parsing the code: {{error}}": "解析代碼時出錯:{{error}}", "Error parsing the code. Please verify it's valid YAML or JSON!": "解析代碼時出錯。請確認它是有效的 YAML 或 JSON!", + "Applying {{ newItemName }}…": "正在應用 {{ newItemName }}…", + "Cancelled applying {{ newItemName }}.": "取消應用 {{ newItemName }}。", + "Applied {{ newItemName }}.": "已應用 {{ newItemName }}。", + "Failed to apply {{ newItemName }}.": "應用 {{ newItemName }} 失敗。", "New Object": "新對象", "View: {{ itemName }}": "查看:{{ itemName }}", "Edit: {{ itemName }}": "編輯:{{ itemName }}", diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts index 4d457e2e7d..383870b6cb 100644 --- a/frontend/src/lib/k8s/KubeObject.ts +++ b/frontend/src/lib/k8s/KubeObject.ts @@ -599,6 +599,18 @@ export class KubeObject { return 'Error'; } } + + static getBaseObject(): Omit & { + metadata: Partial; + } { + return { + apiVersion: Array.isArray(this.apiVersion) ? this.apiVersion[0] : this.apiVersion, + kind: this.kind, + metadata: { + name: '', + }, + }; + } } /** diff --git a/frontend/src/lib/k8s/configMap.ts b/frontend/src/lib/k8s/configMap.ts index 5609fd98cf..246b2cf0eb 100644 --- a/frontend/src/lib/k8s/configMap.ts +++ b/frontend/src/lib/k8s/configMap.ts @@ -14,6 +14,12 @@ class ConfigMap extends KubeObject { get data() { return this.jsonData.data; } + + static getBaseObject(): KubeConfigMap { + const baseObject = super.getBaseObject() as KubeConfigMap; + baseObject.data = {}; + return baseObject; + } } export default ConfigMap; diff --git a/frontend/src/lib/k8s/cronJob.ts b/frontend/src/lib/k8s/cronJob.ts index 36a108cd22..ceddbfe815 100644 --- a/frontend/src/lib/k8s/cronJob.ts +++ b/frontend/src/lib/k8s/cronJob.ts @@ -49,6 +49,37 @@ class CronJob extends KubeObject { return this.getValue('status'); } + static getBaseObject(): KubeCronJob { + const baseObject = super.getBaseObject() as KubeCronJob; + baseObject.metadata = { + ...baseObject.metadata, + namespace: '', + }; + baseObject.spec = { + suspend: false, + schedule: '', + successfulJobsHistoryLimit: 3, + failedJobsHistoryLimit: 1, + concurrencyPolicy: 'Allow', + jobTemplate: { + spec: { + template: { + spec: { + containers: [ + { + name: '', + image: '', + imagePullPolicy: 'Always', + }, + ], + }, + }, + }, + }, + }; + return baseObject; + } + getContainers(): KubeContainer[] { return this.spec.jobTemplate?.spec?.template?.spec?.containers || []; } diff --git a/frontend/src/lib/k8s/daemonSet.ts b/frontend/src/lib/k8s/daemonSet.ts index da4088da3c..5978f39f9c 100644 --- a/frontend/src/lib/k8s/daemonSet.ts +++ b/frontend/src/lib/k8s/daemonSet.ts @@ -13,7 +13,7 @@ export interface KubeDaemonSet extends KubeObjectInterface { }; selector: LabelSelector; template: { - metadata: KubeMetadata; + metadata?: KubeMetadata; spec: KubePodSpec; }; [otherProps: string]: any; @@ -37,6 +37,38 @@ class DaemonSet extends KubeObject { return this.jsonData.status; } + static getBaseObject(): KubeDaemonSet { + const baseObject = super.getBaseObject() as KubeDaemonSet; + baseObject.metadata = { + ...baseObject.metadata, + namespace: '', + }; + baseObject.spec = { + updateStrategy: { + type: 'RollingUpdate', + rollingUpdate: { + maxUnavailable: 1, + }, + }, + selector: { + matchLabels: { app: 'headlamp' }, + }, + template: { + spec: { + containers: [ + { + name: '', + image: '', + imagePullPolicy: 'Always', + }, + ], + nodeName: '', + }, + }, + }; + return baseObject; + } + getContainers(): KubeContainer[] { return this.spec?.template?.spec?.containers || []; } diff --git a/frontend/src/lib/k8s/deployment.ts b/frontend/src/lib/k8s/deployment.ts index 4632392f15..2871aabd61 100644 --- a/frontend/src/lib/k8s/deployment.ts +++ b/frontend/src/lib/k8s/deployment.ts @@ -43,6 +43,35 @@ class Deployment extends KubeObject { const labels = this.spec.selector.matchLabels || {}; return Object.keys(labels).map(key => `${key}=${labels[key]}`); } + + static getBaseObject(): KubeDeployment { + const baseObject = super.getBaseObject() as KubeDeployment; + baseObject.metadata = { + ...baseObject.metadata, + namespace: '', + labels: { app: 'headlamp' }, + }; + baseObject.spec = { + selector: { + matchLabels: { app: 'headlamp' }, + }, + template: { + spec: { + containers: [ + { + name: '', + image: '', + ports: [{ containerPort: 80 }], + imagePullPolicy: 'Always', + }, + ], + nodeName: '', + }, + }, + }; + + return baseObject; + } } export default Deployment; diff --git a/frontend/src/lib/k8s/endpoints.ts b/frontend/src/lib/k8s/endpoints.ts index 90237b605d..40d47823da 100644 --- a/frontend/src/lib/k8s/endpoints.ts +++ b/frontend/src/lib/k8s/endpoints.ts @@ -34,6 +34,29 @@ class Endpoints extends KubeObject { static apiVersion = 'v1'; static isNamespaced = true; + static getBaseObject(): KubeEndpoint { + const baseObject = super.getBaseObject() as KubeEndpoint; + baseObject.subsets = [ + { + addresses: [ + { + hostname: '', + ip: '', + }, + ], + ports: [ + { + name: '', + appProtocol: 'http', + port: 80, + protocol: 'TCP', + }, + ], + }, + ]; + return baseObject; + } + // @todo Remove this when we can break backward compatibility. static get detailsRoute() { return 'Endpoint'; diff --git a/frontend/src/lib/k8s/hpa.ts b/frontend/src/lib/k8s/hpa.ts index 95cc5518b7..42d881bd42 100644 --- a/frontend/src/lib/k8s/hpa.ts +++ b/frontend/src/lib/k8s/hpa.ts @@ -172,6 +172,17 @@ class HPA extends KubeObject { static apiVersion = 'autoscaling/v2'; static isNamespaced = true; + static getBaseObject(): KubeHPA { + const baseObject = super.getBaseObject() as KubeHPA; + baseObject.spec = { + maxReplicas: 0, + minReplicas: 0, + scaleTargetRef: { apiVersion: '', kind: '', name: '' }, + metrics: [], + }; + return baseObject; + } + get spec(): HpaSpec { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/ingress.ts b/frontend/src/lib/k8s/ingress.ts index ca4c913982..25a2f2e703 100644 --- a/frontend/src/lib/k8s/ingress.ts +++ b/frontend/src/lib/k8s/ingress.ts @@ -73,6 +73,39 @@ class Ingress extends KubeObject { static apiVersion = ['networking.k8s.io/v1', 'extensions/v1beta1']; static isNamespaced = true; + static getBaseObject(): KubeIngress { + const baseObject = super.getBaseObject() as KubeIngress; + baseObject.spec = { + rules: [ + { + host: '', + http: { + paths: [ + { + path: '', + backend: { + service: { + name: '', + port: { + number: 80, + }, + }, + }, + }, + ], + }, + }, + ], + tls: [ + { + hosts: [], + secretName: '', + }, + ], + }; + return baseObject; + } + // Normalized, cached rules. private cachedRules: IngressRule[] = []; diff --git a/frontend/src/lib/k8s/ingressClass.ts b/frontend/src/lib/k8s/ingressClass.ts index bb16021d2a..703ff6b8b2 100644 --- a/frontend/src/lib/k8s/ingressClass.ts +++ b/frontend/src/lib/k8s/ingressClass.ts @@ -13,6 +13,12 @@ class IngressClass extends KubeObject { static apiVersion = 'networking.k8s.io/v1'; static isNamespaced = false; + static getBaseObject(): KubeIngressClass { + const baseObject = super.getBaseObject() as KubeIngressClass; + baseObject.spec = { controller: '' }; + return baseObject; + } + get spec(): KubeIngressClass['spec'] { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/lease.ts b/frontend/src/lib/k8s/lease.ts index 954f290f6f..de826afe33 100644 --- a/frontend/src/lib/k8s/lease.ts +++ b/frontend/src/lib/k8s/lease.ts @@ -17,6 +17,17 @@ export class Lease extends KubeObject { static apiVersion = 'coordination.k8s.io/v1'; static isNamespaced = true; + static getBaseObject(): KubeLease { + const baseObject = super.getBaseObject() as KubeLease; + baseObject.spec = { + holderIdentity: '', + leaseDurationSeconds: 0, + leaseTransitions: 0, + renewTime: '', + }; + return baseObject; + } + get spec() { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/limitRange.tsx b/frontend/src/lib/k8s/limitRange.tsx index 1380374c19..67a89fd3f8 100644 --- a/frontend/src/lib/k8s/limitRange.tsx +++ b/frontend/src/lib/k8s/limitRange.tsx @@ -32,6 +32,34 @@ export class LimitRange extends KubeObject { static apiVersion = 'v1'; static isNamespaced = true; + static getBaseObject(): KubeLimitRange { + const baseObject = super.getBaseObject() as KubeLimitRange; + baseObject.spec = { + limits: [ + { + default: { + cpu: '', + memory: '', + }, + defaultRequest: { + cpu: '', + memory: '', + }, + max: { + cpu: '', + memory: '', + }, + min: { + cpu: '', + memory: '', + }, + type: '', + }, + ], + }; + return baseObject; + } + get spec() { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts b/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts index d939d7782b..8afe757c36 100644 --- a/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts +++ b/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts @@ -48,6 +48,32 @@ class MutatingWebhookConfiguration extends KubeObject { static apiVersion = 'networking.k8s.io/v1'; static isNamespaced = true; + static getBaseObject(): KubeNetworkPolicy { + const baseObject = super.getBaseObject() as KubeNetworkPolicy; + baseObject.egress = [ + { + ports: [ + { + port: 80, + protocol: 'TCP', + }, + ], + to: [ + { + podSelector: { + matchLabels: { app: 'headlamp' }, + }, + }, + ], + }, + ]; + baseObject.ingress = [ + { + ports: [ + { + port: 80, + protocol: 'TCP', + }, + ], + from: [ + { + podSelector: { + matchLabels: { app: 'headlamp' }, + }, + }, + ], + }, + ]; + baseObject.podSelector = { + matchLabels: { app: 'headlamp' }, + }; + baseObject.policyTypes = ['Ingress', 'Egress']; + return baseObject; + } + static get pluralName() { return 'networkpolicies'; } diff --git a/frontend/src/lib/k8s/persistentVolume.ts b/frontend/src/lib/k8s/persistentVolume.ts index e442bde0f9..a2e94e1e94 100644 --- a/frontend/src/lib/k8s/persistentVolume.ts +++ b/frontend/src/lib/k8s/persistentVolume.ts @@ -20,6 +20,25 @@ class PersistentVolume extends KubeObject { static apiVersion = 'v1'; static isNamespaced = false; + static getBaseObject(): KubePersistentVolume { + const baseObject = super.getBaseObject() as KubePersistentVolume; + baseObject.metadata = { + ...baseObject.metadata, + namespace: '', + }; + baseObject.spec = { + capacity: { + storage: '', + }, + }; + baseObject.status = { + message: '', + phase: '', + reason: '', + }; + return baseObject; + } + get spec() { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/persistentVolumeClaim.ts b/frontend/src/lib/k8s/persistentVolumeClaim.ts index 9a366729db..240c6a11c4 100644 --- a/frontend/src/lib/k8s/persistentVolumeClaim.ts +++ b/frontend/src/lib/k8s/persistentVolumeClaim.ts @@ -31,6 +31,19 @@ class PersistentVolumeClaim extends KubeObject { static apiVersion = 'v1'; static isNamespaced = true; + static getBaseObject(): KubePersistentVolumeClaim { + const baseObject = super.getBaseObject() as KubePersistentVolumeClaim; + baseObject.metadata = { + ...baseObject.metadata, + namespace: '', + }; + baseObject.spec = { + storageClassName: '', + volumeName: '', + }; + return baseObject; + } + get spec() { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/podDisruptionBudget.ts b/frontend/src/lib/k8s/podDisruptionBudget.ts index e88b222899..32f87fd443 100644 --- a/frontend/src/lib/k8s/podDisruptionBudget.ts +++ b/frontend/src/lib/k8s/podDisruptionBudget.ts @@ -41,6 +41,12 @@ class PDB extends KubeObject { static apiVersion = 'policy/v1'; static isNamespaced = true; + static getBaseObject(): KubePDB { + const baseObject = super.getBaseObject() as KubePDB; + baseObject.spec = { selector: { matchLabels: {} } }; + return baseObject; + } + get spec(): KubePDB['spec'] { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/priorityClass.ts b/frontend/src/lib/k8s/priorityClass.ts index 03a417f775..61ed932187 100644 --- a/frontend/src/lib/k8s/priorityClass.ts +++ b/frontend/src/lib/k8s/priorityClass.ts @@ -13,6 +13,15 @@ class PriorityClass extends KubeObject { static apiVersion = 'scheduling.k8s.io/v1'; static isNamespaced = false; + static getBaseObject(): KubePriorityClass { + const baseObject = super.getBaseObject() as KubePriorityClass; + baseObject.value = 0; + baseObject.preemptionPolicy = ''; + baseObject.globalDefault = false; + baseObject.description = ''; + return baseObject; + } + get value(): number { return this.jsonData!.value; } diff --git a/frontend/src/lib/k8s/replicaSet.ts b/frontend/src/lib/k8s/replicaSet.ts index a20fc2b524..ad1c1de9fc 100644 --- a/frontend/src/lib/k8s/replicaSet.ts +++ b/frontend/src/lib/k8s/replicaSet.ts @@ -38,6 +38,34 @@ class ReplicaSet extends KubeObject { return this.jsonData.status; } + static getBaseObject(): KubeReplicaSet { + const baseObject = super.getBaseObject() as KubeReplicaSet; + baseObject.metadata = { + ...baseObject.metadata, + namespace: '', + }; + baseObject.spec = { + minReadySeconds: 0, + replicas: 1, + selector: { + matchLabels: { app: 'headlamp' }, + }, + template: { + spec: { + containers: [ + { + name: '', + image: '', + imagePullPolicy: 'Always', + }, + ], + nodeName: '', + }, + }, + }; + return baseObject; + } + getContainers(): KubeContainer[] { return this.spec?.template?.spec?.containers || []; } diff --git a/frontend/src/lib/k8s/resourceQuota.ts b/frontend/src/lib/k8s/resourceQuota.ts index 553f88fe88..064ccea5d5 100644 --- a/frontend/src/lib/k8s/resourceQuota.ts +++ b/frontend/src/lib/k8s/resourceQuota.ts @@ -35,6 +35,12 @@ class ResourceQuota extends KubeObject { static apiVersion = 'v1'; static isNamespaced = true; + static getBaseObject(): KubeResourceQuota { + const baseObject = super.getBaseObject() as KubeResourceQuota; + baseObject.spec = { hard: {} }; + return baseObject; + } + get spec(): spec { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/runtime.ts b/frontend/src/lib/k8s/runtime.ts index 9d97aeec25..9dc08bb688 100644 --- a/frontend/src/lib/k8s/runtime.ts +++ b/frontend/src/lib/k8s/runtime.ts @@ -12,6 +12,12 @@ export class RuntimeClass extends KubeObject { static apiVersion = 'node.k8s.io/v1'; static isNamespaced = false; + static getBaseObject(): KubeRuntimeClass { + const baseObject = super.getBaseObject() as KubeRuntimeClass; + baseObject.handler = ''; + return baseObject; + } + get spec() { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/secret.ts b/frontend/src/lib/k8s/secret.ts index 628b225599..db6e9b2ee4 100644 --- a/frontend/src/lib/k8s/secret.ts +++ b/frontend/src/lib/k8s/secret.ts @@ -11,6 +11,12 @@ class Secret extends KubeObject { static apiVersion = 'v1'; static isNamespaced = true; + static getBaseObject(): KubeSecret { + const baseObject = super.getBaseObject() as KubeSecret; + baseObject.data = {}; + return baseObject; + } + get data() { return this.jsonData.data; } diff --git a/frontend/src/lib/k8s/service.ts b/frontend/src/lib/k8s/service.ts index ac46d152db..32302d94e8 100644 --- a/frontend/src/lib/k8s/service.ts +++ b/frontend/src/lib/k8s/service.ts @@ -45,6 +45,26 @@ class Service extends KubeObject { static apiVersion = 'v1'; static isNamespaced = true; + static getBaseObject(): KubeService { + const baseObject = super.getBaseObject() as KubeService; + baseObject.spec = { + clusterIP: '', + ports: [ + { + name: '', + nodePort: 30000, + port: 80, + protocol: 'TCP', + targetPort: 80, + }, + ], + type: 'ClusterIP', + externalIPs: [], + selector: {}, + }; + return baseObject; + } + get spec(): KubeService['spec'] { return this.jsonData.spec; } diff --git a/frontend/src/lib/k8s/serviceAccount.ts b/frontend/src/lib/k8s/serviceAccount.ts index f0623fd6b7..a68a7dfd7a 100644 --- a/frontend/src/lib/k8s/serviceAccount.ts +++ b/frontend/src/lib/k8s/serviceAccount.ts @@ -17,6 +17,16 @@ class ServiceAccount extends KubeObject { static apiVersion = 'v1'; static isNamespaced = true; + static getBaseObject(): KubeServiceAccount { + const baseObject = super.getBaseObject() as KubeServiceAccount; + baseObject.metadata = { + ...baseObject.metadata, + namespace: '', + }; + baseObject.secrets = []; + return baseObject; + } + get secrets(): KubeServiceAccount['secrets'] { return this.jsonData.secrets; } diff --git a/frontend/src/lib/k8s/statefulSet.ts b/frontend/src/lib/k8s/statefulSet.ts index 5859c7215b..dfa649373f 100644 --- a/frontend/src/lib/k8s/statefulSet.ts +++ b/frontend/src/lib/k8s/statefulSet.ts @@ -13,7 +13,7 @@ export interface KubeStatefulSet extends KubeObjectInterface { type: string; }; template: { - metadata: KubeMetadata; + metadata?: KubeMetadata; spec: KubePodSpec; }; [other: string]: any; @@ -37,6 +37,36 @@ class StatefulSet extends KubeObject { return this.jsonData.status; } + static getBaseObject(): KubeStatefulSet { + const baseObject = super.getBaseObject() as KubeStatefulSet; + baseObject.metadata = { + ...baseObject.metadata, + namespace: '', + }; + baseObject.spec = { + selector: { + matchLabels: { app: 'headlamp' }, + }, + updateStrategy: { + type: 'RollingUpdate', + rollingUpdate: { partition: 0 }, + }, + template: { + spec: { + containers: [ + { + name: '', + image: '', + imagePullPolicy: 'Always', + }, + ], + nodeName: '', + }, + }, + }; + return baseObject; + } + getContainers(): KubeContainer[] { return this.spec?.template?.spec?.containers || []; } diff --git a/frontend/src/lib/k8s/storageClass.ts b/frontend/src/lib/k8s/storageClass.ts index a13cb6c7ba..72f6156695 100644 --- a/frontend/src/lib/k8s/storageClass.ts +++ b/frontend/src/lib/k8s/storageClass.ts @@ -13,6 +13,15 @@ class StorageClass extends KubeObject { static apiVersion = 'storage.k8s.io/v1'; static isNamespaced = false; + static getBaseObject(): KubeStorageClass { + const baseObject = super.getBaseObject() as KubeStorageClass; + baseObject.provisioner = ''; + baseObject.reclaimPolicy = ''; + baseObject.volumeBindingMode = ''; + baseObject.allowVolumeExpansion = false; + return baseObject; + } + get provisioner() { return this.jsonData.provisioner; } diff --git a/frontend/src/lib/k8s/validatingWebhookConfiguration.ts b/frontend/src/lib/k8s/validatingWebhookConfiguration.ts index fc97a1dbd3..c49c66534f 100644 --- a/frontend/src/lib/k8s/validatingWebhookConfiguration.ts +++ b/frontend/src/lib/k8s/validatingWebhookConfiguration.ts @@ -29,6 +29,32 @@ class ValidatingWebhookConfiguration extends KubeObject { static apiVersion = 'autoscaling.k8s.io/v1'; static isNamespaced = true; + static getBaseObject(): KubeVPA { + const baseObject = super.getBaseObject() as KubeVPA; + baseObject.spec = { + targetRef: { + apiVersion: '', + kind: '', + name: '', + }, + }; + return baseObject; + } + static async isEnabled(): Promise { let res; try { diff --git a/frontend/src/plugin/__snapshots__/pluginLib.snapshot b/frontend/src/plugin/__snapshots__/pluginLib.snapshot index 7bb73c5a37..7f8fa82a1e 100644 --- a/frontend/src/plugin/__snapshots__/pluginLib.snapshot +++ b/frontend/src/plugin/__snapshots__/pluginLib.snapshot @@ -35,6 +35,7 @@ "ConfirmButton": [Function], "ConfirmDialog": [Function], "CreateButton": [Function], + "CreateResourceButton": [Function], "DateLabel": [Function], "DeleteButton": [Function], "Dialog": [Function],