Skip to content

Commit

Permalink
Support better K8s Resource Name editing
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewballantyne committed Sep 6, 2024
1 parent e344d82 commit 6364a1b
Show file tree
Hide file tree
Showing 22 changed files with 904 additions and 154 deletions.
26 changes: 26 additions & 0 deletions frontend/src/__mocks__/mockK8sNameDescriptionFieldData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as _ from 'lodash-es';
import { RecursivePartial } from '~/typeHelpers';
import { K8sNameDescriptionFieldData } from '~/concepts/k8s/K8sNameDescriptionField/types';

export const mockK8sNameDescriptionFieldData = (
overrides: RecursivePartial<K8sNameDescriptionFieldData> = {},
): K8sNameDescriptionFieldData =>
_.merge(
{},
{
name: '',
description: '',
k8sName: {
value: '',
state: {
immutable: false,
invalidLength: false,
invalidCharacters: false,
autoTrimmed: false,
maxLength: 253,
touched: false,
},
},
},
overrides,
);
4 changes: 2 additions & 2 deletions frontend/src/__mocks__/mockStartNotebookData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ImageStreamKind } from '~/k8sTypes';
import { StartNotebookData } from '~/pages/projects/types';
import { mockK8sNameDescriptionFieldData } from '~/__mocks__/mockK8sNameDescriptionFieldData';

type MockResourceConfigType = {
volumeName?: string;
Expand All @@ -8,8 +9,7 @@ export const mockStartNotebookData = ({
volumeName = 'test-volume',
}: MockResourceConfigType): StartNotebookData => ({
projectName: 'test-project',
notebookName: 'test-notebook',
description: '',
notebookData: mockK8sNameDescriptionFieldData({ name: 'test-notebook', description: '' }),
image: {
imageStream: {
metadata: {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/api/k8s/notebooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { K8sAPIOptions, KnownLabels, NotebookKind } from '~/k8sTypes';
import { usernameTranslate } from '~/utilities/notebookControllerUtils';
import { EnvironmentFromVariable, StartNotebookData } from '~/pages/projects/types';
import { ROOT_MOUNT_PATH } from '~/pages/projects/pvc/const';
import { translateDisplayNameForK8s } from '~/concepts/k8s/utils';
import { getTolerationPatch, TolerationChanges } from '~/utilities/tolerations';
import { applyK8sAPIOptions } from '~/api/apiMergeUtils';
import {
Expand All @@ -36,9 +35,7 @@ export const assembleNotebook = (
): NotebookKind => {
const {
projectName,
notebookName,
notebookId: overrideNotebookId,
description,
notebookData,
notebookSize,
envFrom,
initialAcceleratorProfile,
Expand All @@ -50,7 +47,11 @@ export const assembleNotebook = (
existingTolerations,
existingResources,
} = data;
const notebookId = overrideNotebookId || translateDisplayNameForK8s(notebookName, 'wb-');
const {
name: notebookName,
description,
k8sName: { value: notebookId },
} = notebookData;
const imageUrl = `${image.imageStream?.status?.dockerImageRepository}:${image.imageVersion?.name}`;
const imageSelection = `${image.imageStream?.metadata.name}:${image.imageVersion?.name}`;

Expand Down Expand Up @@ -277,7 +278,6 @@ export const updateNotebook = (
username: string,
opts?: K8sAPIOptions,
): Promise<NotebookKind> => {
assignableData.notebookId = existingNotebook.metadata.name;
const notebook = assembleNotebook(assignableData, username);

const oldNotebook = structuredClone(existingNotebook);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from 'react';
import { HelperTextItem } from '@patternfly/react-core';
import { K8sNameDescriptionFieldData } from '~/concepts/k8s/K8sNameDescriptionField/types';

type Variants = React.ComponentProps<typeof HelperTextItem>['variant'];

type HelperTextItemType = React.FC<{
k8sName: K8sNameDescriptionFieldData['k8sName'];
}>;

export const HelperTextItemMaxLength: HelperTextItemType = ({ k8sName }) => {
let variant: Variants = 'indeterminate';
if (k8sName.state.invalidLength) {
variant = 'error';
} else if (k8sName.state.autoTrimmed) {
variant = 'warning';
} else if (k8sName.value.trim().length > 0) {
variant = 'success';
}

return (
<HelperTextItem variant={variant} hasIcon>
Cannot exceed {k8sName.state.maxLength} characters
</HelperTextItem>
);
};

export const HelperTextItemValidCharacters: HelperTextItemType = ({ k8sName }) => {
let variant: Variants = 'indeterminate';
if (k8sName.state.invalidCharacters) {
variant = 'error';
} else if (k8sName.value.trim().length > 0) {
variant = 'success';
}

return (
<HelperTextItem variant={variant} hasIcon>
Must start and end with a letter or number. Valid characters include lowercase letters,
numbers, and hyphens (-).
</HelperTextItem>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as React from 'react';
import {
Button,
FormGroup,
FormHelperText,
HelperText,
HelperTextItem,
Stack,
StackItem,
TextArea,
TextInput,
} from '@patternfly/react-core';
import {
K8sNameDescriptionFieldData,
K8sNameDescriptionFieldUpdateFunction,
UseK8sNameDescriptionDataConfiguration,
UseK8sNameDescriptionFieldData,
} from '~/concepts/k8s/K8sNameDescriptionField/types';
import ResourceNameDefinitionTooltip from '~/concepts/k8s/ResourceNameDefinitionTooltip';
import useAutoFocusRef from '~/utilities/useAutoFocusRef';
import { handleUpdateLogic, setupDefaults } from '~/concepts/k8s/K8sNameDescriptionField/utils';
import ResourceNameField from '~/concepts/k8s/K8sNameDescriptionField/ResourceNameField';

/** Companion data hook */
export const useK8sNameDescriptionFieldData = (
configuration: UseK8sNameDescriptionDataConfiguration = {},
): UseK8sNameDescriptionFieldData => {
const [data, setData] = React.useState<K8sNameDescriptionFieldData>(() =>
setupDefaults(configuration),
);

// Hold the data in a ref to avoid churn in the update method
const dataRef = React.useRef(data);
dataRef.current = data;
const onDataChange = React.useCallback<K8sNameDescriptionFieldUpdateFunction>(
(key, value) => {
setData(handleUpdateLogic(dataRef.current)(key, value));
},
[setData],
);

return { data, onDataChange };
};

type K8sNameDescriptionFieldProps = {
autoFocusName?: boolean;
dataTestId: string;
descriptionLabel?: string;
nameLabel?: string;
} & UseK8sNameDescriptionFieldData;

/**
* Use in place of any K8s Resource creation / edit.
* @see useK8sNameDescriptionFieldData
*/
const K8sNameDescriptionField: React.FC<K8sNameDescriptionFieldProps> = ({
autoFocusName,
data,
dataTestId,
descriptionLabel = 'Description',
onDataChange,
nameLabel = 'Name',
}) => {
const autoFocusNameRef = useAutoFocusRef(autoFocusName);
const [showK8sField, setShowK8sField] = React.useState(false);

const { name, description, k8sName } = data;

return (
<Stack hasGutter>
<StackItem>
<FormGroup label={nameLabel} isRequired>
<TextInput
data-testid={`${dataTestId}-name`}
id={`${dataTestId}-name`}
ref={autoFocusNameRef}
isRequired
value={name}
onChange={(event, value) => onDataChange('name', value)}
/>
</FormGroup>
{!showK8sField && !k8sName.state.immutable && (
<FormHelperText>
{k8sName.value && (
<HelperText>
<HelperTextItem>
The resource name will be <b>{k8sName.value}</b>.
</HelperTextItem>
</HelperText>
)}
<HelperText>
<HelperTextItem>
<Button
data-testid={`${dataTestId}-editResourceLink`}
variant="link"
isInline
onClick={() => setShowK8sField(true)}
>
Edit resource name
</Button>{' '}
<ResourceNameDefinitionTooltip />
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
</StackItem>
<ResourceNameField
allowEdit={showK8sField}
dataTestId={dataTestId}
k8sName={k8sName}
onDataChange={onDataChange}
/>
<StackItem>
<FormGroup label={descriptionLabel}>
<TextArea
data-testid={`${dataTestId}-description`}
id={`${dataTestId}-description`}
value={description}
onChange={(event, value) => onDataChange('description', value)}
resizeOrientation="vertical"
/>
</FormGroup>
</StackItem>
</Stack>
);
};

export default K8sNameDescriptionField;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import { FormGroup, HelperText, StackItem, TextInput } from '@patternfly/react-core';
import ResourceNameDefinitionTooltip from '~/concepts/k8s/ResourceNameDefinitionTooltip';
import {
HelperTextItemMaxLength,
HelperTextItemValidCharacters,
} from '~/concepts/k8s/K8sNameDescriptionField/HelperTextItemVariants';
import {
K8sNameDescriptionFieldData,
K8sNameDescriptionFieldUpdateFunction,
} from '~/concepts/k8s/K8sNameDescriptionField/types';

type ResourceNameFieldProps = {
allowEdit: boolean;
dataTestId: string;
k8sName: K8sNameDescriptionFieldData['k8sName'];
onDataChange: K8sNameDescriptionFieldUpdateFunction;
};

/** Sub-resource; not for public cunsumption */
const ResourceNameField: React.FC<ResourceNameFieldProps> = ({
allowEdit,
dataTestId,
k8sName,
onDataChange,
}) => {
if (k8sName.state.immutable) {
return (
<StackItem>
<FormGroup label="Resource name" labelIcon={<ResourceNameDefinitionTooltip />}>
{k8sName.value}
</FormGroup>
</StackItem>
);
}

if (!allowEdit) {
return null;
}

return (
<StackItem>
<FormGroup label="Resource name" labelIcon={<ResourceNameDefinitionTooltip />}>
<TextInput
data-testid={`${dataTestId}-resourceName`}
value={k8sName.value}
onChange={(event, value) => onDataChange('k8sName', value)}
/>
<HelperText>
<HelperTextItemMaxLength k8sName={k8sName} />
<HelperTextItemValidCharacters k8sName={k8sName} />
</HelperText>
</FormGroup>
</StackItem>
);
};

export default ResourceNameField;
Loading

0 comments on commit 6364a1b

Please sign in to comment.