needs mocking to render in jsdom
+jest.mock('react-virtualized-auto-sizer', () => ({
+ __esModule: true,
+ default: (props: {
+ children: (size: { width: number; height: number }) => ReactNode;
+ }) => <>{props.children({ width: 400, height: 200 })}>,
+}));
+
+describe('TaskLogStream', () => {
+ it('should render a log stream with the correct log lines', async () => {
+ const logs = { step: ['line 1', 'line 2'], step2: ['line 3'] };
+
+ const { findByText } = await renderInTestApp();
+
+ const logLines = Object.values(logs).flat();
+
+ for (const line of logLines) {
+ await expect(findByText(line)).resolves.toBeInTheDocument();
+ }
+ });
+
+ it('should filter out empty log lines', async () => {
+ const logs = { step: ['line 1', 'line 2'], step2: [] };
+
+ const { findAllByRole } = await renderInTestApp(
+ ,
+ );
+
+ await expect(findAllByRole('row')).resolves.toHaveLength(2);
+ });
+});
diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx
new file mode 100644
index 0000000000..00e8452b42
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 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 React from 'react';
+import { LogViewer } from '@backstage/core-components';
+import { makeStyles } from '@material-ui/core/styles';
+
+const useStyles = makeStyles({
+ root: {
+ width: '100%',
+ height: '100%',
+ position: 'relative',
+ },
+});
+
+export const TaskLogStream = (props: { logs: { [k: string]: string[] } }) => {
+ const styles = useStyles();
+ return (
+
+ l.join('\n'))
+ .filter(Boolean)
+ .join('\n')}
+ />
+
+ );
+};
diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx
new file mode 100644
index 0000000000..cecf682596
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 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 React from 'react';
+
+import { BackstageTheme } from '@backstage/theme';
+import { CircularProgress, makeStyles, StepIconProps } from '@material-ui/core';
+import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline';
+import PanoramaFishEyeIcon from '@material-ui/icons/PanoramaFishEye';
+import classNames from 'classnames';
+import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
+import ErrorOutline from '@material-ui/icons/ErrorOutline';
+
+const useStepIconStyles = makeStyles((theme: BackstageTheme) => ({
+ root: {
+ color: theme.palette.text.disabled,
+ },
+ completed: {
+ color: theme.palette.status.ok,
+ },
+ error: {
+ color: theme.palette.status.error,
+ },
+}));
+
+export const StepIcon = (props: StepIconProps & { skipped: boolean }) => {
+ const classes = useStepIconStyles();
+ const { active, completed, error, skipped } = props;
+
+ const getMiddle = () => {
+ if (active) {
+ return ;
+ }
+ if (completed) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
+ if (skipped) {
+ return ;
+ }
+
+ return ;
+ };
+
+ return (
+
+ {getMiddle()}
+
+ );
+};
diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.test.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.test.tsx
new file mode 100644
index 0000000000..185d2fa7fb
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 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 React from 'react';
+import { StepTime } from './StepTime';
+import { render, act } from '@testing-library/react';
+
+describe('StepTime', () => {
+ it('should return empty when there is no start time', () => {
+ const step = {
+ id: 'test',
+ startedAt: undefined,
+ endedAt: undefined,
+ name: 'test',
+ };
+
+ const { container } = render();
+
+ expect(container.querySelector('span')?.textContent).toBe('');
+ });
+
+ it('should format the time between the start and end date properly', async () => {
+ const step = {
+ id: 'test',
+ startedAt: '2021-01-01T00:00:00Z',
+ endedAt: '2021-01-01T00:00:01Z',
+ name: 'test',
+ };
+
+ const { findByText } = render();
+
+ await expect(findByText('1 second')).resolves.toBeInTheDocument();
+ });
+
+ describe('updates', () => {
+ beforeEach(() => {
+ jest.useFakeTimers({ now: new Date('2021-01-01T00:00:00Z') });
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('should update the time every second', async () => {
+ const step = {
+ id: 'test',
+ startedAt: '2021-01-01T00:00:00Z',
+ endedAt: undefined,
+ name: 'test',
+ };
+
+ const { findByText } = render();
+
+ await expect(findByText('0 seconds')).resolves.toBeInTheDocument();
+
+ act(() => jest.advanceTimersByTime(1000));
+
+ await expect(findByText('1 second')).resolves.toBeInTheDocument();
+
+ act(() => jest.advanceTimersByTime(1000 * 60));
+
+ await expect(
+ findByText('1 minute, 1 second'),
+ ).resolves.toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx
new file mode 100644
index 0000000000..653fe6e942
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 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 React, { useCallback, useState } from 'react';
+import useInterval from 'react-use/lib/useInterval';
+import { DateTime, Interval } from 'luxon';
+import humanizeDuration from 'humanize-duration';
+import { Typography } from '@material-ui/core';
+import { useMountEffect } from '@react-hookz/web';
+
+export const StepTime = (props: {
+ step: {
+ id: string;
+ name: string;
+ startedAt?: string;
+ endedAt?: string;
+ };
+}) => {
+ const [time, setTime] = useState('');
+ const { step } = props;
+
+ const calculate = useCallback(() => {
+ if (!step.startedAt) {
+ setTime('');
+ return;
+ }
+
+ const end = step.endedAt
+ ? DateTime.fromISO(step.endedAt)
+ : DateTime.local();
+
+ const startedAt = DateTime.fromISO(step.startedAt);
+ const formatted = Interval.fromDateTimes(startedAt, end)
+ .toDuration()
+ .valueOf();
+
+ setTime(humanizeDuration(formatted, { round: true }));
+ }, [step.endedAt, step.startedAt]);
+
+ useMountEffect(() => calculate());
+
+ useInterval(() => !step.endedAt && calculate(), 1000);
+
+ return {time};
+};
diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.test.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.test.tsx
new file mode 100644
index 0000000000..afd1f58190
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 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 React from 'react';
+import { TaskSteps } from './TaskSteps';
+import { ScaffolderTaskStatus } from '@backstage/plugin-scaffolder-react';
+import { renderInTestApp } from '@backstage/test-utils';
+
+describe('TaskSteps', () => {
+ it('should render each of the steps', async () => {
+ const steps = [
+ {
+ id: '1',
+ name: 'Step 1',
+ status: 'processing' as ScaffolderTaskStatus,
+ startedAt: Date.now().toLocaleString(),
+ action: 'create',
+ },
+ {
+ id: '2',
+ name: 'Step 2',
+ status: 'failed' as ScaffolderTaskStatus,
+
+ startedAt: Date.now().toLocaleString(),
+ endedAt: Date.now().toLocaleString(),
+ action: 'create',
+ },
+ {
+ id: '3',
+ name: 'Step 3',
+ status: 'completed' as ScaffolderTaskStatus,
+ startedAt: Date.now().toLocaleString(),
+ endedAt: Date.now().toLocaleString(),
+ action: 'create',
+ },
+ {
+ id: '4',
+ name: 'Step 4',
+ status: 'skipped' as ScaffolderTaskStatus,
+ startedAt: Date.now().toLocaleString(),
+ endedAt: Date.now().toLocaleString(),
+ action: 'create',
+ },
+ ];
+
+ const { getByText } = await renderInTestApp();
+
+ for (const step of steps) {
+ expect(getByText(step.name)).toBeInTheDocument();
+ }
+ });
+});
diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx
new file mode 100644
index 0000000000..7cf5ed71bf
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 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 React from 'react';
+import {
+ Stepper as MuiStepper,
+ Step as MuiStep,
+ StepButton as MuiStepButton,
+ StepLabel as MuiStepLabel,
+ StepIconProps,
+ Box,
+} from '@material-ui/core';
+import { TaskStep } from '@backstage/plugin-scaffolder-common';
+import { Step } from '../../../components/hooks/useEventStream';
+import { StepIcon } from './StepIcon';
+import { StepTime } from './StepTime';
+
+interface StepperProps {
+ steps: (TaskStep & Step)[];
+ activeStep?: number;
+}
+
+export const TaskSteps = (props: StepperProps) => {
+ return (
+
+ {props.steps.map((step, index) => {
+ const isCompleted = step.status === 'completed';
+ const isFailed = step.status === 'failed';
+ const isActive = step.status === 'processing';
+ const isSkipped = step.status === 'skipped';
+ const stepIconProps: Partial = {
+ completed: isCompleted,
+ error: isFailed,
+ active: isActive,
+ skipped: isSkipped,
+ };
+
+ return (
+
+
+
+ {step.name}
+
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/index.ts b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/index.ts
new file mode 100644
index 0000000000..28724ba512
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023 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.
+ */
+export { TaskSteps } from './TaskSteps';
diff --git a/plugins/scaffolder/src/next/OngoingTask/index.ts b/plugins/scaffolder/src/next/OngoingTask/index.ts
new file mode 100644
index 0000000000..f0c7e2a6cc
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023 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.
+ */
+export { OngoingTask } from './OngoingTask';
diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx
index 8dd47b478e..049579e16c 100644
--- a/plugins/scaffolder/src/next/Router/Router.tsx
+++ b/plugins/scaffolder/src/next/Router/Router.tsx
@@ -19,6 +19,7 @@ import { TemplateListPage } from '../TemplateListPage';
import { TemplateWizardPage } from '../TemplateWizardPage';
import {
NextFieldExtensionOptions,
+ ScaffolderTaskOutput,
SecretsContextProvider,
useCustomFieldExtensions,
useCustomLayouts,
@@ -28,7 +29,17 @@ import {
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups';
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default';
-import { nextSelectedTemplateRouteRef } from '../routes';
+
+import {
+ nextActionsRouteRef,
+ nextScaffolderListTaskRouteRef,
+ nextScaffolderTaskRouteRef,
+ nextSelectedTemplateRouteRef,
+} from '../routes';
+import { ErrorPage } from '@backstage/core-components';
+import { OngoingTask } from '../OngoingTask';
+import { ActionsPage } from '../../components/ActionsPage';
+import { ListTasksPage } from '../../components/ListTasksPage';
/**
* The Props for the Scaffolder Router
@@ -41,9 +52,21 @@ export type NextRouterProps = {
template: TemplateEntityV1beta3;
}>;
TaskPageComponent?: React.ComponentType<{}>;
+ TemplateOutputsComponent?: React.ComponentType<{
+ output?: ScaffolderTaskOutput;
+ }>;
};
groups?: TemplateGroupFilter[];
+ // todo(blam): rename this to formProps
FormProps?: FormProps;
+ contextMenu?: {
+ /** Whether to show a link to the template editor */
+ editor?: boolean;
+ /** Whether to show a link to the actions documentation */
+ actions?: boolean;
+ /** Whether to show a link to the tasks page */
+ tasks?: boolean;
+ };
};
/**
@@ -52,10 +75,17 @@ export type NextRouterProps = {
* @alpha
*/
export const Router = (props: PropsWithChildren) => {
- const { components: { TemplateCardComponent } = {} } = props;
+ const {
+ components: {
+ TemplateCardComponent,
+ TemplateOutputsComponent,
+ TaskPageComponent = OngoingTask,
+ } = {},
+ } = props;
const outlet = useOutlet() || props.children;
const customFieldExtensions =
useCustomFieldExtensions(outlet);
+
const fieldExtensions = [
...customFieldExtensions,
...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter(
@@ -75,6 +105,7 @@ export const Router = (props: PropsWithChildren) => {
element={
}
@@ -91,6 +122,23 @@ export const Router = (props: PropsWithChildren) => {
}
/>
+
+ }
+ />
+ } />
+ }
+ />
+ }
+ />
);
};
diff --git a/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx b/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx
new file mode 100644
index 0000000000..3bca46af2a
--- /dev/null
+++ b/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2020 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 { useRouteRef } from '@backstage/core-plugin-api';
+import IconButton from '@material-ui/core/IconButton';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+import MenuItem from '@material-ui/core/MenuItem';
+import MenuList from '@material-ui/core/MenuList';
+import Popover from '@material-ui/core/Popover';
+import { makeStyles } from '@material-ui/core/styles';
+import Description from '@material-ui/icons/Description';
+import Edit from '@material-ui/icons/Edit';
+import List from '@material-ui/icons/List';
+import MoreVert from '@material-ui/icons/MoreVert';
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ nextActionsRouteRef,
+ nextEditRouteRef,
+ nextScaffolderListTaskRouteRef,
+} from '../routes';
+
+const useStyles = makeStyles({
+ button: {
+ color: 'white',
+ },
+});
+
+export type ScaffolderPageContextMenuProps = {
+ editor?: boolean;
+ actions?: boolean;
+ tasks?: boolean;
+};
+
+export function ContextMenu(props: ScaffolderPageContextMenuProps) {
+ const classes = useStyles();
+ const [anchorEl, setAnchorEl] = useState();
+ const editLink = useRouteRef(nextEditRouteRef);
+ const actionsLink = useRouteRef(nextActionsRouteRef);
+ const tasksLink = useRouteRef(nextScaffolderListTaskRouteRef);
+
+ const navigate = useNavigate();
+
+ const showEditor = props.editor !== false;
+ const showActions = props.actions !== false;
+ const showTasks = props.tasks !== false;
+
+ if (!showEditor && !showActions) {
+ return null;
+ }
+
+ const onOpen = (event: React.SyntheticEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const onClose = () => {
+ setAnchorEl(undefined);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {showEditor && (
+
+ )}
+ {showActions && (
+
+ )}
+ {showTasks && (
+
+ )}
+
+
+ >
+ );
+}
diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx
index 4eae84b866..b701ceb3c4 100644
--- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx
+++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx
@@ -37,12 +37,18 @@ import { RegisterExistingButton } from './RegisterExistingButton';
import { useRouteRef } from '@backstage/core-plugin-api';
import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups';
import { registerComponentRouteRef } from '../../routes';
+import { ContextMenu } from './ContextMenu';
export type TemplateListPageProps = {
TemplateCardComponent?: React.ComponentType<{
template: TemplateEntityV1beta3;
}>;
groups?: TemplateGroupFilter[];
+ contextMenu?: {
+ editor?: boolean;
+ actions?: boolean;
+ tasks?: boolean;
+ };
};
const defaultGroup: TemplateGroupFilter = {
@@ -61,7 +67,9 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
pageTitleOverride="Create a new component"
title="Create a new component"
subtitle="Create new software components using standard templates in your organization"
- />
+ >
+
+
[];
@@ -43,12 +48,12 @@ export type TemplateWizardPageProps = {
export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
const rootRef = useRouteRef(nextRouteRef);
- const taskRoute = useRouteRef(scaffolderTaskRouteRef);
+ const taskRoute = useRouteRef(nextScaffolderTaskRouteRef);
const { secrets } = useTemplateSecrets();
const scaffolderApi = useApi(scaffolderApiRef);
const navigate = useNavigate();
const { templateName, namespace } = useRouteRefParams(
- selectedTemplateRouteRef,
+ nextSelectedTemplateRouteRef,
);
const templateRef = stringifyEntityRef({
diff --git a/plugins/scaffolder/src/next/routes.ts b/plugins/scaffolder/src/next/routes.ts
index 620122ee60..12fad4210a 100644
--- a/plugins/scaffolder/src/next/routes.ts
+++ b/plugins/scaffolder/src/next/routes.ts
@@ -34,3 +34,24 @@ export const nextScaffolderTaskRouteRef = createSubRouteRef({
parent: nextRouteRef,
path: '/tasks/:taskId',
});
+
+/** @alpha */
+export const nextScaffolderListTaskRouteRef = createSubRouteRef({
+ id: 'scaffolder/next/list-tasks',
+ parent: nextRouteRef,
+ path: '/tasks',
+});
+
+/** @alpha */
+export const nextActionsRouteRef = createSubRouteRef({
+ id: 'scaffolder/next/actions',
+ parent: nextRouteRef,
+ path: '/actions',
+});
+
+/** @alpha */
+export const nextEditRouteRef = createSubRouteRef({
+ id: 'scaffolder/next/edit',
+ parent: nextRouteRef,
+ path: '/edit',
+});