Skip to content

Commit

Permalink
feat: add a search box for filtering actions
Browse files Browse the repository at this point in the history
Signed-off-by: Camila Belo <[email protected]>
  • Loading branch information
camilaibs committed Oct 2, 2024
1 parent 41f8c8e commit 3ac4766
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/silly-readers-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---

Add a actions filter on the list actions page and drawer.
77 changes: 77 additions & 0 deletions plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ApiProvider apis={apis}>
<ActionsPage />
</ApiProvider>,
{
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();
});
});
170 changes: 108 additions & 62 deletions plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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<Action | null>(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]);
Expand Down Expand Up @@ -295,71 +305,107 @@ export const ActionPageContent = () => {
);
};

return value?.map(action => {
if (action.id.startsWith('legacy:')) {
return undefined;
}
return (
<>
<Box pb={3}>
<Autocomplete
id="actions-autocomplete"
options={value}
loading={loading}
getOptionLabel={option => option.id}
renderInput={params => (
<TextField
{...params}
aria-label="Actions"
placeholder="Search for an action"
variant="outlined"
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
)}
onChange={(_event, option) => {
setSelectedAction(option);
}}
fullWidth
/>
</Box>
{(selectedAction ? [selectedAction] : value).map(action => {
if (action.id.startsWith('legacy:')) {
return undefined;
}

const oneOf = renderTables(
'oneOf',
`${action.id}.input`,
action.schema?.input?.oneOf,
);
return (
<Box pb={4} key={action.id}>
<Typography
id={action.id.replaceAll(':', '-')}
variant="h4"
component="h2"
className={classes.code}
>
{action.id}
</Typography>
<Link
className={classes.link}
to={`#${action.id.replaceAll(':', '-')}`}
>
<LinkIcon />
</Link>
{action.description && <MarkdownContent content={action.description} />}
{action.schema?.input && (
<Box pb={2}>
<Typography variant="h5" component="h3">
{t('actionsPage.action.input')}
</Typography>
{renderTable(
formatRows(`${action.id}.input`, action?.schema?.input),
const oneOf = renderTables(
'oneOf',
`${action.id}.input`,
action.schema?.input?.oneOf,
);
return (
<Box pb={3} key={action.id}>
<Box display="flex" alignItems="center">
<Typography
id={action.id.replaceAll(':', '-')}
variant="h5"
component="h2"
className={classes.code}
>
{action.id}
</Typography>
<Link
className={classes.link}
to={`#${action.id.replaceAll(':', '-')}`}
>
<LinkIcon />
</Link>
</Box>
{action.description && (
<MarkdownContent content={action.description} />
)}
{oneOf}
</Box>
)}
{action.schema?.output && (
<Box pb={2}>
<Typography variant="h5" component="h3">
{t('actionsPage.action.output')}
</Typography>
{renderTable(
formatRows(`${action.id}.output`, action?.schema?.output),
{action.schema?.input && (
<Box pb={2}>
<Typography variant="h6" component="h3">
{t('actionsPage.action.input')}
</Typography>
{renderTable(
formatRows(`${action.id}.input`, action?.schema?.input),
)}
{oneOf}
</Box>
)}
</Box>
)}
{action.examples && (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5" component="h3">
{t('actionsPage.action.examples')}
</Typography>
</AccordionSummary>
<AccordionDetails>
{action.schema?.output && (
<Box pb={2}>
<ExamplesTable examples={action.examples} />
<Typography variant="h5" component="h3">
{t('actionsPage.action.output')}
</Typography>
{renderTable(
formatRows(`${action.id}.output`, action?.schema?.output),
)}
</Box>
</AccordionDetails>
</Accordion>
)}
</Box>
);
});
)}
{action.examples && (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h6" component="h3">
{t('actionsPage.action.examples')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Box pb={2}>
<ExamplesTable examples={action.examples} />
</Box>
</AccordionDetails>
</Accordion>
)}
</Box>
);
})}
</>
);
};
export const ActionsPage = () => {
const navigate = useNavigate();
Expand Down

0 comments on commit 3ac4766

Please sign in to comment.