From 9f454b263618cf95101e0012efc2f40f9380273e Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 28 May 2024 07:51:13 -0400 Subject: [PATCH] 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],