feat: add i18n for scaffolder

Signed-off-by: mario ma <mario.ma.node@gmail.com>
This commit is contained in:
mario ma
2024-07-30 11:23:25 +08:00
committed by blam
parent 14c3cab3cb
commit 2ae63cd7a7
45 changed files with 776 additions and 257 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
add i18n for scaffolder
+2
View File
@@ -106,3 +106,5 @@ export default createFrontendPlugin({
}),
extensions: [scaffolderApi, scaffolderPage, scaffolderNavItem],
});
export * from './translation';
@@ -58,6 +58,8 @@ import {
rootRouteRef,
scaffolderListTaskRouteRef,
} from '../../routes';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
code: {
@@ -115,6 +117,7 @@ const ExamplesTable = (props: { examples: ActionExample[] }) => {
const ActionPageContent = () => {
const api = useApi(scaffolderApiRef);
const { t } = useTranslationRef(scaffolderTranslationRef);
const classes = useStyles();
const { loading, value, error } = useAsync(async () => {
@@ -132,8 +135,8 @@ const ActionPageContent = () => {
<ErrorPanel error={error} />
<EmptyState
missing="info"
title="No information to display"
description="There are no actions installed or there was an issue communicating with backend."
title={t('actionsPage.content.emptyState.title')}
description={t('actionsPage.content.emptyState.description')}
/>
</>
);
@@ -141,17 +144,21 @@ const ActionPageContent = () => {
const renderTable = (rows?: JSX.Element[]) => {
if (!rows || rows.length < 1) {
return <Typography>No schema defined</Typography>;
return (
<Typography>{t('actionsPage.content.noRowsDescription')}</Typography>
);
}
return (
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Title</TableCell>
<TableCell>Description</TableCell>
<TableCell>Type</TableCell>
<TableCell>{t('actionsPage.content.tableCell.name')}</TableCell>
<TableCell>{t('actionsPage.content.tableCell.title')}</TableCell>
<TableCell>
{t('actionsPage.content.tableCell.description')}
</TableCell>
<TableCell>{t('actionsPage.content.tableCell.type')}</TableCell>
</TableRow>
</TableHead>
<TableBody>{rows}</TableBody>
@@ -295,7 +302,7 @@ const ActionPageContent = () => {
{action.schema?.input && (
<Box pb={2}>
<Typography variant="h5" component="h3">
Input
{t('actionsPage.action.input')}
</Typography>
{renderTable(
formatRows(`${action.id}.input`, action?.schema?.input),
@@ -306,7 +313,7 @@ const ActionPageContent = () => {
{action.schema?.output && (
<Box pb={2}>
<Typography variant="h5" component="h3">
Output
{t('actionsPage.action.output')}
</Typography>
{renderTable(
formatRows(`${action.id}.output`, action?.schema?.output),
@@ -317,7 +324,7 @@ const ActionPageContent = () => {
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5" component="h3">
Examples
{t('actionsPage.action.examples')}
</Typography>
</AccordionSummary>
<AccordionDetails>
@@ -336,6 +343,7 @@ export const ActionsPage = () => {
const editorLink = useRouteRef(editRouteRef);
const tasksLink = useRouteRef(scaffolderListTaskRouteRef);
const createLink = useRouteRef(rootRouteRef);
const { t } = useTranslationRef(scaffolderTranslationRef);
const scaffolderPageContextMenuProps = {
onEditorClicked: () => navigate(editorLink()),
@@ -347,9 +355,9 @@ export const ActionsPage = () => {
return (
<Page themeId="home">
<Header
pageTitleOverride="Create a New Component"
title="Installed actions"
subtitle="This is the collection of all installed actions"
pageTitleOverride={t('actionsPage.pageTitle')}
title={t('actionsPage.title')}
subtitle={t('actionsPage.subtitle')}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
@@ -41,6 +41,8 @@ import {
import { actionsRouteRef, editRouteRef, rootRouteRef } from '../../routes';
import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/alpha';
import { useNavigate } from 'react-router-dom';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
export interface MyTaskPageProps {
initiallySelectedFilter?: 'owned' | 'all';
@@ -48,6 +50,7 @@ export interface MyTaskPageProps {
const ListTaskPageContent = (props: MyTaskPageProps) => {
const { initiallySelectedFilter = 'owned' } = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const scaffolderApi = useApi(scaffolderApiRef);
const rootLink = useRouteRef(rootRouteRef);
@@ -76,8 +79,8 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
<ErrorPanel error={error} />
<EmptyState
missing="info"
title="No information to display"
description="There are no tasks or there was an issue communicating with backend."
title={t('listTaskPage.content.emptyState.title')}
description={t('listTaskPage.content.emptyState.description')}
/>
</>
);
@@ -94,17 +97,17 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
<CatalogFilterLayout.Content>
<Table<ScaffolderTask>
data={value?.tasks ?? []}
title="Tasks"
title={t('listTaskPage.content.tableTitle')}
columns={[
{
title: 'Task ID',
title: t('listTaskPage.content.tableCell.taskID'),
field: 'id',
render: row => (
<Link to={`${rootLink()}/tasks/${row.id}`}>{row.id}</Link>
),
},
{
title: 'Template',
title: t('listTaskPage.content.tableCell.template'),
field: 'spec.templateInfo.entity.metadata.title',
render: row => (
<TemplateTitleColumn
@@ -113,19 +116,19 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
),
},
{
title: 'Created',
title: t('listTaskPage.content.tableCell.created'),
field: 'createdAt',
render: row => <CreatedAtColumn createdAt={row.createdAt} />,
},
{
title: 'Owner',
title: t('listTaskPage.content.tableCell.owner'),
field: 'createdBy',
render: row => (
<OwnerEntityColumn entityRef={row.spec?.user?.ref} />
),
},
{
title: 'Status',
title: t('listTaskPage.content.tableCell.status'),
field: 'status',
render: row => <TaskStatusColumn status={row.status} />,
},
@@ -141,6 +144,7 @@ export const ListTasksPage = (props: MyTaskPageProps) => {
const editorLink = useRouteRef(editRouteRef);
const actionsLink = useRouteRef(actionsRouteRef);
const createLink = useRouteRef(rootRouteRef);
const { t } = useTranslationRef(scaffolderTranslationRef);
const scaffolderPageContextMenuProps = {
onEditorClicked: () => navigate(editorLink()),
@@ -151,9 +155,9 @@ export const ListTasksPage = (props: MyTaskPageProps) => {
return (
<Page themeId="home">
<Header
pageTitleOverride="Templates Tasks"
title="List template tasks"
subtitle="All tasks that have been started"
pageTitleOverride={t('listTaskPage.pageTitle')}
title={t('listTaskPage.title')}
subtitle={t('listTaskPage.subtitle')}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
@@ -23,6 +23,11 @@ import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import SettingsIcon from '@material-ui/icons/Settings';
import React, { Fragment } from 'react';
import {
TranslationFunction,
useTranslationRef,
} from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import AllIcon from '@material-ui/icons/FontDownload';
@@ -64,19 +69,21 @@ export type ButtonGroup = {
}[];
};
function getFilterGroups(): ButtonGroup[] {
function getFilterGroups(
t: TranslationFunction<typeof scaffolderTranslationRef.T>,
): ButtonGroup[] {
return [
{
name: 'Task Owner',
name: t('ownerListPicker.title'),
items: [
{
id: 'owned',
label: 'Owned',
label: t('ownerListPicker.options.owned'),
icon: SettingsIcon,
},
{
id: 'all',
label: 'All',
label: t('ownerListPicker.options.all'),
icon: AllIcon,
},
],
@@ -90,8 +97,9 @@ export const OwnerListPicker = (props: {
}) => {
const { filter, onSelectOwner } = props;
const classes = useStyles();
const { t } = useTranslationRef(scaffolderTranslationRef);
const filterGroups = getFilterGroups();
const filterGroups = getFilterGroups(t);
return (
<Card className={classes.root}>
{filterGroups.map(group => (
@@ -36,6 +36,8 @@ import {
taskReadPermission,
taskCreatePermission,
} from '@backstage/plugin-scaffolder-common/alpha';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
type ContextMenuProps = {
cancelEnabled?: boolean;
@@ -69,6 +71,7 @@ export const ContextMenu = (props: ContextMenuProps) => {
const scaffolderApi = useApi(scaffolderApiRef);
const analytics = useAnalytics();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
const { t } = useTranslationRef(scaffolderTranslationRef);
const [{ status: cancelStatus }, { execute: cancel }] = useAsync(async () => {
if (taskId) {
@@ -118,14 +121,24 @@ export const ContextMenu = (props: ContextMenuProps) => {
<ListItemIcon>
<Toc fontSize="small" />
</ListItemIcon>
<ListItemText primary={logsVisible ? 'Hide Logs' : 'Show Logs'} />
<ListItemText
primary={
logsVisible
? t('ongoingTask.contextMenu.hideLogs')
: t('ongoingTask.contextMenu.showLogs')
}
/>
</MenuItem>
<MenuItem onClick={() => onToggleButtonBar?.(!buttonBarVisible)}>
<ListItemIcon>
<ControlPointIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary={buttonBarVisible ? 'Hide Button Bar' : 'Show Button Bar'}
primary={
buttonBarVisible
? t('ongoingTask.contextMenu.hideButtonBar')
: t('ongoingTask.contextMenu.showButtonBar')
}
/>
</MenuItem>
<MenuItem
@@ -136,7 +149,7 @@ export const ContextMenu = (props: ContextMenuProps) => {
<ListItemIcon>
<Retry fontSize="small" />
</ListItemIcon>
<ListItemText primary="Start Over" />
<ListItemText primary={t('ongoingTask.contextMenu.startOver')} />
</MenuItem>
<MenuItem
onClick={cancel}
@@ -150,7 +163,7 @@ export const ContextMenu = (props: ContextMenuProps) => {
<ListItemIcon>
<Cancel fontSize="small" />
</ListItemIcon>
<ListItemText primary="Cancel" />
<ListItemText primary={t('ongoingTask.contextMenu.cancel')} />
</MenuItem>
</MenuList>
</Popover>
@@ -42,6 +42,8 @@ import {
taskReadPermission,
taskCreatePermission,
} from '@backstage/plugin-scaffolder-common/alpha';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -85,6 +87,7 @@ export const OngoingTask = (props: {
})) ?? [],
[taskStream],
);
const { t } = useTranslationRef(scaffolderTranslationRef);
const [logsVisible, setLogVisibleState] = useState(false);
const [buttonBarVisible, setButtonBarVisibleState] = useState(true);
@@ -166,7 +169,7 @@ export const OngoingTask = (props: {
const Outputs = props.TemplateOutputsComponent ?? DefaultTemplateOutputs;
const templateName =
taskStream.task?.spec.templateInfo?.entity?.metadata.name;
taskStream.task?.spec.templateInfo?.entity?.metadata.name || '';
const cancelEnabled = !(taskStream.cancelled || taskStream.completed);
@@ -174,14 +177,16 @@ export const OngoingTask = (props: {
<Page themeId="website">
<Header
pageTitleOverride={
templateName ? `Run of ${templateName}` : `Scaffolder Run`
templateName
? t('ongoingTask.pageTitle.hasTemplateName', { templateName })
: t('ongoingTask.pageTitle.noTemplateName')
}
title={
<div>
Run of <code>{templateName}</code>
{t('ongoingTask.title')} <code>{templateName}</code>
</div>
}
subtitle={`Task ${taskId}`}
subtitle={t('ongoingTask.subtitle', { taskId: taskId as string })}
>
<ContextMenu
cancelEnabled={cancelEnabled}
@@ -230,7 +235,7 @@ export const OngoingTask = (props: {
onClick={triggerCancel}
data-testid="cancel-button"
>
Cancel
{t('ongoingTask.cancelButtonTitle')}
</Button>
<Button
className={classes.logsVisibilityButton}
@@ -238,7 +243,9 @@ export const OngoingTask = (props: {
variant="outlined"
onClick={() => setLogVisibleState(!logsVisible)}
>
{logsVisible ? 'Hide Logs' : 'Show Logs'}
{logsVisible
? t('ongoingTask.hideLogsButtonTitle')
: t('ongoingTask.showLogsButtonTitle')}
</Button>
<Button
variant="contained"
@@ -247,7 +254,7 @@ export const OngoingTask = (props: {
onClick={startOver}
data-testid="start-over-button"
>
Start Over
{t('ongoingTask.startOverButtonTitle')}
</Button>
</div>
</Box>
@@ -26,7 +26,7 @@ import {
} from '@backstage/plugin-catalog-react';
import { AlertApi, alertApiRef } from '@backstage/core-plugin-api';
import { ApiProvider } from '@backstage/core-app-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
const entities: Entity[] = [
@@ -86,7 +86,7 @@ const apis = TestApiRegistry.from(
describe('<TemplateTypePicker/>', () => {
it('renders available entity types', async () => {
const rendered = await renderWithEffects(
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -109,7 +109,7 @@ describe('<TemplateTypePicker/>', () => {
});
it('sets the selected type filters', async () => {
const rendered = await renderWithEffects(
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -28,6 +28,8 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { useEntityTypeFilter } from '@backstage/plugin-catalog-react';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
@@ -41,6 +43,7 @@ export const TemplateTypePicker = () => {
const alertApi = useApi(alertApiRef);
const { error, loading, availableTypes, selectedTypes, setSelectedTypes } =
useEntityTypeFilter();
const { t } = useTranslationRef(scaffolderTranslationRef);
if (loading) return <Progress />;
@@ -61,7 +64,7 @@ export const TemplateTypePicker = () => {
component="label"
htmlFor="categories-picker"
>
Categories
{t('templateTypePicker.title')}
</Typography>
<Autocomplete<string, true>
id="categories-picker"
@@ -16,6 +16,8 @@
import React from 'react';
import { EntityNamePickerProps } from './schema';
import TextField from '@material-ui/core/TextField';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export { EntityNamePickerSchema } from './schema';
@@ -23,10 +25,14 @@ export { EntityNamePickerSchema } from './schema';
* EntityName Picker
*/
export const EntityNamePicker = (props: EntityNamePickerProps) => {
const { t } = useTranslationRef(scaffolderTranslationRef);
const {
onChange,
required,
schema: { title = 'Name', description = 'Unique name of the component' },
schema: {
title = t('fields.entityNamePicker.title'),
description = t('fields.entityNamePicker.description'),
},
rawErrors,
formData,
uiSchema: { 'ui:autofocus': autoFocus },
@@ -44,6 +44,8 @@ import {
EntityPickerFilterQuery,
} from './schema';
import { VirtualizedListbox } from '../VirtualizedListbox';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export { EntityPickerSchema } from './schema';
@@ -54,9 +56,13 @@ export { EntityPickerSchema } from './schema';
* @public
*/
export const EntityPicker = (props: EntityPickerProps) => {
const { t } = useTranslationRef(scaffolderTranslationRef);
const {
onChange,
schema: { title = 'Entity', description = 'An entity from the catalog' },
schema: {
title = t('fields.entityPicker.title'),
description = t('fields.entityPicker.description'),
},
required,
uiSchema,
rawErrors,
@@ -24,6 +24,8 @@ import FormControl from '@material-ui/core/FormControl';
import TextField from '@material-ui/core/TextField';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { EntityTagsPickerProps } from './schema';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export { EntityTagsPickerSchema } from './schema';
@@ -43,6 +45,7 @@ export const EntityTagsPicker = (props: EntityTagsPickerProps) => {
const kinds = uiSchema['ui:options']?.kinds;
const showCounts = uiSchema['ui:options']?.showCounts;
const helperText = uiSchema['ui:options']?.helperText;
const { t } = useTranslationRef(scaffolderTranslationRef);
const { loading, value: existingTags } = useAsync(async () => {
const facet = 'metadata.tags';
@@ -109,13 +112,10 @@ export const EntityTagsPicker = (props: EntityTagsPickerProps) => {
renderInput={params => (
<TextField
{...params}
label="Tags"
label={t('fields.entityTagsPicker.title')}
onChange={e => setInputValue(e.target.value)}
error={inputError}
helperText={
helperText ??
"Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters"
}
helperText={helperText ?? t('fields.entityTagsPicker.description')}
FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }}
/>
)}
@@ -15,10 +15,10 @@
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
import { CatalogApi } from '@backstage/catalog-client';
import { MyGroupsPicker } from './MyGroupsPicker';
import { TestApiProvider } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import {
catalogApiRef,
entityPresentationApiRef,
@@ -115,7 +115,7 @@ describe('<MyGroupsPicker />', () => {
required,
} as unknown as FieldProps<string>;
render(
await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, mockIdentityApi],
@@ -191,7 +191,7 @@ describe('<MyGroupsPicker />', () => {
required,
} as unknown as FieldProps<string>;
const { queryByText, getByRole } = render(
const { queryByText, getByRole } = await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, mockIdentityApi],
@@ -252,7 +252,7 @@ describe('<MyGroupsPicker />', () => {
required,
} as unknown as FieldProps<string>;
const { getByRole } = render(
const { getByRole } = await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, mockIdentityApi],
@@ -316,7 +316,7 @@ describe('<MyGroupsPicker />', () => {
formData: 'group:default/group1',
} as unknown as FieldProps<string>;
const { getByRole } = render(
const { getByRole } = await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, mockIdentityApi],
@@ -36,12 +36,18 @@ import { NotFoundError } from '@backstage/errors';
import useAsync from 'react-use/esm/useAsync';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { VirtualizedListbox } from '../VirtualizedListbox';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export { MyGroupsPickerSchema };
export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
const { t } = useTranslationRef(scaffolderTranslationRef);
const {
schema: { title, description },
schema: {
title = t('fields.myGroupsPicker.title'),
description = t('fields.myGroupsPicker.description'),
},
required,
rawErrors,
onChange,
@@ -23,6 +23,8 @@ import { EntityPicker } from '../EntityPicker/EntityPicker';
import { OwnedEntityPickerProps } from './schema';
import { EntityPickerProps } from '../EntityPicker/schema';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export { OwnedEntityPickerSchema } from './schema';
@@ -33,8 +35,12 @@ export { OwnedEntityPickerSchema } from './schema';
* @public
*/
export const OwnedEntityPicker = (props: OwnedEntityPickerProps) => {
const { t } = useTranslationRef(scaffolderTranslationRef);
const {
schema: { title = 'Entity', description = 'An entity from the catalog' },
schema: {
title = t('fields.ownedEntityPicker.title'),
description = t('fields.ownedEntityPicker.description'),
},
uiSchema,
required,
} = props;
@@ -16,6 +16,8 @@
import React from 'react';
import { EntityPicker } from '../EntityPicker/EntityPicker';
import { OwnerPickerProps } from './schema';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export { OwnerPickerSchema } from './schema';
@@ -26,8 +28,12 @@ export { OwnerPickerSchema } from './schema';
* @public
*/
export const OwnerPicker = (props: OwnerPickerProps) => {
const { t } = useTranslationRef(scaffolderTranslationRef);
const {
schema: { title = 'Owner', description = 'The owner of the component' },
schema: {
title = t('fields.ownerPicker.title'),
description = t('fields.ownerPicker.description'),
},
uiSchema,
...restProps
} = props;
@@ -16,11 +16,12 @@
import React from 'react';
import { AzureRepoPicker } from './AzureRepoPicker';
import { render, fireEvent } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
describe('AzureRepoPicker', () => {
it('renders the two input fields', async () => {
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<AzureRepoPicker onChange={jest.fn()} rawErrors={[]} state={{}} />,
);
@@ -30,9 +31,9 @@ describe('AzureRepoPicker', () => {
});
describe('org field', () => {
it('calls onChange when the organisation changes', () => {
it('calls onChange when the organisation changes', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<AzureRepoPicker onChange={onChange} rawErrors={[]} state={{}} />,
);
@@ -45,9 +46,9 @@ describe('AzureRepoPicker', () => {
});
describe('project field', () => {
it('calls onChange when the project changes', () => {
it('calls onChange when the project changes', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<AzureRepoPicker onChange={onChange} rawErrors={[]} state={{}} />,
);
@@ -20,6 +20,8 @@ import FormHelperText from '@material-ui/core/FormHelperText';
import TextField from '@material-ui/core/TextField';
import { BaseRepoUrlPickerProps } from './types';
import { Select, SelectItem } from '@backstage/core-components';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export const AzureRepoPicker = (
props: BaseRepoUrlPickerProps<{
@@ -34,6 +36,7 @@ export const AzureRepoPicker = (
state,
onChange,
} = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const organizationItems: SelectItem[] = allowedOrganizations
? allowedOrganizations.map(i => ({ label: i, value: i }))
@@ -53,27 +56,30 @@ export const AzureRepoPicker = (
error={rawErrors?.length > 0 && !organization}
>
{allowedOrganizations?.length ? (
<Select
native
label="Organization"
onChange={s =>
onChange({ organization: String(Array.isArray(s) ? s[0] : s) })
}
disabled={allowedOrganizations.length === 1}
selected={organization}
items={organizationItems}
/>
<>
<Select
native
label={t('fields.azureRepoPicker.organization.title')}
onChange={s =>
onChange({ organization: String(Array.isArray(s) ? s[0] : s) })
}
disabled={allowedOrganizations.length === 1}
selected={organization}
items={organizationItems}
/>
<FormHelperText>
{t('fields.azureRepoPicker.organization.description')}
</FormHelperText>
</>
) : (
<TextField
id="orgInput"
label="Organization"
label={t('fields.azureRepoPicker.organization.title')}
onChange={e => onChange({ organization: e.target.value })}
helperText={t('fields.azureRepoPicker.organization.description')}
value={organization}
/>
)}
<FormHelperText>
The Organization that this repo will belong to
</FormHelperText>
</FormControl>
<FormControl
margin="normal"
@@ -81,27 +87,30 @@ export const AzureRepoPicker = (
error={rawErrors?.length > 0 && !project}
>
{allowedProject?.length ? (
<Select
native
label="Project"
onChange={s =>
onChange({ project: String(Array.isArray(s) ? s[0] : s) })
}
disabled={allowedProject.length === 1}
selected={project}
items={projectItems}
/>
<>
<Select
native
label={t('fields.azureRepoPicker.project.title')}
onChange={s =>
onChange({ project: String(Array.isArray(s) ? s[0] : s) })
}
disabled={allowedProject.length === 1}
selected={project}
items={projectItems}
/>
<FormHelperText>
{t('fields.azureRepoPicker.project.description')}
</FormHelperText>
</>
) : (
<TextField
id="projectInput"
label="Project"
label={t('fields.azureRepoPicker.project.title')}
onChange={e => onChange({ project: e.target.value })}
value={project}
helperText={t('fields.azureRepoPicker.project.description')}
/>
)}
<FormHelperText>
The Project that this repo will belong to
</FormHelperText>
</FormControl>
</>
);
@@ -16,9 +16,9 @@
import React from 'react';
import { BitbucketRepoPicker } from './BitbucketRepoPicker';
import { render, fireEvent, waitFor } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestApiProvider } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import {
ScaffolderApi,
scaffolderApiRef,
@@ -36,7 +36,7 @@ describe('BitbucketRepoPicker', () => {
it('renders a select if there is a list of allowed owners', async () => {
const allowedOwners = ['owner1', 'owner2'];
const { findByText } = render(
const { findByText } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={jest.fn()}
@@ -51,10 +51,10 @@ describe('BitbucketRepoPicker', () => {
expect(await findByText('owner2')).toBeInTheDocument();
});
it('renders workspace input when host is bitbucket.org', () => {
it('renders workspace input when host is bitbucket.org', async () => {
const state = { host: 'bitbucket.org', workspace: 'lolsWorkspace' };
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={jest.fn()}
@@ -68,12 +68,12 @@ describe('BitbucketRepoPicker', () => {
expect(getAllByRole('textbox')[0]).toHaveValue('lolsWorkspace');
});
it('hides the workspace input when the host is not bitbucket.org', () => {
it('hides the workspace input when the host is not bitbucket.org', async () => {
const state = {
host: 'mycustom.domain.bitbucket.org',
};
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={jest.fn()}
@@ -87,9 +87,9 @@ describe('BitbucketRepoPicker', () => {
});
describe('workspace field', () => {
it('calls onChange when the workspace changes', () => {
it('calls onChange when the workspace changes', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={onChange}
@@ -114,9 +114,9 @@ describe('BitbucketRepoPicker', () => {
});
describe('project field', () => {
it('calls onChange when the project changes', () => {
it('calls onChange when the project changes', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={onChange}
@@ -138,7 +138,7 @@ describe('BitbucketRepoPicker', () => {
});
it('Does not render a select if the list of allowed projects does not exist', async () => {
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={jest.fn()}
@@ -153,7 +153,7 @@ describe('BitbucketRepoPicker', () => {
});
it('Does not render a select if the list of allowed projects is empty', async () => {
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={jest.fn()}
@@ -170,7 +170,7 @@ describe('BitbucketRepoPicker', () => {
it('Does render a select if there is a list of allowed projects', async () => {
const allowedProjects = ['project1', 'project2'];
const { findByText } = render(
const { findByText } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={jest.fn()}
@@ -190,7 +190,7 @@ describe('BitbucketRepoPicker', () => {
it('should populate workspaces if host is set and accessToken is provided', async () => {
const onChange = jest.fn();
const { getAllByRole, getByText } = render(
const { getAllByRole, getByText } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={onChange}
@@ -220,7 +220,7 @@ describe('BitbucketRepoPicker', () => {
it('should populate projects if host and workspace are set and accessToken is provided', async () => {
const onChange = jest.fn();
const { getAllByRole, getByText } = render(
const { getAllByRole, getByText } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={onChange}
@@ -248,7 +248,7 @@ describe('BitbucketRepoPicker', () => {
it('should populate repositories if host, workspace and project are set and accessToken is provided', async () => {
const onChange = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoPicker
onChange={onChange}
@@ -23,6 +23,8 @@ import TextField from '@material-ui/core/TextField';
import useDebounce from 'react-use/esm/useDebounce';
import { useApi } from '@backstage/core-plugin-api';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
/**
* The underlying component that is rendered in the form for the `BitbucketRepoPicker`
@@ -48,6 +50,8 @@ export const BitbucketRepoPicker = (
state,
accessToken,
} = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const { host, workspace, project } = state;
const ownerItems: SelectItem[] = allowedOwners
? allowedOwners?.map(i => ({ label: i, value: i }))
@@ -164,7 +168,7 @@ export const BitbucketRepoPicker = (
{allowedOwners?.length ? (
<Select
native
label="Allowed Workspaces"
label={t('fields.bitbucketRepoPicker.workspaces.title')}
onChange={s =>
onChange({ workspace: String(Array.isArray(s) ? s[0] : s) })
}
@@ -180,14 +184,18 @@ export const BitbucketRepoPicker = (
}}
options={availableWorkspaces}
renderInput={params => (
<TextField {...params} label="Workspace" required />
<TextField
{...params}
label={t('fields.bitbucketRepoPicker.workspaces.inputTitle')}
required
/>
)}
freeSolo
autoSelect
/>
)}
<FormHelperText>
The Workspace that this repo will belong to
{t('fields.bitbucketRepoPicker.workspaces.description')}
</FormHelperText>
</FormControl>
)}
@@ -199,7 +207,7 @@ export const BitbucketRepoPicker = (
{allowedProjects?.length ? (
<Select
native
label="Allowed Projects"
label={t('fields.bitbucketRepoPicker.project.title')}
onChange={s =>
onChange({ project: String(Array.isArray(s) ? s[0] : s) })
}
@@ -215,14 +223,18 @@ export const BitbucketRepoPicker = (
}}
options={availableProjects}
renderInput={params => (
<TextField {...params} label="Project" required />
<TextField
{...params}
label={t('fields.bitbucketRepoPicker.project.inputTitle')}
required
/>
)}
freeSolo
autoSelect
/>
)}
<FormHelperText>
The Project that this repo will belong to
{t('fields.bitbucketRepoPicker.project.description')}
</FormHelperText>
</FormControl>
</>
@@ -16,13 +16,14 @@
import React from 'react';
import { GerritRepoPicker } from './GerritRepoPicker';
import { render, fireEvent } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
describe('GerritRepoPicker', () => {
describe('owner input field', () => {
it('calls onChange when the owner input changes', () => {
it('calls onChange when the owner input changes', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<GerritRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -39,9 +40,9 @@ describe('GerritRepoPicker', () => {
});
describe('parent field', () => {
it('calls onChange when the parent changes', () => {
it('calls onChange when the parent changes', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<GerritRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -18,20 +18,23 @@ import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import TextField from '@material-ui/core/TextField';
import { BaseRepoUrlPickerProps } from './types';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export const GerritRepoPicker = (props: BaseRepoUrlPickerProps) => {
const { onChange, rawErrors, state } = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const { workspace, owner } = state;
return (
<>
<FormControl margin="normal" error={rawErrors?.length > 0 && !workspace}>
<TextField
id="ownerInput"
label="Owner"
label={t('fields.gerritRepoPicker.owner.title')}
onChange={e => onChange({ owner: e.target.value })}
helperText={t('fields.gerritRepoPicker.owner.description')}
value={owner}
/>
<FormHelperText>The owner of the project (optional)</FormHelperText>
</FormControl>
<FormControl
margin="normal"
@@ -40,13 +43,11 @@ export const GerritRepoPicker = (props: BaseRepoUrlPickerProps) => {
>
<TextField
id="parentInput"
label="Parent"
label={t('fields.gerritRepoPicker.parent.title')}
onChange={e => onChange({ workspace: e.target.value })}
value={workspace}
helperText={t('fields.gerritRepoPicker.parent.description')}
/>
<FormHelperText>
The project parent that the repo will belong to
</FormHelperText>
</FormControl>
</>
);
@@ -16,13 +16,14 @@
import React from 'react';
import { GiteaRepoPicker } from './GiteaRepoPicker';
import { render, fireEvent } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
describe('GiteaRepoPicker', () => {
describe('owner input field', () => {
it('calls onChange when the owner input changes', () => {
it('calls onChange when the owner input changes', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<GiteaRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -19,6 +19,8 @@ import FormHelperText from '@material-ui/core/FormHelperText';
import TextField from '@material-ui/core/TextField';
import { Select, SelectItem } from '@backstage/core-components';
import { BaseRepoUrlPickerProps } from './types';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export const GiteaRepoPicker = (
props: BaseRepoUrlPickerProps<{
@@ -27,6 +29,7 @@ export const GiteaRepoPicker = (
}>,
) => {
const { allowedOwners = [], state, onChange, rawErrors } = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const ownerItems: SelectItem[] = allowedOwners
? allowedOwners.map(i => ({ label: i, value: i }))
: [{ label: 'Loading...', value: 'loading' }];
@@ -41,32 +44,36 @@ export const GiteaRepoPicker = (
error={rawErrors?.length > 0 && !owner}
>
{allowedOwners?.length ? (
<Select
native
label="Owner Available"
onChange={selected =>
onChange({
owner: String(Array.isArray(selected) ? selected[0] : selected),
})
}
disabled={allowedOwners.length === 1}
selected={owner}
items={ownerItems}
/>
<>
<Select
native
label={t('fields.giteaRepoPicker.owner.title')}
onChange={selected =>
onChange({
owner: String(
Array.isArray(selected) ? selected[0] : selected,
),
})
}
disabled={allowedOwners.length === 1}
selected={owner}
items={ownerItems}
/>
<FormHelperText>
{t('fields.giteaRepoPicker.owner.description')}
</FormHelperText>
</>
) : (
<>
<TextField
id="ownerInput"
label="Owner"
label={t('fields.giteaRepoPicker.owner.inputTitle')}
onChange={e => onChange({ owner: e.target.value })}
helperText={t('fields.giteaRepoPicker.owner.description')}
value={owner}
/>
</>
)}
<FormHelperText>
Gitea namespace where this repository will belong to. It can be the
name of organization, group, subgroup, user, or the project.
</FormHelperText>
</FormControl>
</>
);
@@ -16,13 +16,14 @@
import React from 'react';
import { GithubRepoPicker } from './GithubRepoPicker';
import { render, fireEvent } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
describe('GithubRepoPicker', () => {
describe('owner field', () => {
it('renders a select if there is a list of allowed owners', async () => {
const allowedOwners = ['owner1', 'owner2'];
const { findByText } = render(
const { findByText } = await renderInTestApp(
<GithubRepoPicker
onChange={jest.fn()}
rawErrors={[]}
@@ -38,7 +39,7 @@ describe('GithubRepoPicker', () => {
it('calls onChange when the owner is changed to a different owner', async () => {
const onChange = jest.fn();
const allowedOwners = ['owner1', 'owner2'];
const { getByRole } = render(
const { getByRole } = await renderInTestApp(
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -54,10 +55,10 @@ describe('GithubRepoPicker', () => {
expect(onChange).toHaveBeenCalledWith({ owner: 'owner2' });
});
it('is disabled picked when only one allowed owner', () => {
it('is disabled picked when only one allowed owner', async () => {
const onChange = jest.fn();
const allowedOwners = ['owner1'];
const { getByRole } = render(
const { getByRole } = await renderInTestApp(
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -71,7 +72,7 @@ describe('GithubRepoPicker', () => {
it('should display free text if no allowed owners are passed', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -19,6 +19,8 @@ import FormHelperText from '@material-ui/core/FormHelperText';
import TextField from '@material-ui/core/TextField';
import { Select, SelectItem } from '@backstage/core-components';
import { BaseRepoUrlPickerProps } from './types';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export const GithubRepoPicker = (
props: BaseRepoUrlPickerProps<{
@@ -26,6 +28,7 @@ export const GithubRepoPicker = (
}>,
) => {
const { allowedOwners = [], rawErrors, state, onChange } = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const ownerItems: SelectItem[] = allowedOwners
? allowedOwners.map(i => ({ label: i, value: i }))
: [{ label: 'Loading...', value: 'loading' }];
@@ -40,27 +43,30 @@ export const GithubRepoPicker = (
error={rawErrors?.length > 0 && !owner}
>
{allowedOwners?.length ? (
<Select
native
label="Owner Available"
onChange={s =>
onChange({ owner: String(Array.isArray(s) ? s[0] : s) })
}
disabled={allowedOwners.length === 1}
selected={owner}
items={ownerItems}
/>
<>
<Select
native
label={t('fields.githubRepoPicker.owner.title')}
onChange={s =>
onChange({ owner: String(Array.isArray(s) ? s[0] : s) })
}
disabled={allowedOwners.length === 1}
selected={owner}
items={ownerItems}
/>
<FormHelperText>
{t('fields.githubRepoPicker.owner.description')}
</FormHelperText>
</>
) : (
<TextField
id="ownerInput"
label="Owner"
label={t('fields.githubRepoPicker.owner.inputTitle')}
onChange={e => onChange({ owner: e.target.value })}
helperText={t('fields.githubRepoPicker.owner.description')}
value={owner}
/>
)}
<FormHelperText>
The organization, user or project that this repo will belong to
</FormHelperText>
</FormControl>
</>
);
@@ -16,13 +16,14 @@
import React from 'react';
import { GitlabRepoPicker } from './GitlabRepoPicker';
import { render, fireEvent } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
describe('GitlabRepoPicker', () => {
describe('owner field', () => {
it('renders a select if there is a list of allowed owners', async () => {
const allowedOwners = ['owner1', 'owner2'];
const { findByText } = render(
const { findByText } = await renderInTestApp(
<GitlabRepoPicker
onChange={jest.fn()}
rawErrors={[]}
@@ -38,7 +39,7 @@ describe('GitlabRepoPicker', () => {
it('calls onChange when the owner is changed to a different owner', async () => {
const onChange = jest.fn();
const allowedOwners = ['owner1', 'owner2'];
const { getByRole } = render(
const { getByRole } = await renderInTestApp(
<GitlabRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -54,10 +55,10 @@ describe('GitlabRepoPicker', () => {
expect(onChange).toHaveBeenCalledWith({ owner: 'owner2' });
});
it('is disabled picked when only one allowed owner', () => {
it('is disabled picked when only one allowed owner', async () => {
const onChange = jest.fn();
const allowedOwners = ['owner1'];
const { getByRole } = render(
const { getByRole } = await renderInTestApp(
<GitlabRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -71,7 +72,7 @@ describe('GitlabRepoPicker', () => {
it('should display free text if no allowed owners are passed', async () => {
const onChange = jest.fn();
const { getAllByRole } = render(
const { getAllByRole } = await renderInTestApp(
<GitlabRepoPicker
onChange={onChange}
rawErrors={[]}
@@ -19,6 +19,8 @@ import FormHelperText from '@material-ui/core/FormHelperText';
import TextField from '@material-ui/core/TextField';
import { Select, SelectItem } from '@backstage/core-components';
import { BaseRepoUrlPickerProps } from './types';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export const GitlabRepoPicker = (
props: BaseRepoUrlPickerProps<{
@@ -27,6 +29,7 @@ export const GitlabRepoPicker = (
}>,
) => {
const { allowedOwners = [], state, onChange, rawErrors } = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const ownerItems: SelectItem[] = allowedOwners
? allowedOwners.map(i => ({ label: i, value: i }))
: [{ label: 'Loading...', value: 'loading' }];
@@ -41,30 +44,34 @@ export const GitlabRepoPicker = (
error={rawErrors?.length > 0 && !owner}
>
{allowedOwners?.length ? (
<Select
native
label="Owner Available"
onChange={selected =>
onChange({
owner: String(Array.isArray(selected) ? selected[0] : selected),
})
}
disabled={allowedOwners.length === 1}
selected={owner}
items={ownerItems}
/>
<>
<Select
native
label={t('fields.gitlabRepoPicker.owner.title')}
onChange={selected =>
onChange({
owner: String(
Array.isArray(selected) ? selected[0] : selected,
),
})
}
disabled={allowedOwners.length === 1}
selected={owner}
items={ownerItems}
/>
<FormHelperText>
{t('fields.gitlabRepoPicker.owner.description')}
</FormHelperText>
</>
) : (
<TextField
id="ownerInput"
label="Owner"
label={t('fields.gitlabRepoPicker.owner.inputTitle')}
onChange={e => onChange({ owner: e.target.value })}
helperText={t('fields.gitlabRepoPicker.owner.description')}
value={owner}
/>
)}
<FormHelperText>
GitLab namespace where this repository will belong to. It can be the
name of organization, group, subgroup, user, or the project.
</FormHelperText>
</FormControl>
</>
);
@@ -20,6 +20,8 @@ import FormHelperText from '@material-ui/core/FormHelperText';
import { useApi } from '@backstage/core-plugin-api';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import useAsync from 'react-use/esm/useAsync';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export const RepoUrlPickerHost = (props: {
host?: string;
@@ -28,6 +30,7 @@ export const RepoUrlPickerHost = (props: {
rawErrors: string[];
}) => {
const { host, hosts, onChange, rawErrors } = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const scaffolderApi = useApi(scaffolderApiRef);
const { value: { integrations } = { integrations: [] }, loading } = useAsync(
@@ -73,7 +76,7 @@ export const RepoUrlPickerHost = (props: {
<Select
native
disabled={hosts?.length === 1}
label="Host"
label={t('fields.repoUrlPickerHost.host.title')}
onChange={s => onChange(String(Array.isArray(s) ? s[0] : s))}
selected={host}
items={hostsOptions}
@@ -81,7 +84,7 @@ export const RepoUrlPickerHost = (props: {
/>
<FormHelperText>
The host where the repository will be created
{t('fields.repoUrlPickerHost.host.description')}
</FormHelperText>
</FormControl>
</>
@@ -15,15 +15,16 @@
*/
import React from 'react';
import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName';
import { render, fireEvent } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
import { renderInTestApp } from '@backstage/test-utils';
describe('RepoUrlPickerRepoName', () => {
it('should call onChange with the first allowed repo if there is none set already', async () => {
const onChange = jest.fn();
render(
await renderInTestApp(
<RepoUrlPickerRepoName
onChange={onChange}
allowedRepos={['foo', 'bar']}
@@ -39,7 +40,7 @@ describe('RepoUrlPickerRepoName', () => {
const onChange = jest.fn();
const { getByRole } = render(
const { getByRole } = await renderInTestApp(
<RepoUrlPickerRepoName
onChange={onChange}
allowedRepos={allowedRepos}
@@ -59,7 +60,7 @@ describe('RepoUrlPickerRepoName', () => {
it('should render a normal text area when no options are passed', async () => {
const onChange = jest.fn();
const { getByRole } = render(
const { getByRole } = await renderInTestApp(
<RepoUrlPickerRepoName
onChange={onChange}
allowedRepos={[]}
@@ -85,7 +86,7 @@ describe('RepoUrlPickerRepoName', () => {
const onChange = jest.fn();
const { getByRole, getByText } = render(
const { getByRole, getByText } = await renderInTestApp(
<RepoUrlPickerRepoName
onChange={onChange}
availableRepos={availableRepos}
@@ -19,6 +19,8 @@ import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import Autocomplete from '@material-ui/lab/Autocomplete';
import TextField from '@material-ui/core/TextField';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
export const RepoUrlPickerRepoName = (props: {
repoName?: string;
@@ -28,6 +30,7 @@ export const RepoUrlPickerRepoName = (props: {
availableRepos?: string[];
}) => {
const { repoName, allowedRepos, onChange, rawErrors, availableRepos } = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
useEffect(() => {
// If there is no repoName chosen currently
@@ -53,7 +56,7 @@ export const RepoUrlPickerRepoName = (props: {
{allowedRepos?.length ? (
<Select
native
label="Repositories Available"
label={t('fields.repoUrlPickerRepoName.repository.title')}
onChange={selected =>
onChange(String(Array.isArray(selected) ? selected[0] : selected))
}
@@ -69,13 +72,19 @@ export const RepoUrlPickerRepoName = (props: {
}}
options={availableRepos || []}
renderInput={params => (
<TextField {...params} label="Repository" required />
<TextField
{...params}
label={t('fields.repoUrlPickerRepoName.repository.inputTitle')}
required
/>
)}
freeSolo
autoSelect
/>
)}
<FormHelperText>The name of the repository</FormHelperText>
<FormHelperText>
{t('fields.repoUrlPickerRepoName.repository.description')}
</FormHelperText>
</FormControl>
</>
);
@@ -33,6 +33,8 @@ import { Form } from '@backstage/plugin-scaffolder-react/alpha';
import { TemplateEditorForm } from './TemplateEditorForm';
import validator from '@rjsf/validator-ajv8';
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
root: {
@@ -68,6 +70,7 @@ export const CustomFieldExplorer = ({
onClose?: () => void;
}) => {
const classes = useStyles();
const { t } = useTranslationRef(scaffolderTranslationRef);
const fieldOptions = customFieldExtensions.filter(field => !!field.schema);
const [selectedField, setSelectedField] = useState(fieldOptions[0]);
const [fieldFormState, setFieldFormState] = useState({});
@@ -120,11 +123,11 @@ export const CustomFieldExplorer = ({
<div className={classes.controls}>
<FormControl variant="outlined" size="small" fullWidth>
<InputLabel id="select-field-label">
Choose Custom Field Extension
{t('templateEditorPage.customFieldExplorer.selectFieldLabel')}
</InputLabel>
<Select
value={selectedField}
label="Choose Custom Field Extension"
label={t('templateEditorPage.customFieldExplorer.selectFieldLabel')}
labelId="select-field-label"
onChange={e =>
handleSelectionChange(e.target.value as FieldExtensionOptions)
@@ -144,7 +147,9 @@ export const CustomFieldExplorer = ({
</div>
<div className={classes.fieldForm}>
<Card>
<CardHeader title="Field Options" />
<CardHeader
title={t('templateEditorPage.customFieldExplorer.fieldForm.title')}
/>
<CardContent>
<Form
showErrorList={false}
@@ -165,7 +170,9 @@ export const CustomFieldExplorer = ({
type="submit"
disabled={!selectedField.schema?.uiOptions}
>
Apply
{t(
'templateEditorPage.customFieldExplorer.fieldForm.applyButtonTitle',
)}
</Button>
</Form>
</CardContent>
@@ -173,7 +180,9 @@ export const CustomFieldExplorer = ({
</div>
<div className={classes.preview}>
<Card>
<CardHeader title="Example Template Spec" />
<CardHeader
title={t('templateEditorPage.customFieldExplorer.preview.title')}
/>
<CardContent>
<CodeMirror
readOnly
@@ -26,6 +26,8 @@ import React, { useEffect, useState } from 'react';
import { useDryRun } from '../DryRunContext';
import { DryRunResultsList } from './DryRunResultsList';
import { DryRunResultsView } from './DryRunResultsView';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
const useStyles = makeStyles(theme => ({
header: {
@@ -51,6 +53,7 @@ export function DryRunResults() {
const dryRun = useDryRun();
const [expanded, setExpanded] = useState(false);
const [hidden, setHidden] = useState(true);
const { t } = useTranslationRef(scaffolderTranslationRef);
const resultsLength = dryRun.results.length;
const prevResultsLength = usePrevious(resultsLength);
@@ -76,7 +79,7 @@ export function DryRunResults() {
className={classes.header}
expandIcon={<ExpandMoreIcon />}
>
<Typography>Dry-run results</Typography>
<Typography>{t('templateEditorPage.dryRunResults.title')}</Typography>
</AccordionSummary>
<Divider orientation="horizontal" />
<AccordionDetails className={classes.content}>
@@ -28,6 +28,8 @@ import DownloadIcon from '@material-ui/icons/GetApp';
import React from 'react';
import { useDryRun } from '../DryRunContext';
import { downloadBlob } from '../../../lib/download';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
const useStyles = makeStyles(theme => ({
root: {
@@ -49,6 +51,7 @@ const useStyles = makeStyles(theme => ({
export function DryRunResultsList() {
const classes = useStyles();
const dryRun = useDryRun();
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<List className={classes.root} dense>
@@ -77,12 +80,18 @@ export function DryRunResultsList() {
>
{failed ? <CancelIcon /> : <CheckIcon />}
</ListItemIcon>
<ListItemText primary={`Result ${result.id}`} />
<ListItemText
primary={t('templateEditorPage.dryRunResultsList.title', {
resultId: `${result.id}`,
})}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
aria-label="download"
title="Download as .zip"
title={t(
'templateEditorPage.dryRunResultsList.downloadButtonTitle',
)}
disabled={isLoading}
onClick={() => downloadResult()}
>
@@ -91,7 +100,9 @@ export function DryRunResultsList() {
<IconButton
edge="end"
aria-label="delete"
title="Delete result"
title={t(
'templateEditorPage.dryRunResultsList.deleteButtonTitle',
)}
onClick={() => dryRun.deleteResult(result.id)}
>
<DeleteIcon />
@@ -29,6 +29,8 @@ import { DryRunResultsSplitView } from './DryRunResultsSplitView';
import { FileBrowser } from '../../../components/FileBrowser';
import { TaskPageLinks } from './TaskPageLinks';
import { TaskStatusStepper } from './TaskStatusStepper';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
const useStyles = makeStyles({
root: {
@@ -173,13 +175,23 @@ export function DryRunResultsView() {
const [selectedTab, setSelectedTab] = useState<'files' | 'log' | 'output'>(
'files',
);
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<div className={classes.root}>
<Tabs value={selectedTab} onChange={(_, v) => setSelectedTab(v)}>
<Tab value="files" label="Files" />
<Tab value="log" label="Log" />
<Tab value="output" label="Output" />
<Tab
value="files"
label={t('templateEditorPage.dryRunResultsView.tab.files')}
/>
<Tab
value="log"
label={t('templateEditorPage.dryRunResultsView.tab.log')}
/>
<Tab
value="output"
label={t('templateEditorPage.dryRunResultsView.tab.output')}
/>
</Tabs>
<Divider />
@@ -31,6 +31,8 @@ import { DateTime, Interval } from 'luxon';
import useInterval from 'react-use/esm/useInterval';
import humanizeDuration from 'humanize-duration';
import classNames from 'classnames';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -148,6 +150,7 @@ export const TaskStatusStepper = memo(
}) => {
const { steps, currentStepId, onUserStepChange } = props;
const classes = useStyles(props);
const { t } = useTranslationRef(scaffolderTranslationRef);
return (
<div className={classes.root}>
@@ -178,7 +181,11 @@ export const TaskStatusStepper = memo(
<div className={classes.labelWrapper}>
<Typography variant="subtitle2">{step.name}</Typography>
{isSkipped ? (
<Typography variant="caption">Skipped</Typography>
<Typography variant="caption">
{t(
'templateEditorPage.taskStatusStepper.skippedStepTitle',
)}
</Typography>
) : (
<StepTimeTicker step={step} />
)}
@@ -23,6 +23,8 @@ import SaveIcon from '@material-ui/icons/Save';
import React from 'react';
import { useDirectoryEditor } from './DirectoryEditorContext';
import { FileBrowser } from '../../components/FileBrowser';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
button: {
@@ -47,6 +49,7 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
const classes = useStyles();
const directoryEditor = useDirectoryEditor();
const changedFiles = directoryEditor.files.filter(file => file.dirty);
const { t } = useTranslationRef(scaffolderTranslationRef);
const handleClose = () => {
if (!props.onClose) {
@@ -55,7 +58,7 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
if (changedFiles.length > 0) {
// eslint-disable-next-line no-alert
const accepted = window.confirm(
'Are you sure? Unsaved changes will be lost',
t('templateEditorPage.templateEditorBrowser.closeConfirmMessage'),
);
if (!accepted) {
return;
@@ -67,7 +70,9 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
return (
<>
<div className={classes.buttons}>
<Tooltip title="Save all files">
<Tooltip
title={t('templateEditorPage.templateEditorBrowser.saveIconTooltip')}
>
<IconButton
className={classes.button}
disabled={directoryEditor.files.every(file => !file.dirty)}
@@ -76,7 +81,11 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
<SaveIcon />
</IconButton>
</Tooltip>
<Tooltip title="Reload directory">
<Tooltip
title={t(
'templateEditorPage.templateEditorBrowser.reloadIconTooltip',
)}
>
<IconButton
className={classes.button}
onClick={() => directoryEditor.reload()}
@@ -85,7 +94,9 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
</IconButton>
</Tooltip>
<div className={classes.buttonsGap} />
<Tooltip title="Close directory">
<Tooltip
title={t('templateEditorPage.templateEditorBrowser.closeIconTooltip')}
>
<IconButton className={classes.button} onClick={handleClose}>
<CloseIcon />
</IconButton>
@@ -23,6 +23,8 @@ import Typography from '@material-ui/core/Typography';
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';
import { makeStyles } from '@material-ui/core/styles';
import { WebFileSystemAccess } from '../../lib/filesystem';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
introText: {
@@ -50,6 +52,7 @@ interface EditorIntroProps {
export function TemplateEditorIntro(props: EditorIntroProps) {
const classes = useStyles();
const supportsLoad = WebFileSystemAccess.isSupported();
const { t } = useTranslationRef(scaffolderTranslationRef);
const cardLoadLocal = (
<Card className={classes.card} elevation={4}>
@@ -65,14 +68,13 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
color={supportsLoad ? undefined : 'textSecondary'}
style={{ display: 'flex', flexFlow: 'row nowrap' }}
>
Load Template Directory
{t('templateEditorPage.templateEditorIntro.loadLocal.title')}
</Typography>
<Typography
variant="body1"
color={supportsLoad ? undefined : 'textSecondary'}
>
Load a local template directory, allowing you to both edit and try
executing your own template.
{t('templateEditorPage.templateEditorIntro.loadLocal.description')}
</Typography>
</CardContent>
</CardActionArea>
@@ -80,7 +82,9 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
<div className={classes.infoIcon}>
<Tooltip
placement="top"
title="Only supported in some Chromium-based browsers"
title={t(
'templateEditorPage.templateEditorIntro.loadLocal.unSupportsLoadTooltip',
)}
>
<InfoOutlinedIcon />
</Tooltip>
@@ -94,11 +98,10 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
<CardActionArea onClick={() => props.onSelect?.('form')}>
<CardContent>
<Typography variant="h4" component="h3" gutterBottom>
Edit Template Form
{t('templateEditorPage.templateEditorIntro.formEditor.title')}
</Typography>
<Typography variant="body1">
Preview and edit a template form, either using a sample template or
by loading a template from the catalog.
{t('templateEditorPage.templateEditorIntro.formEditor.description')}
</Typography>
</CardContent>
</CardActionArea>
@@ -110,11 +113,12 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
<CardActionArea onClick={() => props.onSelect?.('field-explorer')}>
<CardContent>
<Typography variant="h4" component="h3" gutterBottom>
Custom Field Explorer
{t('templateEditorPage.templateEditorIntro.fieldExplorer.title')}
</Typography>
<Typography variant="body1">
View and play around with available installed custom field
extensions.
{t(
'templateEditorPage.templateEditorIntro.fieldExplorer.description',
)}
</Typography>
</CardContent>
</CardActionArea>
@@ -124,7 +128,7 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
return (
<div style={props.style}>
<Typography variant="h4" component="h2" className={classes.introText}>
Get started by choosing one of the options below
{t('templateEditorPage.templateEditorIntro.title')}
</Typography>
<div
style={{
@@ -35,6 +35,8 @@ import {
rootRouteRef,
scaffolderListTaskRouteRef,
} from '../../routes';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
type Selection =
| {
@@ -60,6 +62,7 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
const actionsLink = useRouteRef(actionsRouteRef);
const tasksLink = useRouteRef(scaffolderListTaskRouteRef);
const createLink = useRouteRef(rootRouteRef);
const { t } = useTranslationRef(scaffolderTranslationRef);
const scaffolderPageContextMenuProps = {
onEditorClicked: undefined,
@@ -117,8 +120,8 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
return (
<Page themeId="home">
<Header
title="Template Editor"
subtitle="Edit, preview, and try out templates and template forms"
title={t('templateEditorPage.title')}
subtitle={t('templateEditorPage.subTitle')}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
@@ -27,6 +27,8 @@ import { useKeyboardEvent } from '@react-hookz/web';
import CodeMirror from '@uiw/react-codemirror';
import React, { useMemo } from 'react';
import { useDirectoryEditor } from './DirectoryEditorContext';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
container: {
@@ -66,6 +68,7 @@ export function TemplateEditorTextArea(props: {
}) {
const { errorText } = props;
const classes = useStyles();
const { t } = useTranslationRef(scaffolderTranslationRef);
const panelExtension = useMemo(() => {
if (!errorText) {
@@ -102,7 +105,11 @@ export function TemplateEditorTextArea(props: {
<div className={classes.floatingButtons}>
<Paper>
{props.onSave && (
<Tooltip title="Save file">
<Tooltip
title={t(
'templateEditorPage.templateEditorTextArea.saveIconTooltip',
)}
>
<IconButton
className={classes.floatingButton}
onClick={() => props.onSave?.()}
@@ -112,7 +119,11 @@ export function TemplateEditorTextArea(props: {
</Tooltip>
)}
{props.onReload && (
<Tooltip title="Reload file">
<Tooltip
title={t(
'templateEditorPage.templateEditorTextArea.refreshIconTooltip',
)}
>
<IconButton
className={classes.floatingButton}
onClick={() => props.onReload?.()}
@@ -36,6 +36,8 @@ import {
} from '@backstage/plugin-scaffolder-react';
import { TemplateEditorForm } from './TemplateEditorForm';
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const EXAMPLE_TEMPLATE_PARAMS_YAML = `# Edit the template parameters below to see how they will render in the scaffolder form UI
parameters:
@@ -119,6 +121,7 @@ export const TemplateFormPreviewer = ({
layouts?: LayoutOptions[];
}) => {
const classes = useStyles();
const { t } = useTranslationRef(scaffolderTranslationRef);
const alertApi = useApi(alertApiRef);
const catalogApi = useApi(catalogApiRef);
const [selectedTemplate, setSelectedTemplate] = useState('');
@@ -176,11 +179,11 @@ export const TemplateFormPreviewer = ({
<div className={classes.controls}>
<FormControl variant="outlined" size="small" fullWidth>
<InputLabel id="select-template-label">
Load Existing Template
{t('templateEditorPage.templateFormPreviewer.title')}
</InputLabel>
<Select
value={selectedTemplate}
label="Load Existing Template"
label={t('templateEditorPage.templateFormPreviewer.title')}
labelId="select-template-label"
onChange={e => handleSelectChange(e.target.value)}
>
@@ -52,6 +52,11 @@ import {
} from '../../routes';
import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
import {
TranslationFunction,
useTranslationRef,
} from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
/**
* @alpha
@@ -74,17 +79,13 @@ export type TemplateListPageProps = {
};
};
const defaultGroup: TemplateGroupFilter = {
title: 'Templates',
filter: () => true,
};
const createGroupsWithOther = (
groups: TemplateGroupFilter[],
t: TranslationFunction<typeof scaffolderTranslationRef.T>,
): TemplateGroupFilter[] => [
...groups,
{
title: 'Other Templates',
title: t('templateListPage.templateGroups.otherTitle'),
filter: e => ![...groups].some(({ filter }) => filter(e)),
},
];
@@ -107,10 +108,16 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
const viewTechDocsLink = useRouteRef(viewTechDocRouteRef);
const templateRoute = useRouteRef(selectedTemplateRouteRef);
const app = useApp();
const { t } = useTranslationRef(scaffolderTranslationRef);
const groups = givenGroups.length
? createGroupsWithOther(givenGroups)
: [defaultGroup];
? createGroupsWithOther(givenGroups, t)
: [
{
title: t('templateListPage.templateGroups.defaultTitle'),
filter: () => true,
},
];
const scaffolderPageContextMenuProps = {
onEditorClicked:
@@ -137,13 +144,15 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
? [
{
icon: app.getSystemIcon('docs') ?? DocsIcon,
text: 'View TechDocs',
text: t(
'templateListPage.additionalLinksForEntity.viewTechDocsTitle',
),
url: viewTechDocsLink({ kind, namespace, name }),
},
]
: [];
},
[app, viewTechDocsLink],
[app, viewTechDocsLink, t],
);
const onTemplateSelected = useCallback(
@@ -159,23 +168,23 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
<EntityListProvider>
<Page themeId="home">
<Header
pageTitleOverride="Create a new component"
title="Create a new component"
subtitle="Create new software components using standard templates in your organization"
pageTitleOverride={t('templateListPage.pageTitle')}
title={t('templateListPage.title')}
subtitle={t('templateListPage.subtitle')}
{...headerOptions}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
<Content>
<ContentHeader title="Available Templates">
<ContentHeader title={t('templateListPage.contentHeader.title')}>
<RegisterExistingButton
title="Register Existing Component"
title={t(
'templateListPage.contentHeader.registerExistingButtonTitle',
)}
to={registerComponentLink && registerComponentLink()}
/>
<SupportButton>
Create new software components using standard templates. Different
templates create different kinds of components (services,
websites, documentation, ...).
{t('templateListPage.contentHeader.supportButtonTitle')}
</SupportButton>
</ContentHeader>
@@ -45,6 +45,8 @@ import {
scaffolderTaskRouteRef,
selectedTemplateRouteRef,
} from '../../routes';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import { TemplateWizardPageContextMenu } from './TemplateWizardPageContextMenu';
@@ -75,6 +77,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
const { templateName, namespace } = useRouteRefParams(
selectedTemplateRouteRef,
);
const { t } = useTranslationRef(scaffolderTranslationRef);
const templateRef = stringifyEntityRef({
kind: 'Template',
@@ -103,9 +106,9 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
<AnalyticsContext attributes={{ entityRef: templateRef }}>
<Page themeId="website">
<Header
pageTitleOverride="Create a new component"
title="Create a new component"
subtitle="Create new software components using standard templates in your organization"
pageTitleOverride={t('templateWizardPage.pageTitle')}
title={t('templateWizardPage.title')}
subtitle={t('templateWizardPage.subtitle')}
{...props.headerOptions}
>
<TemplateWizardPageContextMenu editUrl={editUrl} />
@@ -24,6 +24,8 @@ import { makeStyles } from '@material-ui/core/styles';
import Edit from '@material-ui/icons/Edit';
import MoreVert from '@material-ui/icons/MoreVert';
import React, { useState } from 'react';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
button: {
@@ -41,6 +43,7 @@ export function TemplateWizardPageContextMenu(
const { editUrl } = props;
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
const { t } = useTranslationRef(scaffolderTranslationRef);
if (!editUrl) {
return null;
@@ -83,7 +86,11 @@ export function TemplateWizardPageContextMenu(
<ListItemIcon>
<Edit fontSize="small" />
</ListItemIcon>
<ListItemText primary="Edit Configuration" />
<ListItemText
primary={t(
'templateWizardPage.pageContextMenu.editConfigurationTitle',
)}
/>
</MenuItem>
</MenuList>
</Popover>
+286
View File
@@ -0,0 +1,286 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @alpha */
export const scaffolderTranslationRef = createTranslationRef({
id: 'scaffolder',
messages: {
actionsPage: {
title: 'Installed actions',
pageTitle: 'Create a New Component',
subtitle: 'This is the collection of all installed actions',
content: {
emptyState: {
title: 'No information to display',
description:
'There are no actions installed or there was an issue communicating with backend.',
},
tableCell: {
name: 'Name',
title: 'Title',
description: 'Description',
type: 'Type',
},
noRowsDescription: 'No schema defined',
},
action: {
input: 'Input',
output: 'Output',
examples: 'Examples',
},
},
fields: {
entityNamePicker: {
title: 'Name',
description: 'Unique name of the component',
},
entityPicker: {
title: 'Entity',
description: 'An entity from the catalog',
},
entityTagsPicker: {
title: 'Tags',
description:
"Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters",
},
myGroupsPicker: {
title: 'Entity',
description: 'An entity from the catalog',
},
ownedEntityPicker: {
title: 'Entity',
description: 'An entity from the catalog',
},
ownerPicker: {
title: 'Owner',
description: 'The owner of the component',
},
azureRepoPicker: {
organization: {
title: 'Organization',
description: 'The Organization that this repo will belong to',
},
project: {
title: 'Project',
description: 'The Project that this repo will belong to',
},
},
bitbucketRepoPicker: {
workspaces: {
title: 'Allowed Workspaces',
inputTitle: 'Workspaces',
description: 'The Workspace that this repo will belong to',
},
project: {
title: 'Allowed Projects',
inputTitle: 'Projects',
description: 'The Project that this repo will belong to',
},
},
gerritRepoPicker: {
owner: {
title: 'Owner',
description: 'The owner of the project (optional)',
},
parent: {
title: 'Parent',
description: 'The project parent that the repo will belong to',
},
},
giteaRepoPicker: {
owner: {
title: 'Owner Available',
inputTitle: 'Owner',
description:
'Gitea namespace where this repository will belong to. It can be the name of organization, group, subgroup, user, or the project.',
},
},
githubRepoPicker: {
owner: {
title: 'Owner Available',
inputTitle: 'Owner',
description:
'The organization, user or project that this repo will belong to',
},
},
gitlabRepoPicker: {
owner: {
title: 'Owner Available',
inputTitle: 'Owner',
description:
'GitLab namespace where this repository will belong to. It can be the name of organization, group, subgroup, user, or the project.',
},
},
repoUrlPickerHost: {
host: {
title: 'Host',
description: 'The host where the repository will be created',
},
},
repoUrlPickerRepoName: {
repository: {
title: 'Repositories Available',
inputTitle: 'Repository',
description: 'The name of the repository',
},
},
},
listTaskPage: {
title: 'List template tasks',
pageTitle: 'Templates Tasks',
subtitle: 'All tasks that have been started',
content: {
emptyState: {
title: 'No information to display',
description:
'There are no tasks or there was an issue communicating with backend.',
},
tableTitle: 'Tasks',
tableCell: {
taskID: 'Task ID',
template: 'Template',
created: 'Created',
owner: 'Owner',
status: 'Status',
},
},
},
ownerListPicker: {
title: 'Task Owner',
options: {
owned: 'Owned',
all: 'All',
},
},
ongoingTask: {
title: 'Run of',
pageTitle: {
hasTemplateName: 'Run of {{templateName}}',
noTemplateName: 'Scaffolder Run',
},
subtitle: 'Task {{taskId}}',
cancelButtonTitle: 'Cancel',
startOverButtonTitle: 'Start Over',
hideLogsButtonTitle: 'Hide Logs',
showLogsButtonTitle: 'Show Logs',
contextMenu: {
hideLogs: 'Hide Logs',
showLogs: 'Show Logs',
hideButtonBar: 'Hide Button Bar',
showButtonBar: 'Show Button Bar',
startOver: 'Start Over',
cancel: 'Cancel',
},
},
templateTypePicker: {
title: 'Categories',
},
templateEditorPage: {
title: 'Template Editor',
subTitle: 'Edit, preview, and try out templates and template forms',
dryRunResults: {
title: 'Dry-run results',
},
dryRunResultsList: {
title: 'Result {{resultId}}',
downloadButtonTitle: 'Download as .zip',
deleteButtonTitle: 'Delete result',
},
dryRunResultsView: {
tab: {
files: 'Files',
log: 'Log',
output: 'Output',
},
},
taskStatusStepper: {
skippedStepTitle: 'Skipped',
},
customFieldExplorer: {
selectFieldLabel: 'Choose Custom Field Extension',
fieldForm: {
title: 'Field Options',
applyButtonTitle: 'Apply',
},
preview: {
title: 'Example Template Spec',
},
},
templateEditorBrowser: {
closeConfirmMessage: 'Are you sure? Unsaved changes will be lost',
saveIconTooltip: 'Save all files',
reloadIconTooltip: 'Reload directory',
closeIconTooltip: 'Close directory',
},
templateEditorIntro: {
title: 'Get started by choosing one of the options below',
loadLocal: {
title: 'Load Template Directory',
description:
'Load a local template directory, allowing you to both edit and try executing your own template.',
unSupportsLoadTooltip:
'Only supported in some Chromium-based browsers',
},
formEditor: {
title: 'Edit Template Form',
description:
'Preview and edit a template form, either using a sample template or by loading a template from the catalog.',
},
fieldExplorer: {
title: 'Custom Field Explorer',
description:
'View and play around with available installed custom field extensions.',
},
},
templateEditorTextArea: {
saveIconTooltip: 'Save file',
refreshIconTooltip: 'Reload file',
},
templateFormPreviewer: {
title: 'Load Existing Template',
},
},
templateListPage: {
title: 'Create a new component',
subtitle:
'Create new software components using standard templates in your organization',
pageTitle: 'Create a new component',
templateGroups: {
defaultTitle: 'Templates',
otherTitle: 'Other Templates',
},
contentHeader: {
title: 'Available Templates',
registerExistingButtonTitle: 'Register Existing Component',
supportButtonTitle:
'Create new software components using standard templates. Different templates create different kinds of components (services, websites, documentation, ...).',
},
additionalLinksForEntity: {
viewTechDocsTitle: 'View TechDocs',
},
},
templateWizardPage: {
title: 'Create a new component',
subtitle:
'Create new software components using standard templates in your organization',
pageTitle: 'Create a new component',
pageContextMenu: {
editConfigurationTitle: 'Edit Configuration',
},
},
},
});