Skip to content

Commit

Permalink
frontend: Add create resource UI
Browse files Browse the repository at this point in the history
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.

Fixes: #1820

Signed-off-by: Evangelos Skopelitis <[email protected]>
  • Loading branch information
skoeva committed Jul 18, 2024
1 parent c309b09 commit 8c2313e
Show file tree
Hide file tree
Showing 25 changed files with 354 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -486,39 +486,7 @@
</h1>
<div
class="MuiBox-root css-ldp2l3"
>
<label
class="MuiFormControlLabel-root MuiFormControlLabel-labelPlacementEnd css-j204z7-MuiFormControlLabel-root"
>
<span
class="MuiSwitch-root MuiSwitch-sizeMedium css-julti5-MuiSwitch-root"
>
<span
class="MuiButtonBase-root MuiSwitch-switchBase MuiSwitch-colorPrimary Mui-checked PrivateSwitchBase-root MuiSwitch-switchBase MuiSwitch-colorPrimary Mui-checked Mui-checked css-1nsozxe-MuiButtonBase-root-MuiSwitch-switchBase"
>
<input
checked=""
class="PrivateSwitchBase-input MuiSwitch-input css-1m9pwf3"
type="checkbox"
/>
<span
class="MuiSwitch-thumb css-jsexje-MuiSwitch-thumb"
/>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</span>
<span
class="MuiSwitch-track css-1yjjitx-MuiSwitch-track"
/>
</span>
<span
class="MuiTypography-root MuiTypography-body1 MuiFormControlLabel-label css-1ezega9-MuiTypography-root"
>
Only warnings (3)
</span>
</label>
</div>
/>
</div>
</div>
<div
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/components/common/CreateResourceButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Meta, StoryFn } from '@storybook/react';
import React from 'react';
import { Provider } from 'react-redux';
import ConfigMap from '../../lib/k8s/configMap';
import { Lease } from '../../lib/k8s/lease';
import { RuntimeClass } from '../../lib/k8s/runtime';
import Secret from '../../lib/k8s/secret';
import store from '../../redux/stores/store';
import { CreateResourceButton, CreateResourceButtonProps } from './CreateResourceButton';

export default {
title: 'CreateResourceButton',
component: CreateResourceButton,
decorators: [
Story => (
<Provider store={store}>
<Story />
</Provider>
),
],
} as Meta;

const Template: StoryFn<CreateResourceButtonProps> = args => <CreateResourceButton {...args} />;

export const ConfigMapStory = Template.bind({});
ConfigMapStory.args = {
resourceClass: ConfigMap,
};

export const LeaseStory = Template.bind({});
LeaseStory.args = {
resourceClass: Lease,
};

export const RuntimeClassStory = Template.bind({});
RuntimeClassStory.args = {
resourceClass: RuntimeClass,
};

export const SecretStory = Template.bind({});
SecretStory.args = {
resourceClass: Secret,
};
126 changes: 126 additions & 0 deletions frontend/src/components/common/CreateResourceButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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 { ClassWithBaseObject, KubeObject, KubeObjectInterface } from '../../lib/k8s/cluster';
import { clusterAction } from '../../redux/clusterActionSlice';
import { EventStatus, HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice';
import { ActionButton, AuthVisible, EditorDialog } from '../common';

export interface CreateResourceButtonProps {
resourceClass: ClassWithBaseObject<KubeObject>;
}

export function CreateResourceButton(props: CreateResourceButtonProps) {
const { resourceClass } = props;
const { t } = useTranslation(['glossary', 'translation']);
const [openDialog, setOpenDialog] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState('');
const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE);
const dispatch = useDispatch();

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);

const clusterName = getCluster() || '';

dispatch(
clusterAction(() => applyFunc(massagedNewItemDefs, 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,
})
);

dispatchCreateEvent({
status: EventStatus.CONFIRMED,
});
}
const baseObject = resourceClass.getBaseObject ? resourceClass.getBaseObject() : {};
const resourceName = resourceClass.kind;

return (
<AuthVisible item={resourceClass} authVerb="create">
<ActionButton
color="primary"
description={t('translation|Create {{ resourceName }}', { resourceName })}
icon={'mdi:plus-circle'}
onClick={() => {
setOpenDialog(true);
}}
/>

<EditorDialog
item={baseObject}
open={openDialog}
onClose={() => setOpenDialog(false)}
onSave={handleSave}
saveLabel={t('translation|Apply')}
errorMessage={errorMessage}
onEditorChanged={() => setErrorMessage('')}
title={t('translation|Create {{ resourceName }}', { resourceName })}
/>
</AuthVisible>
);
}
6 changes: 3 additions & 3 deletions frontend/src/components/common/Resource/EditorDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ export default function EditorDialog(props: EditorDialogProps) {
setCode(originalCodeRef.current);
}

// async function applyFunc(newItems)

function handleSave() {
// Verify the YAML even means anything before trying to use it.
const { obj, format, error } = getObjectsFromCode(code);
Expand Down Expand Up @@ -311,9 +313,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 });
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/common/Resource/ResourceListView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { PropsWithChildren } from 'react';
import { KubeObject } from '../../../lib/k8s/cluster';
import { ClassWithBaseObject, KubeObject } from '../../../lib/k8s/cluster';
import { CreateResourceButton } from '../CreateResourceButton';
import SectionBox from '../SectionBox';
import SectionFilterHeader, { SectionFilterHeaderProps } from '../SectionFilterHeader';
import ResourceTable, { ResourceTableProps } from './ResourceTable';
Expand All @@ -10,11 +11,9 @@ export interface ResourceListViewProps<ItemType>
headerProps?: Omit<SectionFilterHeaderProps, 'title'>;
}

type Class<T> = new (...args: any[]) => T;

export interface ResourceListViewWithResourceClassProps<ItemType>
extends Omit<ResourceListViewProps<ItemType>, 'data'> {
resourceClass: Class<ItemType>;
resourceClass: ClassWithBaseObject<ItemType>;
}

export default function ResourceListView<ItemType>(
Expand All @@ -23,6 +22,7 @@ export default function ResourceListView<ItemType>(
const { title, children, headerProps, ...tableProps } = props;
const withNamespaceFilter =
'resourceClass' in props && (props.resourceClass as KubeObject)?.isNamespaced;
const resourceClass = 'resourceClass' in props ? props.resourceClass : null;

return (
<SectionBox
Expand All @@ -32,6 +32,9 @@ export default function ResourceListView<ItemType>(
title={title}
noNamespaceFilter={!withNamespaceFilter}
{...headerProps}
titleSideActions={
resourceClass ? [<CreateResourceButton resourceClass={resourceClass} />] : undefined
}
/>
) : (
title
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<DocumentFragment />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<DocumentFragment />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<DocumentFragment />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<DocumentFragment />
1 change: 1 addition & 0 deletions frontend/src/components/common/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const checkExports = [
'Chart',
'ConfirmDialog',
'ConfirmButton',
'CreateResourceButton',
'Dialog',
'EmptyContent',
'ErrorPage',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,15 @@
>
<div
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
/>
>
<div
class="MuiBox-root css-70qvj9"
>
<div
class="MuiBox-root css-ldp2l3"
/>
</div>
</div>
</div>
<div
class="MuiBox-root css-1txv3mw"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,19 @@
</h1>
<div
class="MuiBox-root css-ldp2l3"
/>
>
<button
aria-label="Create "
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,19 @@
</h1>
<div
class="MuiBox-root css-ldp2l3"
/>
>
<button
aria-label="Create "
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,19 @@
</h1>
<div
class="MuiBox-root css-ldp2l3"
/>
>
<button
aria-label="Create "
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 8c2313e

Please sign in to comment.