Skip to content

Commit

Permalink
feat: K8sNameDescriptionField and related components
Browse files Browse the repository at this point in the history
  • Loading branch information
adamviktora committed Oct 23, 2024
1 parent 2b30ab4 commit 3bfe3d8
Show file tree
Hide file tree
Showing 16 changed files with 480 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-01"
>
<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,33 @@
---
# 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', 'NameWithResourceNameField']
---

import { K8sNameDescriptionField, NameWithResourceNameField } 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"

```

### Example without description

Internally in ai-infra-ui-components the `K8sNameDescriptionField` is built out of `NameWithResourceNameField` component, which encapsulates only the Name and Resource name fields - it has been created for future use cases where the "Description" field won't occur.

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

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import {
K8sNameDescriptionField,
K8sNameDescriptionFieldData,
UseK8sNameDescriptionFieldData
} from '@patternfly/ai-infra-ui-components';
import { 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 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 (
<Form>
<K8sNameDescriptionField data={data} dataTestId="example-0" onDataChange={onDataChange} />
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import {
NameWithResourceNameField,
NameWithResourceNameFieldData,
UseK8sNameDescriptionFieldData
} from '@patternfly/ai-infra-ui-components';
import { Form } from '@patternfly/react-core';

export const NameWithResourceNameFieldBasic: React.FunctionComponent = () => {
const [name, setName] = React.useState('');
const [resourceName, setResourceName] = React.useState<string | null>(null);

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

const data: NameWithResourceNameFieldData = {
name: name,
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);
}
};

return (
<Form>
<NameWithResourceNameField data={data} dataTestId="example-1" 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,54 @@
import * as React from 'react';
import { FormGroup, FormSection, TextArea } from '@patternfly/react-core';
import { UseK8sNameDescriptionFieldData } from './types';
import { NameWithResourceNameField } from './NameWithResourceNameField';

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;
/** 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
}: K8sNameDescriptionFieldProps) => (
<FormSection style={{ margin: 0 }}>
<NameWithResourceNameField
autoFocusName={autoFocusName}
data={data}
dataTestId={dataTestId}
nameLabel={nameLabel}
onDataChange={onDataChange}
/>
<FormGroup label={descriptionLabel} fieldId={`${dataTestId}-description`}>
<TextArea
aria-readonly={!onDataChange}
data-testid={`${dataTestId}-description`}
id={`${dataTestId}-description`}
name={`${dataTestId}-description`}
value={data.description}
onChange={(_event, value) => onDataChange?.('description', value)}
resizeOrientation="vertical"
/>
</FormGroup>
</FormSection>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from 'react';
import { Button, FormGroup, FormHelperText, HelperText, HelperTextItem, TextInput } from '@patternfly/react-core';
import { ResourceNameDefinitionPopover } from './ResourceNameDefinitionPopover';
import { ResourceNameField } from './ResourceNameField';
import { NameWithResourceNameFieldData, UseK8sNameDescriptionFieldData } from './types';

export type NameWithResourceNameFieldProps = {
/** Flag to indicate if the initial focus should be in the name text input */
autoFocusName?: boolean;
/** Initial data */
data: NameWithResourceNameFieldData;
/** Test id */
dataTestId: string;
/** Label of the name field */
nameLabel?: string;
/** 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'];
};

export const NameWithResourceNameField: React.FC<NameWithResourceNameFieldProps> = ({
autoFocusName,
data,
dataTestId,
nameLabel = 'Name',
onDataChange
}: NameWithResourceNameFieldProps) => {
const [showK8sField, setShowK8sField] = React.useState(false);

const { name, k8sName } = data;

return (
<>
<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}
/>
</>
);
};
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>
}
/>
);
Loading

0 comments on commit 3bfe3d8

Please sign in to comment.