Skip to content

Commit

Permalink
Merge pull request #63 from adamviktora/resource-name-field
Browse files Browse the repository at this point in the history
feat: K8sNameDescriptionField and related components
  • Loading branch information
nicolethoen authored Nov 19, 2024
2 parents 9dfc6c5 + 195bf8c commit fbf966f
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 1 deletion.
1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@
],
"no-unused-labels": "error",
"no-var": "error",
"object-shorthand": "error",
"one-var": ["error", "never"],
"prefer-const": "error",
"radix": ["error", "as-needed"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
# Sidenav top-level section
# should be the same for all markdown files
section: AI-infra-ui-components
# Sidenav secondary level section
# should be the same for all markdown files
id: FieldGroupHelpLabelIcon
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
source: react
# If you use typescript, the name of the interface to display props for
# These are found through the sourceProps function provided in patternfly-docs.source.js
propComponents: ['FieldGroupHelpLabelIcon']
---

import { FieldGroupHelpLabelIcon } from "@patternfly/ai-infra-ui-components";

Note: this component documents the API and enhances the [existing FieldGroupHelpLabelIcon](https://github.com/opendatahub-io/odh-dashboard/blob/main/frontend/src/components/FieldGroupHelpLabelIcon.tsx) component from odh-dashboard. It can be imported from [@patternfly/ai-infra-ui-components](https://www.npmjs.com/package/@patternfly/AI-infra-ui-components). Alternatively, it can be used within the odh-dashboard via the import: `~/components/FieldGroupHelpLabelIcon`

This AI-infra-ui-components version of `FieldGroupHelpLabelIcon` uses internally a new PatternFly's `FormGroupLabelHelp` component instead of odh-dasboard's `DashboardPopupIconButton`.
**We recommend** replacing occurences of `<DashboardPopupIconButton icon={<OutlinedQuestionCircleIcon />} aria-label="More info"/>` with `<FormGroupLabelHelp aria-label="More info" />`.

### Example

```js file="./FieldGroupHelpLabelIconBasic.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { FieldGroupHelpLabelIcon } from '@patternfly/ai-infra-ui-components';
import { Form, FormGroup, TextInput } from '@patternfly/react-core';

export const FieldGroupHelpLabelIconBasic: React.FunctionComponent = () => {
const [name, setName] = React.useState('');

const handleNameChange = (_event, name: string) => {
setName(name);
};

return (
<Form>
<FormGroup
label="Full name"
labelHelp={<FieldGroupHelpLabelIcon content="The name of a person" />}
isRequired
fieldId="simple-form-name"
>
<TextInput type="text" id="simple-form-name" name="simple-form-name" value={name} onChange={handleNameChange} />
</FormGroup>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
# Sidenav top-level section
# should be the same for all markdown files
section: AI-infra-ui-components
# Sidenav secondary level section
# should be the same for all markdown files
id: K8sNameDescriptionField
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
source: react
# If you use typescript, the name of the interface to display props for
# These are found through the sourceProps function provided in patternfly-docs.source.js
propComponents: ['K8sNameDescriptionField']
---

import { K8sNameDescriptionField } from "@patternfly/ai-infra-ui-components";

Note: this component documents the API and enhances the [existing K8sNameDescriptionField](https://github.com/opendatahub-io/odh-dashboard/blob/main/frontend/src/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField.tsx) component from odh-dashboard. It can be imported from [@patternfly/ai-infra-ui-components](https://www.npmjs.com/package/@patternfly/AI-infra-ui-components). Alternatively, it can be used within the odh-dashboard via the import: `~/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField`

In the following examples, sample `data` and `onDataChange` props are provided. To obtain these props properly, you should use the [useK8sNameDescriptionFieldData hook](https://github.com/opendatahub-io/odh-dashboard/blob/main/frontend/src/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField.tsx#L23).

### Example

```js file="./K8sNameDescriptionFieldBasic.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import {
K8sNameDescriptionField,
K8sNameDescriptionFieldData,
UseK8sNameDescriptionFieldData
} from '@patternfly/ai-infra-ui-components';
import { Checkbox, Form } from '@patternfly/react-core';

export const K8sNameDescriptionFieldBasic: React.FunctionComponent = () => {
const [name, setName] = React.useState('');
const [resourceName, setResourceName] = React.useState<string | null>(null);
const [description, setDescription] = React.useState('');
const [showDescription, setShowDescription] = React.useState(true);

const createResourceName = (name: string) => name.toLowerCase().replaceAll(' ', '-');
const RESOURCE_NAME_MAX_LENGTH = 30;

const data: K8sNameDescriptionFieldData = {
name: name,
description: description,
k8sName: {
value: resourceName ?? createResourceName(name),
state: {
immutable: false,
invalidCharacters: resourceName !== null && resourceName.includes(' '),
invalidLength: resourceName !== null && resourceName.length > RESOURCE_NAME_MAX_LENGTH,
safePrefix: undefined,
maxLength: RESOURCE_NAME_MAX_LENGTH,
touched: false
}
}
};

const onDataChange: UseK8sNameDescriptionFieldData['onDataChange'] = (key, value) => {
if (key === 'name') {
setName(value);
} else if (key === 'k8sName') {
setResourceName(value);
} else if (key === 'description') {
setDescription(value);
}
};

return (
<>
<Checkbox
id="checkbox-1"
onClick={() => setShowDescription((checked) => !checked)}
checked={showDescription}
label="Show description"
/>
<Form>
<K8sNameDescriptionField
showDescription={showDescription}
data={data}
dataTestId="example-0"
onDataChange={onDataChange}
/>
</Form>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import { Popover, FormGroupLabelHelp } from '@patternfly/react-core';

export type FieldGroupHelpLabelIconProps = {
/** Body content of the popover. Should provide additional context for a form field.
* If you want to close the popover after an action within the body content, you can provide a function which will receive a callback as an argument to hide the popover, i.e. bodyContent={hide => <Button onClick={() => hide()}>Close} */
content: React.ComponentProps<typeof Popover>['bodyContent'];
};

/**
* A help icon with a popover. Should be used in FormGroup in a labelHelp prop to provide additional context for a form field.
*/
export const FieldGroupHelpLabelIcon: React.FC<FieldGroupHelpLabelIconProps> = ({
content
}: FieldGroupHelpLabelIconProps) => (
<Popover bodyContent={content}>
<FormGroupLabelHelp aria-label="More info" />
</Popover>
);
1 change: 1 addition & 0 deletions packages/module/src/FieldGroupHelpLabelIcon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './FieldGroupHelpLabelIcon';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react';
import { HelperTextItem } from '@patternfly/react-core';
import { K8sNameDescriptionFieldData } from './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.value.trim().length > 0) {
variant = 'success';
}

return <HelperTextItem variant={variant}>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}>
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,111 @@
import * as React from 'react';
import {
Button,
FormGroup,
FormHelperText,
FormSection,
HelperText,
HelperTextItem,
TextArea,
TextInput
} from '@patternfly/react-core';
import { UseK8sNameDescriptionFieldData } from './types';
import { ResourceNameDefinitionPopover } from './ResourceNameDefinitionPopover';
import { ResourceNameField } from './ResourceNameField';

export type K8sNameDescriptionFieldProps = {
/** Flag to indicate if the initial focus should be in the name text input */
autoFocusName?: boolean;
/** Initial data */
data: UseK8sNameDescriptionFieldData['data'];
/** Test id */
dataTestId: string;
/** Label of the description field */
descriptionLabel?: string;
/** Label of the name field */
nameLabel?: string;
/** Flag indicating whether to show the description field */
showDescription?: boolean;
/** Callback function on text input changes. Accepts 2 arguments.
* First is a key determining which text input we handle (name | k8sName | description), second is the updated value. */
onDataChange?: UseK8sNameDescriptionFieldData['onDataChange'];
};

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

const { name, k8sName, description } = data;

return (
<FormSection style={{ margin: 0 }}>
<FormGroup label={nameLabel} isRequired fieldId={`${dataTestId}-name`}>
<TextInput
aria-readonly={!onDataChange}
data-testid={`${dataTestId}-name`}
id={`${dataTestId}-name`}
name={`${dataTestId}-name`}
autoFocus={autoFocusName}
isRequired
value={name}
onChange={(_event, value) => onDataChange?.('name', value)}
/>
{!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)}
style={{ marginRight: 'var(--pf-t--global--spacer--xs)' }}
>
Edit resource name
</Button>{' '}
<ResourceNameDefinitionPopover />
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
</FormGroup>
<ResourceNameField
allowEdit={showK8sField}
dataTestId={dataTestId}
k8sName={k8sName}
onDataChange={onDataChange}
/>
{showDescription && (
<FormGroup label={descriptionLabel} fieldId={`${dataTestId}-description`}>
<TextArea
aria-readonly={!onDataChange}
data-testid={`${dataTestId}-description`}
id={`${dataTestId}-description`}
name={`${dataTestId}-description`}
value={description}
onChange={(_event, value) => onDataChange?.('description', value)}
resizeOrientation="vertical"
/>
</FormGroup>
)}
</FormSection>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react';
import { Stack, StackItem } from '@patternfly/react-core';
import { FieldGroupHelpLabelIcon } from '../FieldGroupHelpLabelIcon';

export const ResourceNameDefinitionPopover: React.FC = () => (
<FieldGroupHelpLabelIcon
content={
<Stack hasGutter>
<StackItem>Resource names are what your resources are labeled in OpenShift.</StackItem>
<StackItem>Resource names are not editable after creation.</StackItem>
</Stack>
}
/>
);
59 changes: 59 additions & 0 deletions packages/module/src/K8sNameDescriptionField/ResourceNameField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import { FormGroup, HelperText, TextInput, ValidatedOptions } from '@patternfly/react-core';
import { ResourceNameDefinitionPopover } from './ResourceNameDefinitionPopover';
import { HelperTextItemMaxLength, HelperTextItemValidCharacters } from './HelperTextItemVariants';
import { K8sNameDescriptionFieldData, K8sNameDescriptionFieldUpdateFunction } from './types';

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

/** Sub-resource; not for public consumption */
export const ResourceNameField: React.FC<ResourceNameFieldProps> = ({
allowEdit,
dataTestId,
k8sName,
onDataChange
}) => {
const formGroupProps: React.ComponentProps<typeof FormGroup> = {
label: 'Resource name',
labelHelp: <ResourceNameDefinitionPopover />,
fieldId: `${dataTestId}-resourceName`
};

if (k8sName.state.immutable) {
return <FormGroup {...formGroupProps}>{k8sName.value}</FormGroup>;
}

if (!allowEdit) {
return null;
}

let validated: ValidatedOptions = ValidatedOptions.default;
if (k8sName.state.invalidLength || k8sName.state.invalidCharacters) {
validated = ValidatedOptions.error;
} else if (k8sName.value.length > 0) {
validated = ValidatedOptions.success;
}

return (
<FormGroup {...formGroupProps} isRequired>
<TextInput
data-testid={`${dataTestId}-resourceName`}
name={`${dataTestId}-resourceName`}
id={`${dataTestId}-resourceName`}
isRequired
value={k8sName.value}
onChange={(_event, value) => onDataChange?.('k8sName', value)}
validated={validated}
/>
<HelperText>
<HelperTextItemMaxLength k8sName={k8sName} />
<HelperTextItemValidCharacters k8sName={k8sName} />
</HelperText>
</FormGroup>
);
};
2 changes: 2 additions & 0 deletions packages/module/src/K8sNameDescriptionField/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './K8sNameDescriptionField';
export * from './types';
Loading

0 comments on commit fbf966f

Please sign in to comment.