From 3ac4766ca73985188ddac8b9655c5c4d9aceb66e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 1 Oct 2024 12:08:05 +0200 Subject: [PATCH] feat: add a search box for filtering actions Signed-off-by: Camila Belo --- .changeset/silly-readers-build.md | 5 + .../ActionsPage/ActionsPage.test.tsx | 77 ++++++++ .../components/ActionsPage/ActionsPage.tsx | 178 +++++++++++------- 3 files changed, 194 insertions(+), 66 deletions(-) create mode 100644 .changeset/silly-readers-build.md diff --git a/.changeset/silly-readers-build.md b/.changeset/silly-readers-build.md new file mode 100644 index 0000000000..f7f5ad8c01 --- /dev/null +++ b/.changeset/silly-readers-build.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Add a actions filter on the list actions page and drawer. diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index b548bcbad4..bf056dd3b3 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -509,4 +509,81 @@ describe('TemplatePage', () => { expect(rendered.getByText('array(unknown)')).toBeInTheDocument(); }); + + it('should filter an action', async () => { + scaffolderApiMock.listActions.mockResolvedValue([ + { + id: 'githut:repo:create', + description: 'Create a new Github repository', + schema: { + input: { + type: 'object', + required: ['name'], + properties: { + name: { + title: 'Repository name', + type: 'string', + }, + }, + }, + }, + }, + { + id: 'githut:repo:push', + description: 'Push to a Github repository', + schema: { + input: { + type: 'object', + required: ['url'], + properties: { + url: { + title: 'Repository url', + type: 'string', + }, + }, + }, + }, + }, + ]); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes: { + '/create/actions': rootRouteRef, + }, + }, + ); + + expect( + rendered.getByRole('heading', { name: 'githut:repo:create' }), + ).toBeInTheDocument(); + expect( + rendered.getByRole('heading', { name: 'githut:repo:push' }), + ).toBeInTheDocument(); + + // should filter actions when searching + await userEvent.type( + rendered.getByPlaceholderText('Search for an action'), + 'create', + ); + await userEvent.keyboard('[ArrowDown][Enter]'); + expect( + rendered.getByRole('heading', { name: 'githut:repo:create' }), + ).toBeInTheDocument(); + expect( + rendered.queryByRole('heading', { name: 'githut:repo:push' }), + ).not.toBeInTheDocument(); + + // should show all actions when clearing the search + await userEvent.click(rendered.getByTitle('Clear')); + expect( + rendered.getByRole('heading', { name: 'githut:repo:create' }), + ).toBeInTheDocument(); + expect( + rendered.getByRole('heading', { name: 'githut:repo:push' }), + ).toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 0949d948fc..34df875a67 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -16,6 +16,7 @@ import React, { Fragment, useEffect, useState } from 'react'; import useAsync from 'react-use/esm/useAsync'; import { + Action, ActionExample, scaffolderApiRef, } from '@backstage/plugin-scaffolder-react'; @@ -39,6 +40,10 @@ import classNames from 'classnames'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import ExpandLessIcon from '@material-ui/icons/ExpandLess'; import LinkIcon from '@material-ui/icons/Link'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import TextField from '@material-ui/core/TextField'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import SearchIcon from '@material-ui/icons/Search'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { @@ -125,14 +130,19 @@ export const ActionPageContent = () => { const { t } = useTranslationRef(scaffolderTranslationRef); const classes = useStyles(); - const { loading, value, error } = useAsync(async () => { + const { + loading, + value = [], + error, + } = useAsync(async () => { return api.listActions(); }, [api]); + const [selectedAction, setSelectedAction] = useState(null); const [isExpanded, setIsExpanded] = useState<{ [key: string]: boolean }>({}); useEffect(() => { - if (value && window.location.hash) { + if (value.length && window.location.hash) { document.querySelector(window.location.hash)?.scrollIntoView(); } }, [value]); @@ -295,71 +305,107 @@ export const ActionPageContent = () => { ); }; - return value?.map(action => { - if (action.id.startsWith('legacy:')) { - return undefined; - } - - const oneOf = renderTables( - 'oneOf', - `${action.id}.input`, - action.schema?.input?.oneOf, - ); - return ( - - - {action.id} - - - - - {action.description && } - {action.schema?.input && ( - - - {t('actionsPage.action.input')} - - {renderTable( - formatRows(`${action.id}.input`, action?.schema?.input), - )} - {oneOf} - - )} - {action.schema?.output && ( - - - {t('actionsPage.action.output')} - - {renderTable( - formatRows(`${action.id}.output`, action?.schema?.output), - )} - - )} - {action.examples && ( - - }> - - {t('actionsPage.action.examples')} - - - - - - - - - )} + return ( + <> + + option.id} + renderInput={params => ( + + + + ), + }} + /> + )} + onChange={(_event, option) => { + setSelectedAction(option); + }} + fullWidth + /> - ); - }); + {(selectedAction ? [selectedAction] : value).map(action => { + if (action.id.startsWith('legacy:')) { + return undefined; + } + + const oneOf = renderTables( + 'oneOf', + `${action.id}.input`, + action.schema?.input?.oneOf, + ); + return ( + + + + {action.id} + + + + + + {action.description && ( + + )} + {action.schema?.input && ( + + + {t('actionsPage.action.input')} + + {renderTable( + formatRows(`${action.id}.input`, action?.schema?.input), + )} + {oneOf} + + )} + {action.schema?.output && ( + + + {t('actionsPage.action.output')} + + {renderTable( + formatRows(`${action.id}.output`, action?.schema?.output), + )} + + )} + {action.examples && ( + + }> + + {t('actionsPage.action.examples')} + + + + + + + + + )} + + ); + })} + + ); }; export const ActionsPage = () => { const navigate = useNavigate();