From ab2c12fa1dad1fcf647cd8c4028f84b38e023c0f Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 16 Jan 2023 17:02:15 +0100 Subject: [PATCH 01/26] chore: moved across the work from previous branch Signed-off-by: blam --- .../src/components/hooks/useEventStream.ts | 2 +- plugins/scaffolder/src/next/Router/Router.tsx | 14 ++++- .../scaffolder/src/next/TaskPage/TaskPage.tsx | 63 +++++++++++++++++++ .../src/next/TaskPage/TaskSteps/TaskSteps.tsx | 63 +++++++++++++++++++ .../src/next/TaskPage/TaskSteps/index.ts | 16 +++++ plugins/scaffolder/src/next/TaskPage/index.ts | 16 +++++ .../TemplateWizardPage/TemplateWizardPage.tsx | 17 +++-- 7 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 plugins/scaffolder/src/next/TaskPage/TaskPage.tsx create mode 100644 plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx create mode 100644 plugins/scaffolder/src/next/TaskPage/TaskSteps/index.ts create mode 100644 plugins/scaffolder/src/next/TaskPage/index.ts diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder/src/components/hooks/useEventStream.ts index 0b36c85ad8..be0ff922a4 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder/src/components/hooks/useEventStream.ts @@ -25,7 +25,7 @@ import { import { useApi } from '@backstage/core-plugin-api'; import { Subscription } from '@backstage/types'; -type Step = { +export type Step = { id: string; status: ScaffolderTaskStatus; endedAt?: string; diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index 8dd47b478e..f5e186eb67 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -28,7 +28,14 @@ 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 { FormProps } from '../types'; +import { + nextScaffolderTaskRouteRef, + nextSelectedTemplateRouteRef, +} from '../routes'; +import { ErrorPage } from '@backstage/core-components'; +import { TaskPage } from '../TaskPage'; /** * The Props for the Scaffolder Router @@ -91,6 +98,11 @@ export const Router = (props: PropsWithChildren) => { } /> + } /> + } + /> ); }; diff --git a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx new file mode 100644 index 0000000000..f259231e4d --- /dev/null +++ b/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx @@ -0,0 +1,63 @@ +/* + * 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, { useMemo } from 'react'; +import { Page, Header, Content } from '@backstage/core-components'; +import { useTaskEventStream } from '../../components/hooks/useEventStream'; +import { useParams } from 'react-router-dom'; +import { Box, LinearProgress, Paper } from '@material-ui/core'; +import { TaskSteps } from './TaskSteps'; + +export const TaskPage = () => { + const { taskId } = useParams(); + // check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. + const taskStream = useTaskEventStream(taskId!); + const steps = useMemo( + () => + taskStream.task?.spec.steps.map(step => ({ + ...step, + ...taskStream?.steps?.[step.id], + })) ?? [], + [taskStream], + ); + + const activeStep = React.useMemo(() => { + for (let i = steps.length - 1; i >= 0; i--) { + if (steps[i].status !== 'open') { + return i; + } + } + + return 0; + }, [steps]); + + return ( + +
+ + + + + + + + + + ); +}; diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx b/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx new file mode 100644 index 0000000000..fb24a8a832 --- /dev/null +++ b/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx @@ -0,0 +1,63 @@ +/* + * 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, +} from '@material-ui/core'; +import { TaskStep } from '@backstage/plugin-scaffolder-common'; +import { Step } from '../../../components/hooks/useEventStream'; + +interface StepperProps { + steps: (TaskStep & Step)[]; + activeStep?: number; + setActiveStep?: (step: number) => void; +} + +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'; + + return ( + + + + {step.name} + + + + ); + })} + + ); +}; diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/index.ts b/plugins/scaffolder/src/next/TaskPage/TaskSteps/index.ts new file mode 100644 index 0000000000..28724ba512 --- /dev/null +++ b/plugins/scaffolder/src/next/TaskPage/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/TaskPage/index.ts b/plugins/scaffolder/src/next/TaskPage/index.ts new file mode 100644 index 0000000000..9b6e502fed --- /dev/null +++ b/plugins/scaffolder/src/next/TaskPage/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 { TaskPage } from './TaskPage'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index d24482a583..b71d0bd7a3 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -28,12 +28,17 @@ import { Workflow, type LayoutOptions, } from '@backstage/plugin-scaffolder-react'; -import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; +import { + NextFieldExtensionOptions, + FormProps, +} from '@backstage/plugin-scaffolder-react'; import { JsonValue } from '@backstage/types'; -import { type FormProps } from '../types'; -import { nextRouteRef } from '../routes'; -import { scaffolderTaskRouteRef, selectedTemplateRouteRef } from '../../routes'; import { Header, Page } from '@backstage/core-components'; +import { + nextRouteRef, + nextScaffolderTaskRouteRef, + nextSelectedTemplateRouteRef, +} from '../routes'; export type TemplateWizardPageProps = { customFieldExtensions: NextFieldExtensionOptions[]; @@ -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({ From 29feff150ed9b13d6d94e8c0e4358ff83a0e7aa2 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 17 Jan 2023 15:49:02 +0100 Subject: [PATCH 02/26] chore: added some more things for enabling testing Signed-off-by: blam --- app-config.yaml | 6 +++ packages/backend/src/plugins/scaffolder.ts | 28 +++++++++++++- packages/backend/test-template.yaml | 37 +++++++++++++++++++ .../default-app/app-config.local.yaml | 4 ++ .../scaffolder/src/next/TaskPage/TaskPage.tsx | 4 +- .../src/next/TaskPage/TaskSteps/TaskSteps.tsx | 19 ++++++---- 6 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 packages/backend/test-template.yaml diff --git a/app-config.yaml b/app-config.yaml index dd72051189..2eed4762ce 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -255,6 +255,12 @@ catalog: # Backstage example entities - type: file target: ../catalog-model/examples/all.yaml + + - type: file + target: ./test-template.yaml + rules: + - allow: [Template] + # Backstage example groups and users - type: file target: ../catalog-model/examples/acme-corp.yaml diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index d079b64c28..b6275e9883 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -15,7 +15,12 @@ */ import { CatalogClient } from '@backstage/catalog-client'; -import { createRouter } from '@backstage/plugin-scaffolder-backend'; +import { ScmIntegrations } from '@backstage/integration'; +import { + createRouter, + createTemplateAction, + createBuiltinActions, +} from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; @@ -26,6 +31,26 @@ export default async function createPlugin( discoveryApi: env.discovery, }); + const defaultActions = createBuiltinActions({ + catalogClient, + reader: env.reader, + config: env.config, + integrations: ScmIntegrations.fromConfig(env.config), + }); + + const delayAction = createTemplateAction({ + id: 'mock:delay', + async handler(ctx) { + const interval = setInterval( + () => ctx.logger.info('Writing something', new Date().toISOString()), + 1000, + ); + + await new Promise(resolve => setTimeout(resolve, 5000)); + clearTimeout(interval); + }, + }); + return await createRouter({ logger: env.logger, config: env.config, @@ -34,5 +59,6 @@ export default async function createPlugin( reader: env.reader, identity: env.identity, scheduler: env.scheduler, + actions: [...defaultActions, delayAction], }); } diff --git a/packages/backend/test-template.yaml b/packages/backend/test-template.yaml new file mode 100644 index 0000000000..13a002e0b9 --- /dev/null +++ b/packages/backend/test-template.yaml @@ -0,0 +1,37 @@ +apiVersion: scaffolder.backstage.io/v1beta3 +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template +kind: Template +metadata: + name: example-delay-tempalte + title: Delay template + description: An example template for the scaffolder that creates a simple Node.js service +spec: + type: service + + parameters: [] + + steps: + - id: delay1 + name: Delay 1 + action: mock:delay + + - id: delay2 + name: Delay 2 + action: mock:delay + + - id: delay3 + name: Delay 3 + action: mock:delay + + - id: delay4 + name: Delay 4 + action: mock:delay + + - id: delay5 + if: true + name: Delay 5 + action: mock:delay + + - id: delay6 + name: Delay 6 + action: mock:delay diff --git a/packages/create-app/templates/default-app/app-config.local.yaml b/packages/create-app/templates/default-app/app-config.local.yaml index 976293b546..6b4dcd0327 100644 --- a/packages/create-app/templates/default-app/app-config.local.yaml +++ b/packages/create-app/templates/default-app/app-config.local.yaml @@ -1 +1,5 @@ # Backstage override configuration for your local development environment +catalog: + locations: + - type: file + target: ./test-template.yaml diff --git a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx index f259231e4d..f9a84d2355 100644 --- a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx @@ -52,7 +52,9 @@ export const TaskPage = () => { /> - + {!taskStream.completed && ( + + )} diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx b/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx index fb24a8a832..4095872465 100644 --- a/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx +++ b/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx @@ -19,9 +19,11 @@ import { Step as MuiStep, StepButton as MuiStepButton, StepLabel as MuiStepLabel, + StepIconProps, } from '@material-ui/core'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { Step } from '../../../components/hooks/useEventStream'; +import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline'; interface StepperProps { steps: (TaskStep & Step)[]; @@ -41,17 +43,20 @@ export const TaskSteps = (props: StepperProps) => { const isFailed = step.status === 'failed'; const isActive = step.status === 'processing'; const isSkipped = step.status === 'skipped'; + const stepIconProps: Partial = { + completed: isCompleted, + error: isFailed, + active: isActive, + }; + + if (isSkipped) { + stepIconProps.icon = ; + } return ( - + {step.name} From 07c675f7ebd4fe3b72a808ff6cfd35c3f4b15af6 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 25 Jan 2023 10:14:48 +0100 Subject: [PATCH 03/26] chore: added times to the dropdown box Signed-off-by: blam --- plugins/scaffolder/src/next/Router/Router.tsx | 1 - .../scaffolder/src/next/TaskPage/TaskPage.tsx | 5 +- .../src/next/TaskPage/TaskSteps/StepIcon.tsx | 71 +++++++++++++++++++ .../src/next/TaskPage/TaskSteps/StepTime.tsx | 53 ++++++++++++++ .../src/next/TaskPage/TaskSteps/TaskSteps.tsx | 17 +++-- 5 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 plugins/scaffolder/src/next/TaskPage/TaskSteps/StepIcon.tsx create mode 100644 plugins/scaffolder/src/next/TaskPage/TaskSteps/StepTime.tsx diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index f5e186eb67..8de9ac57e4 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -29,7 +29,6 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default'; -import { FormProps } from '../types'; import { nextScaffolderTaskRouteRef, nextSelectedTemplateRouteRef, diff --git a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx index f9a84d2355..138c6ac24e 100644 --- a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx @@ -52,11 +52,10 @@ export const TaskPage = () => { /> - {!taskStream.completed && ( - - )} + {!taskStream.completed && } + {/* */} diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/StepIcon.tsx b/plugins/scaffolder/src/next/TaskPage/TaskSteps/StepIcon.tsx new file mode 100644 index 0000000000..39ef37c771 --- /dev/null +++ b/plugins/scaffolder/src/next/TaskPage/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 FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; +import classNames from 'classnames'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import DeleteOutline from '@material-ui/icons/DeleteOutline'; + +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/TaskPage/TaskSteps/StepTime.tsx b/plugins/scaffolder/src/next/TaskPage/TaskSteps/StepTime.tsx new file mode 100644 index 0000000000..f2d3116854 --- /dev/null +++ b/plugins/scaffolder/src/next/TaskPage/TaskSteps/StepTime.tsx @@ -0,0 +1,53 @@ +/* + * 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, { 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'; + +export const StepTime = ({ + step, +}: { + step: { + id: string; + name: string; + startedAt?: string; + endedAt?: string; + }; +}) => { + const [time, setTime] = useState(''); + + useInterval(() => { + 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 })); + }, 1000); + + return {time}; +}; diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx b/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx index 4095872465..40f9f9c189 100644 --- a/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx +++ b/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx @@ -23,7 +23,8 @@ import { } from '@material-ui/core'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { Step } from '../../../components/hooks/useEventStream'; -import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline'; +import { StepIcon } from './StepIcon'; +import { StepTime } from './StepTime'; interface StepperProps { steps: (TaskStep & Step)[]; @@ -43,21 +44,23 @@ export const TaskSteps = (props: StepperProps) => { const isFailed = step.status === 'failed'; const isActive = step.status === 'processing'; const isSkipped = step.status === 'skipped'; - const stepIconProps: Partial = { + const stepIconProps: Partial = { completed: isCompleted, error: isFailed, active: isActive, + skipped: isSkipped, }; - if (isSkipped) { - stepIconProps.icon = ; - } - return ( - + {step.name} +
+
From 66948f1a99dd676ad0f602de66b9f96e8f1308e2 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 25 Jan 2023 14:19:50 +0100 Subject: [PATCH 04/26] chore: make a little clearer when a task has failed Signed-off-by: blam --- .../src/next/TaskPage/TaskBorder.tsx | 51 +++++++++++++++++++ .../scaffolder/src/next/TaskPage/TaskPage.tsx | 8 ++- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 plugins/scaffolder/src/next/TaskPage/TaskBorder.tsx diff --git a/plugins/scaffolder/src/next/TaskPage/TaskBorder.tsx b/plugins/scaffolder/src/next/TaskPage/TaskBorder.tsx new file mode 100644 index 0000000000..1bf09bf207 --- /dev/null +++ b/plugins/scaffolder/src/next/TaskPage/TaskBorder.tsx @@ -0,0 +1,51 @@ +/* + * 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 { LinearProgress, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + failed: { + backgroundColor: theme.palette.error.main, + }, + success: { + backgroundColor: theme.palette.success.main, + }, +})); + +export const TaskBorder = ({ + isComplete, + isError, +}: { + isComplete: boolean; + isError: boolean; +}) => { + const styles = useStyles(); + + if (!isComplete) { + return ; + } + + if (isError) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx index 138c6ac24e..87d5d35453 100644 --- a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx @@ -17,8 +17,9 @@ import React, { useMemo } from 'react'; import { Page, Header, Content } from '@backstage/core-components'; import { useTaskEventStream } from '../../components/hooks/useEventStream'; import { useParams } from 'react-router-dom'; -import { Box, LinearProgress, Paper } from '@material-ui/core'; +import { Box, Paper } from '@material-ui/core'; import { TaskSteps } from './TaskSteps'; +import { TaskBorder } from './TaskBorder'; export const TaskPage = () => { const { taskId } = useParams(); @@ -52,7 +53,10 @@ export const TaskPage = () => { /> - {!taskStream.completed && } + {/* */} From 2b27e58084815f9a88fea94d19f5bb71e13e0ed7 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 25 Jan 2023 15:29:21 +0100 Subject: [PATCH 05/26] chore: change order of hooks Signed-off-by: blam --- .../scaffolder-react/src/next/components/Stepper/Stepper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 9242ef0086..f802121597 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -127,6 +127,8 @@ export const Stepper = (stepperProps: StepperProps) => { [setFormState], ); + const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); + const handleNext = async ({ formData = {}, }: { @@ -151,8 +153,6 @@ export const Stepper = (stepperProps: StepperProps) => { setFormState(current => ({ ...current, ...formData })); }; - const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); - return ( <> From 5cc6278ddc52d8f72ae0cd46f18766484cf4a6cb Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 25 Jan 2023 16:26:01 +0100 Subject: [PATCH 06/26] chore: moving some files around and making them pretty Signed-off-by: blam --- .../src/components/LogViewer/LogLine.tsx | 7 ++-- .../OngoingTask.tsx} | 18 ++++++--- .../{TaskPage => OngoingTask}/TaskBorder.tsx | 0 .../src/next/OngoingTask/TaskLogStream.tsx | 39 +++++++++++++++++++ .../TaskSteps/StepIcon.tsx | 0 .../TaskSteps/StepTime.tsx | 10 +++-- .../TaskSteps/TaskSteps.tsx | 0 .../TaskSteps/index.ts | 0 .../next/{TaskPage => OngoingTask}/index.ts | 2 +- plugins/scaffolder/src/next/Router/Router.tsx | 4 +- 10 files changed, 65 insertions(+), 15 deletions(-) rename plugins/scaffolder/src/next/{TaskPage/TaskPage.tsx => OngoingTask/OngoingTask.tsx} (82%) rename plugins/scaffolder/src/next/{TaskPage => OngoingTask}/TaskBorder.tsx (100%) create mode 100644 plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx rename plugins/scaffolder/src/next/{TaskPage => OngoingTask}/TaskSteps/StepIcon.tsx (100%) rename plugins/scaffolder/src/next/{TaskPage => OngoingTask}/TaskSteps/StepTime.tsx (85%) rename plugins/scaffolder/src/next/{TaskPage => OngoingTask}/TaskSteps/TaskSteps.tsx (100%) rename plugins/scaffolder/src/next/{TaskPage => OngoingTask}/TaskSteps/index.ts (100%) rename plugins/scaffolder/src/next/{TaskPage => OngoingTask}/index.ts (93%) diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 8f3a2ce65c..c8ff5cbceb 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -15,7 +15,6 @@ */ import React, { useMemo } from 'react'; -import Typography from '@material-ui/core/Typography'; import { AnsiChunk, AnsiLine, ChunkModifiers } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import classnames from 'classnames'; @@ -159,8 +158,8 @@ export function LogLine({ const elements = useMemo( () => chunks.map(({ text, modifiers, highlight }, index) => ( - {text} - + )), [chunks, highlightResultIndex, classes], ); diff --git a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx similarity index 82% rename from plugins/scaffolder/src/next/TaskPage/TaskPage.tsx rename to plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx index 87d5d35453..feed582745 100644 --- a/plugins/scaffolder/src/next/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx @@ -20,8 +20,9 @@ import { useParams } from 'react-router-dom'; import { Box, Paper } from '@material-ui/core'; import { TaskSteps } from './TaskSteps'; import { TaskBorder } from './TaskBorder'; +import { TaskLogStream } from './TaskLogStream'; -export const TaskPage = () => { +export const OngoingTask = () => { const { taskId } = useParams(); // check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. const taskStream = useTaskEventStream(taskId!); @@ -44,12 +45,19 @@ export const TaskPage = () => { return 0; }, [steps]); + const templateName = + taskStream.task?.spec.templateInfo?.entity?.metadata.name; + return (
+ Run of {templateName} + + } + subtitle={`Task ${taskId}`} /> @@ -59,7 +67,7 @@ export const TaskPage = () => { /> - {/* */} + diff --git a/plugins/scaffolder/src/next/TaskPage/TaskBorder.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx similarity index 100% rename from plugins/scaffolder/src/next/TaskPage/TaskBorder.tsx rename to plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx new file mode 100644 index 0000000000..0060a9c9be --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx @@ -0,0 +1,39 @@ +/* + * 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: '200px', + position: 'relative', + }, +}); + +export const TaskLogStream = (opts: { logs: { [k: string]: string[] } }) => { + const styles = useStyles(); + return ( +
+ l.join('\n')) + .join('\n')} + /> +
+ ); +}; diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/StepIcon.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx similarity index 100% rename from plugins/scaffolder/src/next/TaskPage/TaskSteps/StepIcon.tsx rename to plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/StepTime.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx similarity index 85% rename from plugins/scaffolder/src/next/TaskPage/TaskSteps/StepTime.tsx rename to plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx index f2d3116854..746007a7d3 100644 --- a/plugins/scaffolder/src/next/TaskPage/TaskSteps/StepTime.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import useInterval from 'react-use/lib/useInterval'; import { DateTime, Interval } from 'luxon'; import humanizeDuration from 'humanize-duration'; @@ -31,7 +31,7 @@ export const StepTime = ({ }) => { const [time, setTime] = useState(''); - useInterval(() => { + const calculate = useCallback(() => { if (!step.startedAt) { setTime(''); return; @@ -47,7 +47,11 @@ export const StepTime = ({ .valueOf(); setTime(humanizeDuration(formatted, { round: true })); - }, 1000); + }, [step.endedAt, step.startedAt]); + + useMemo(() => calculate(), [calculate]); + + useInterval(() => !step.endedAt && calculate(), 1000); return {time}; }; diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx similarity index 100% rename from plugins/scaffolder/src/next/TaskPage/TaskSteps/TaskSteps.tsx rename to plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx diff --git a/plugins/scaffolder/src/next/TaskPage/TaskSteps/index.ts b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/index.ts similarity index 100% rename from plugins/scaffolder/src/next/TaskPage/TaskSteps/index.ts rename to plugins/scaffolder/src/next/OngoingTask/TaskSteps/index.ts diff --git a/plugins/scaffolder/src/next/TaskPage/index.ts b/plugins/scaffolder/src/next/OngoingTask/index.ts similarity index 93% rename from plugins/scaffolder/src/next/TaskPage/index.ts rename to plugins/scaffolder/src/next/OngoingTask/index.ts index 9b6e502fed..f0c7e2a6cc 100644 --- a/plugins/scaffolder/src/next/TaskPage/index.ts +++ b/plugins/scaffolder/src/next/OngoingTask/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { TaskPage } from './TaskPage'; +export { OngoingTask } from './OngoingTask'; diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index 8de9ac57e4..b9c18f9d02 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -34,7 +34,7 @@ import { nextSelectedTemplateRouteRef, } from '../routes'; import { ErrorPage } from '@backstage/core-components'; -import { TaskPage } from '../TaskPage'; +import { OngoingTask } from '../OngoingTask'; /** * The Props for the Scaffolder Router @@ -97,7 +97,7 @@ export const Router = (props: PropsWithChildren) => { } /> - } /> + } /> } From c230ae79201037eea289c61045314f6c854eef65 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 30 Jan 2023 16:04:55 +0100 Subject: [PATCH 07/26] feat: added the ability to tail the logs on the LogViewer Signed-off-by: blam --- .../core-components/src/components/LogViewer/LogViewer.tsx | 4 ++++ .../src/components/LogViewer/RealLogViewer.tsx | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 20530cffaf..f1396eef36 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -39,6 +39,10 @@ export interface LogViewerProps { classes?: { root?: string; }; + /** + * Whether the LogViewer should automatically scroll to the bottom of the log. + */ + tail?: boolean; } /** diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index 59e0bea44b..d211399c69 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -32,6 +32,7 @@ import { useLogViewerSelection } from './useLogViewerSelection'; export interface RealLogViewerProps { text: string; classes?: { root?: string }; + tail?: boolean; } export function RealLogViewer(props: RealLogViewerProps) { @@ -52,6 +53,12 @@ export function RealLogViewer(props: RealLogViewerProps) { } }, [search.resultLine]); + useEffect(() => { + if (props.tail && listRef.current) { + listRef.current.scrollToItem(lines.length - 1, 'center'); + } + }, [lines.length, props.tail]); + useEffect(() => { if (location.hash) { // #line-6 -> 6 From ce32a61d388c29008deeaf20cd5494eb3d6d42d0 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 30 Jan 2023 22:03:36 +0100 Subject: [PATCH 08/26] chore: changeset and fixing tserrors Signed-off-by: blam --- .changeset/nasty-students-double.md | 5 +++++ .../scaffolder/src/next/OngoingTask/TaskBorder.tsx | 12 +++++------- .../src/next/OngoingTask/TaskLogStream.tsx | 4 ++++ 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 .changeset/nasty-students-double.md diff --git a/.changeset/nasty-students-double.md b/.changeset/nasty-students-double.md new file mode 100644 index 0000000000..d552fdf91e --- /dev/null +++ b/.changeset/nasty-students-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added the ability to tail the `LogViewer` logs diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx index 1bf09bf207..ada4db1240 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx @@ -39,13 +39,11 @@ export const TaskBorder = ({ return ; } - if (isError) { - return ( - - ); - } - return ( - + ); }; diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx index 0060a9c9be..d587f47f17 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx @@ -30,8 +30,12 @@ export const TaskLogStream = (opts: { logs: { [k: string]: string[] } }) => { return (
l.join('\n')) + .filter(Boolean) + .join('\n')} />
From c4b6770b67265d48c3e94860fd64347dda669c90 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 Jan 2023 12:53:53 +0100 Subject: [PATCH 09/26] chore: refactor and tidy up the UI slightly Signed-off-by: blam --- .../src/next/OngoingTask/OngoingTask.tsx | 98 ++++++++++++++++--- .../src/next/OngoingTask/TaskLogStream.tsx | 4 +- .../next/OngoingTask/TaskSteps/StepIcon.tsx | 8 +- 3 files changed, 89 insertions(+), 21 deletions(-) diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx index feed582745..68e05f29e2 100644 --- a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx @@ -13,18 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { Page, Header, Content } from '@backstage/core-components'; import { useTaskEventStream } from '../../components/hooks/useEventStream'; -import { useParams } from 'react-router-dom'; -import { Box, Paper } from '@material-ui/core'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Box, Button, Paper } from '@material-ui/core'; import { TaskSteps } from './TaskSteps'; import { TaskBorder } from './TaskBorder'; import { TaskLogStream } from './TaskLogStream'; +import { nextSelectedTemplateRouteRef } from '../routes'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import qs from 'qs'; export const OngoingTask = () => { + // todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. const { taskId } = useParams(); - // check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. + const templateRouteRef = useRouteRef(nextSelectedTemplateRouteRef); + const navigate = useNavigate(); const taskStream = useTaskEventStream(taskId!); const steps = useMemo( () => @@ -35,7 +40,15 @@ export const OngoingTask = () => { [taskStream], ); - const activeStep = React.useMemo(() => { + const [logsVisible, setLogsVisible] = useState(false); + + useEffect(() => { + if (taskStream.error) { + setLogsVisible(true); + } + }, [taskStream.error]); + + const activeStep = useMemo(() => { for (let i = steps.length - 1; i >= 0; i--) { if (steps[i].status !== 'open') { return i; @@ -45,6 +58,30 @@ export const OngoingTask = () => { return 0; }, [steps]); + const startOver = useCallback(() => { + const { namespace, name } = + taskStream.task?.spec.templateInfo?.entity?.metadata ?? {}; + + const formData = taskStream.task?.spec.parameters ?? {}; + + if (!namespace || !name) { + return; + } + + navigate({ + pathname: templateRouteRef({ + namespace, + templateName: name, + }), + search: `?${qs.stringify({ formData: JSON.stringify(formData) })}`, + }); + }, [ + navigate, + taskStream.task?.spec.parameters, + taskStream.task?.spec.templateInfo?.entity?.metadata, + templateRouteRef, + ]); + const templateName = taskStream.task?.spec.templateInfo?.entity?.metadata.name; @@ -60,16 +97,49 @@ export const OngoingTask = () => { subtitle={`Task ${taskId}`} /> - - - - - + + + + + + + + + + + + + + + + + + + {logsVisible ? ( + + + + + + - + ) : null} ); diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx index d587f47f17..65c0022e1d 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx @@ -20,7 +20,7 @@ import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles({ root: { width: '100%', - height: '200px', + height: '100%', position: 'relative', }, }); @@ -32,10 +32,8 @@ export const TaskLogStream = (opts: { logs: { [k: string]: string[] } }) => { 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 index 39ef37c771..cecf682596 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx @@ -18,10 +18,10 @@ 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 FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; +import PanoramaFishEyeIcon from '@material-ui/icons/PanoramaFishEye'; import classNames from 'classnames'; import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; -import DeleteOutline from '@material-ui/icons/DeleteOutline'; +import ErrorOutline from '@material-ui/icons/ErrorOutline'; const useStepIconStyles = makeStyles((theme: BackstageTheme) => ({ root: { @@ -48,14 +48,14 @@ export const StepIcon = (props: StepIconProps & { skipped: boolean }) => { } if (error) { - return ; + return ; } if (skipped) { return ; } - return ; + return ; }; return ( From 25d25778d0534d1a5acbc01ec73d844b402e9c90 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 Jan 2023 13:39:56 +0100 Subject: [PATCH 10/26] chore: added default outputs to the template when there's links Signed-off-by: blam Signed-off-by: blam --- .../src/next/OngoingTask/OngoingTask.tsx | 4 +- .../OngoingTask/Outputs/DefaultOutputs.tsx | 35 ++++++++++ .../next/OngoingTask/Outputs/LinkOutputs.tsx | 70 +++++++++++++++++++ .../src/next/OngoingTask/Outputs/index.ts | 16 +++++ 4 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 plugins/scaffolder/src/next/OngoingTask/Outputs/DefaultOutputs.tsx create mode 100644 plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx create mode 100644 plugins/scaffolder/src/next/OngoingTask/Outputs/index.ts diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx index 68e05f29e2..15669469fe 100644 --- a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx @@ -24,6 +24,7 @@ import { TaskLogStream } from './TaskLogStream'; import { nextSelectedTemplateRouteRef } from '../routes'; import { useRouteRef } from '@backstage/core-plugin-api'; import qs from 'qs'; +import { DefaultOutputs } from './Outputs'; export const OngoingTask = () => { // todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. @@ -108,7 +109,7 @@ export const OngoingTask = () => { - + { - {logsVisible ? ( diff --git a/plugins/scaffolder/src/next/OngoingTask/Outputs/DefaultOutputs.tsx b/plugins/scaffolder/src/next/OngoingTask/Outputs/DefaultOutputs.tsx new file mode 100644 index 0000000000..39bf89e4c5 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/Outputs/DefaultOutputs.tsx @@ -0,0 +1,35 @@ +/* + * 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 { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; +import { Box, Paper } from '@material-ui/core'; +import { LinkOutputs } from './LinkOutputs'; + +export const DefaultOutputs = (props: { output?: ScaffolderTaskOutput }) => { + if (!props.output?.links) { + return null; + } + + return ( + + + + + + + + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx b/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx new file mode 100644 index 0000000000..5d0f9c76fd --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx @@ -0,0 +1,70 @@ +/* + * 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 { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; +import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; +import { Box, Button, makeStyles, Link } from '@material-ui/core'; +import React from 'react'; +import WebIcon from '@material-ui/icons/Web'; +import { parseEntityRef } from '@backstage/catalog-model'; + +const useStyles = makeStyles({ + root: { + '&:hover': { + textDecoration: 'none', + }, + }, +}); + +export const LinkOutputs = (props: { output: ScaffolderTaskOutput }) => { + const { links = [] } = props.output; + const classes = useStyles(); + const app = useApp(); + const entityRoute = useRouteRef(entityRouteRef); + + const iconResolver = (key?: string): IconComponent => + app.getSystemIcon(key!) ?? WebIcon; + + return ( + <> + {links + .filter(({ url, entityRef }) => url || entityRef) + .map(({ url, entityRef, title, icon }) => { + if (entityRef) { + const entityName = parseEntityRef(entityRef); + const target = entityRoute(entityName); + return { title, icon, url: target }; + } + return { title, icon, url: url! }; + }) + .map(({ url, title, icon }, i) => { + const Icon = iconResolver(icon); + return ( + } + > + {title} + + ); + })} + + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/Outputs/index.ts b/plugins/scaffolder/src/next/OngoingTask/Outputs/index.ts new file mode 100644 index 0000000000..647f1eebb2 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/Outputs/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 { DefaultOutputs } from './DefaultOutputs'; From 468cbbf55f3a6ed12f1f3503a4d21faeb8bbf619 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 Jan 2023 13:46:07 +0100 Subject: [PATCH 11/26] chore: reset some files Signed-off-by: blam --- app-config.yaml | 6 --- packages/backend/src/plugins/scaffolder.ts | 28 +------------- packages/backend/test-template.yaml | 37 ------------------- .../default-app/app-config.local.yaml | 4 -- 4 files changed, 1 insertion(+), 74 deletions(-) delete mode 100644 packages/backend/test-template.yaml diff --git a/app-config.yaml b/app-config.yaml index 2eed4762ce..dd72051189 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -255,12 +255,6 @@ catalog: # Backstage example entities - type: file target: ../catalog-model/examples/all.yaml - - - type: file - target: ./test-template.yaml - rules: - - allow: [Template] - # Backstage example groups and users - type: file target: ../catalog-model/examples/acme-corp.yaml diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index b6275e9883..d079b64c28 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -15,12 +15,7 @@ */ import { CatalogClient } from '@backstage/catalog-client'; -import { ScmIntegrations } from '@backstage/integration'; -import { - createRouter, - createTemplateAction, - createBuiltinActions, -} from '@backstage/plugin-scaffolder-backend'; +import { createRouter } from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; @@ -31,26 +26,6 @@ export default async function createPlugin( discoveryApi: env.discovery, }); - const defaultActions = createBuiltinActions({ - catalogClient, - reader: env.reader, - config: env.config, - integrations: ScmIntegrations.fromConfig(env.config), - }); - - const delayAction = createTemplateAction({ - id: 'mock:delay', - async handler(ctx) { - const interval = setInterval( - () => ctx.logger.info('Writing something', new Date().toISOString()), - 1000, - ); - - await new Promise(resolve => setTimeout(resolve, 5000)); - clearTimeout(interval); - }, - }); - return await createRouter({ logger: env.logger, config: env.config, @@ -59,6 +34,5 @@ export default async function createPlugin( reader: env.reader, identity: env.identity, scheduler: env.scheduler, - actions: [...defaultActions, delayAction], }); } diff --git a/packages/backend/test-template.yaml b/packages/backend/test-template.yaml deleted file mode 100644 index 13a002e0b9..0000000000 --- a/packages/backend/test-template.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: scaffolder.backstage.io/v1beta3 -# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template -kind: Template -metadata: - name: example-delay-tempalte - title: Delay template - description: An example template for the scaffolder that creates a simple Node.js service -spec: - type: service - - parameters: [] - - steps: - - id: delay1 - name: Delay 1 - action: mock:delay - - - id: delay2 - name: Delay 2 - action: mock:delay - - - id: delay3 - name: Delay 3 - action: mock:delay - - - id: delay4 - name: Delay 4 - action: mock:delay - - - id: delay5 - if: true - name: Delay 5 - action: mock:delay - - - id: delay6 - name: Delay 6 - action: mock:delay diff --git a/packages/create-app/templates/default-app/app-config.local.yaml b/packages/create-app/templates/default-app/app-config.local.yaml index 6b4dcd0327..976293b546 100644 --- a/packages/create-app/templates/default-app/app-config.local.yaml +++ b/packages/create-app/templates/default-app/app-config.local.yaml @@ -1,5 +1 @@ # Backstage override configuration for your local development environment -catalog: - locations: - - type: file - target: ./test-template.yaml From 2d66fca317d0b2014127853b85b850b9a2f7d718 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 Jan 2023 13:50:28 +0100 Subject: [PATCH 12/26] chore: changelog Signed-off-by: blam --- packages/core-components/api-report.md | 1 + plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 721c208406..f720e0305d 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -693,6 +693,7 @@ export interface LogViewerProps { classes?: { root?: string; }; + tail?: boolean; text: string; } diff --git a/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx b/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx index 5d0f9c76fd..596e91d5ab 100644 --- a/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx @@ -16,7 +16,7 @@ import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; -import { Box, Button, makeStyles, Link } from '@material-ui/core'; +import { Button, makeStyles, Link } from '@material-ui/core'; import React from 'react'; import WebIcon from '@material-ui/icons/Web'; import { parseEntityRef } from '@backstage/catalog-model'; From b46f385effdf4928df5968e95bbc2077e7f6d3a0 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 Jan 2023 13:51:26 +0100 Subject: [PATCH 13/26] chore: add changeset Signed-off-by: blam --- .changeset/eleven-shoes-crash.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/eleven-shoes-crash.md diff --git a/.changeset/eleven-shoes-crash.md b/.changeset/eleven-shoes-crash.md new file mode 100644 index 0000000000..8013f311d5 --- /dev/null +++ b/.changeset/eleven-shoes-crash.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +scaffolder/next: Implementing a simple `OngoingTask` page From ce0866d2e94c243ddc4bd0ba8f1e311d55a6d042 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 Jan 2023 16:26:15 +0100 Subject: [PATCH 14/26] chore: writing some tests for the new components Signed-off-by: blam --- .../src/next/OngoingTask/TaskBorder.test.tsx | 40 ++++++++++ .../src/next/OngoingTask/TaskBorder.tsx | 9 +-- .../next/OngoingTask/TaskLogStream.test.tsx | 50 ++++++++++++ .../src/next/OngoingTask/TaskLogStream.tsx | 4 +- .../OngoingTask/TaskSteps/StepTime.test.tsx | 79 +++++++++++++++++++ .../next/OngoingTask/TaskSteps/StepTime.tsx | 5 +- 6 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 plugins/scaffolder/src/next/OngoingTask/TaskBorder.test.tsx create mode 100644 plugins/scaffolder/src/next/OngoingTask/TaskLogStream.test.tsx create mode 100644 plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.test.tsx diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskBorder.test.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.test.tsx new file mode 100644 index 0000000000..f3a59ad344 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.test.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 { TaskBorder } from './TaskBorder'; +import { render } from '@testing-library/react'; + +describe('TaskBorder', () => { + it('should render a pending linear progress if the task is not complete', () => { + const { getByRole } = render( + , + ); + + const progressBar = getByRole('progressbar'); + + expect(progressBar).toBeInTheDocument(); + expect(progressBar).toHaveClass('MuiLinearProgress-indeterminate'); + }); + + it('should render a determinate progress bar if the task is complete', () => { + const { getByRole } = render(); + + const progressBar = getByRole('progressbar'); + + expect(progressBar).toBeInTheDocument(); + expect(progressBar).toHaveClass('MuiLinearProgress-determinate'); + }); +}); diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx index ada4db1240..fdfab17367 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx @@ -26,23 +26,20 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ }, })); -export const TaskBorder = ({ - isComplete, - isError, -}: { +export const TaskBorder = (props: { isComplete: boolean; isError: boolean; }) => { const styles = useStyles(); - if (!isComplete) { + if (!props.isComplete) { return ; } return ( ); diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.test.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.test.tsx new file mode 100644 index 0000000000..e8f9e67e96 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import React, { ReactNode } from 'react'; +import { TaskLogStream } from './TaskLogStream'; + +// The inside 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 index 65c0022e1d..2cfcdf1da6 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx @@ -25,13 +25,13 @@ const useStyles = makeStyles({ }, }); -export const TaskLogStream = (opts: { logs: { [k: string]: string[] } }) => { +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/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 index 746007a7d3..2ec89f385a 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx @@ -19,9 +19,7 @@ import { DateTime, Interval } from 'luxon'; import humanizeDuration from 'humanize-duration'; import { Typography } from '@material-ui/core'; -export const StepTime = ({ - step, -}: { +export const StepTime = (props: { step: { id: string; name: string; @@ -30,6 +28,7 @@ export const StepTime = ({ }; }) => { const [time, setTime] = useState(''); + const { step } = props; const calculate = useCallback(() => { if (!step.startedAt) { From f08364291e9ff6ce0fadbe98a527fd75444b0ad9 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 31 Jan 2023 16:34:00 +0100 Subject: [PATCH 15/26] chore: more test Signed-off-by: blam Signed-off-by: blam --- .../OngoingTask/TaskSteps/TaskSteps.test.tsx | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.test.tsx 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(); + } + }); +}); From ba679976efb0257479386a75fa795d7b5df87ea9 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Feb 2023 15:15:30 +0100 Subject: [PATCH 16/26] chore: use the backstage link component instead and wrap a div instead Signed-off-by: blam --- .../src/next/OngoingTask/Outputs/LinkOutputs.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx b/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx index 596e91d5ab..d7c5ad0b67 100644 --- a/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx @@ -16,10 +16,11 @@ import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; -import { Button, makeStyles, Link } from '@material-ui/core'; +import { Button, makeStyles } from '@material-ui/core'; import React from 'react'; import WebIcon from '@material-ui/icons/Web'; import { parseEntityRef } from '@backstage/catalog-model'; +import { Link } from '@backstage/core-components'; const useStyles = makeStyles({ root: { @@ -53,15 +54,10 @@ export const LinkOutputs = (props: { output: ScaffolderTaskOutput }) => { .map(({ url, title, icon }, i) => { const Icon = iconResolver(icon); return ( - } - > - {title} + + ); })} From cb25785e251a941c21556f9c8163da809c4bece7 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Feb 2023 16:00:08 +0100 Subject: [PATCH 17/26] chore: move the buttons to the contextmenu instead Signed-off-by: blam --- .../src/next/OngoingTask/ContextMenu.tsx | 87 +++++++++++++++++++ .../src/next/OngoingTask/OngoingTask.tsx | 35 +++----- 2 files changed, 98 insertions(+), 24 deletions(-) create mode 100644 plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx diff --git a/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx new file mode 100644 index 0000000000..d4c771a9f8 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx @@ -0,0 +1,87 @@ +/* + * 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 { BackstageTheme } from '@backstage/theme'; +import { + IconButton, + ListItemIcon, + ListItemText, + makeStyles, + MenuItem, + MenuList, + Popover, +} from '@material-ui/core'; +import Retry from '@material-ui/icons/Repeat'; +import Toc from '@material-ui/icons/Toc'; +import MoreVert from '@material-ui/icons/MoreVert'; +import React, { useState } from 'react'; + +type ContextMenuProps = { + logsVisible?: boolean; + onToggleLogs?: (state: boolean) => void; + onStartOver?: () => void; +}; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + button: { + color: theme.palette.common.white, + }, +})); + +export const ContextMenu = (props: ContextMenuProps) => { + const { logsVisible, onToggleLogs, onStartOver } = props; + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(); + + return ( + <> + ) => { + setAnchorEl(event.currentTarget); + }} + data-testid="menu-button" + color="inherit" + className={classes.button} + > + + + setAnchorEl(undefined)} + anchorEl={anchorEl} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + > + + onToggleLogs?.(!logsVisible)}> + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx index 15669469fe..a146780683 100644 --- a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx @@ -25,6 +25,7 @@ import { nextSelectedTemplateRouteRef } from '../routes'; import { useRouteRef } from '@backstage/core-plugin-api'; import qs from 'qs'; import { DefaultOutputs } from './Outputs'; +import { ContextMenu } from './ContextMenu'; export const OngoingTask = () => { // todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. @@ -41,11 +42,11 @@ export const OngoingTask = () => { [taskStream], ); - const [logsVisible, setLogsVisible] = useState(false); + const [logsVisible, setLogVisibleState] = useState(false); useEffect(() => { if (taskStream.error) { - setLogsVisible(true); + setLogVisibleState(true); } }, [taskStream.error]); @@ -96,7 +97,13 @@ export const OngoingTask = () => {
} subtitle={`Task ${taskId}`} - /> + > + +
@@ -110,27 +117,7 @@ export const OngoingTask = () => { - - - - - - - - + {logsVisible ? ( From 7dcd44286df2b62d03b2ca42a7a96dda112148e1 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 21:34:47 +0100 Subject: [PATCH 18/26] chore: removing the tail functionality for now Signed-off-by: blam Signed-off-by: blam --- packages/core-components/api-report.md | 1 - .../core-components/src/components/LogViewer/LogViewer.tsx | 4 ---- .../src/components/LogViewer/RealLogViewer.tsx | 7 ------- 3 files changed, 12 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index f720e0305d..721c208406 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -693,7 +693,6 @@ export interface LogViewerProps { classes?: { root?: string; }; - tail?: boolean; text: string; } diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index f1396eef36..20530cffaf 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -39,10 +39,6 @@ export interface LogViewerProps { classes?: { root?: string; }; - /** - * Whether the LogViewer should automatically scroll to the bottom of the log. - */ - tail?: boolean; } /** diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index d211399c69..59e0bea44b 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -32,7 +32,6 @@ import { useLogViewerSelection } from './useLogViewerSelection'; export interface RealLogViewerProps { text: string; classes?: { root?: string }; - tail?: boolean; } export function RealLogViewer(props: RealLogViewerProps) { @@ -53,12 +52,6 @@ export function RealLogViewer(props: RealLogViewerProps) { } }, [search.resultLine]); - useEffect(() => { - if (props.tail && listRef.current) { - listRef.current.scrollToItem(lines.length - 1, 'center'); - } - }, [lines.length, props.tail]); - useEffect(() => { if (location.hash) { // #line-6 -> 6 From ec83fd4616568f32ff1eecb2e9337fb303fd140f Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 21:35:36 +0100 Subject: [PATCH 19/26] chore: removing changeset Signed-off-by: blam --- .changeset/nasty-students-double.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/nasty-students-double.md diff --git a/.changeset/nasty-students-double.md b/.changeset/nasty-students-double.md deleted file mode 100644 index d552fdf91e..0000000000 --- a/.changeset/nasty-students-double.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Added the ability to tail the `LogViewer` logs From 3887621d5d00aca9aa9319b484428c4f80a7d225 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 21:35:48 +0100 Subject: [PATCH 20/26] chore: added the error panel Signed-off-by: blam --- .../src/next/OngoingTask/OngoingTask.tsx | 24 ++++++++++++++++--- .../src/next/OngoingTask/TaskLogStream.tsx | 1 - .../next/OngoingTask/TaskSteps/TaskSteps.tsx | 5 ++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx index a146780683..e166a96dd7 100644 --- a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ import React, { useEffect, useMemo, useState, useCallback } from 'react'; -import { Page, Header, Content } from '@backstage/core-components'; +import { Page, Header, Content, ErrorPanel } from '@backstage/core-components'; import { useTaskEventStream } from '../../components/hooks/useEventStream'; import { useNavigate, useParams } from 'react-router-dom'; -import { Box, Button, Paper } from '@material-ui/core'; +import { Box, makeStyles, Paper } from '@material-ui/core'; import { TaskSteps } from './TaskSteps'; import { TaskBorder } from './TaskBorder'; import { TaskLogStream } from './TaskLogStream'; @@ -27,12 +27,20 @@ import qs from 'qs'; import { DefaultOutputs } from './Outputs'; import { ContextMenu } from './ContextMenu'; +const useStyles = makeStyles({ + contentWrapper: { + display: 'flex', + flexDirection: 'column', + }, +}); + export const OngoingTask = () => { // todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. const { taskId } = useParams(); const templateRouteRef = useRouteRef(nextSelectedTemplateRouteRef); const navigate = useNavigate(); const taskStream = useTaskEventStream(taskId!); + const classes = useStyles(); const steps = useMemo( () => taskStream.task?.spec.steps.map(step => ({ @@ -104,7 +112,16 @@ export const OngoingTask = () => { logsVisible={logsVisible} />
- + + {taskStream.error ? ( + + + + ) : null} + { + {logsVisible ? ( diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx index 2cfcdf1da6..00e8452b42 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx @@ -30,7 +30,6 @@ export const TaskLogStream = (props: { logs: { [k: string]: string[] } }) => { return (
l.join('\n')) .filter(Boolean) diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx index 40f9f9c189..7cf5ed71bf 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx @@ -20,6 +20,7 @@ import { 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'; @@ -29,7 +30,6 @@ import { StepTime } from './StepTime'; interface StepperProps { steps: (TaskStep & Step)[]; activeStep?: number; - setActiveStep?: (step: number) => void; } export const TaskSteps = (props: StepperProps) => { @@ -58,8 +58,7 @@ export const TaskSteps = (props: StepperProps) => { StepIconProps={stepIconProps} StepIconComponent={StepIcon} > - {step.name} -
+ {step.name} From 33b6ed5a5a6c5ca92aa81d69a6c303fd1d3da321 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Feb 2023 21:52:15 +0100 Subject: [PATCH 21/26] chore: moving the outputs around and fixing some navigation] Signed-off-by: blam --- .../DefaultTemplateOutputs.tsx} | 11 ++++++-- .../TemplateOutputs}/LinkOutputs.tsx | 2 +- .../next/components/TemplateOutputs}/index.ts | 2 +- .../src/next/components/index.ts | 1 + .../src/next/OngoingTask/OngoingTask.tsx | 15 ++++++++--- plugins/scaffolder/src/next/Router/Router.tsx | 26 +++++++++++++++++-- .../TemplateListPage/TemplateListPage.tsx | 10 ++++++- 7 files changed, 57 insertions(+), 10 deletions(-) rename plugins/{scaffolder/src/next/OngoingTask/Outputs/DefaultOutputs.tsx => scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx} (81%) rename plugins/{scaffolder/src/next/OngoingTask/Outputs => scaffolder-react/src/next/components/TemplateOutputs}/LinkOutputs.tsx (96%) rename plugins/{scaffolder/src/next/OngoingTask/Outputs => scaffolder-react/src/next/components/TemplateOutputs}/index.ts (90%) diff --git a/plugins/scaffolder/src/next/OngoingTask/Outputs/DefaultOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx similarity index 81% rename from plugins/scaffolder/src/next/OngoingTask/Outputs/DefaultOutputs.tsx rename to plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index 39bf89e4c5..5fbd3a5142 100644 --- a/plugins/scaffolder/src/next/OngoingTask/Outputs/DefaultOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -14,11 +14,18 @@ * limitations under the License. */ import React from 'react'; -import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; import { Box, Paper } from '@material-ui/core'; import { LinkOutputs } from './LinkOutputs'; +import { ScaffolderTaskOutput } from '../../../api'; -export const DefaultOutputs = (props: { output?: ScaffolderTaskOutput }) => { +/** + * The DefaultOutputs renderer for the scaffolder task output + * + * @alpha + */ +export const DefaultTemplateOutputs = (props: { + output?: ScaffolderTaskOutput; +}) => { if (!props.output?.links) { return null; } diff --git a/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx similarity index 96% rename from plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx rename to plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx index d7c5ad0b67..d172aa9cb3 100644 --- a/plugins/scaffolder/src/next/OngoingTask/Outputs/LinkOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx @@ -15,12 +15,12 @@ */ import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; -import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; import { Button, makeStyles } from '@material-ui/core'; import React from 'react'; import WebIcon from '@material-ui/icons/Web'; import { parseEntityRef } from '@backstage/catalog-model'; import { Link } from '@backstage/core-components'; +import { ScaffolderTaskOutput } from '../../../api'; const useStyles = makeStyles({ root: { diff --git a/plugins/scaffolder/src/next/OngoingTask/Outputs/index.ts b/plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts similarity index 90% rename from plugins/scaffolder/src/next/OngoingTask/Outputs/index.ts rename to plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts index 647f1eebb2..1c998eb1b9 100644 --- a/plugins/scaffolder/src/next/OngoingTask/Outputs/index.ts +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { DefaultOutputs } from './DefaultOutputs'; +export { DefaultTemplateOutputs } from './DefaultTemplateOutputs'; diff --git a/plugins/scaffolder-react/src/next/components/index.ts b/plugins/scaffolder-react/src/next/components/index.ts index babe3b4dcb..05e5a1105a 100644 --- a/plugins/scaffolder-react/src/next/components/index.ts +++ b/plugins/scaffolder-react/src/next/components/index.ts @@ -18,3 +18,4 @@ export * from './TemplateCard'; export * from './ReviewState'; export * from './TemplateGroup'; export * from './Workflow'; +export * from './TemplateOutputs'; diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx index e166a96dd7..a68df01ed5 100644 --- a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx @@ -24,8 +24,11 @@ import { TaskLogStream } from './TaskLogStream'; import { nextSelectedTemplateRouteRef } from '../routes'; import { useRouteRef } from '@backstage/core-plugin-api'; import qs from 'qs'; -import { DefaultOutputs } from './Outputs'; import { ContextMenu } from './ContextMenu'; +import { + DefaultTemplateOutputs, + ScaffolderTaskOutput, +} from '@backstage/plugin-scaffolder-react'; const useStyles = makeStyles({ contentWrapper: { @@ -34,7 +37,11 @@ const useStyles = makeStyles({ }, }); -export const OngoingTask = () => { +export const OngoingTask = (props: { + TemplateOutputsComponent?: React.ComponentType<{ + output?: ScaffolderTaskOutput; + }>; +}) => { // todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. const { taskId } = useParams(); const templateRouteRef = useRouteRef(nextSelectedTemplateRouteRef); @@ -92,6 +99,8 @@ export const OngoingTask = () => { templateRouteRef, ]); + const Outputs = props.TemplateOutputsComponent ?? DefaultTemplateOutputs; + const templateName = taskStream.task?.spec.templateInfo?.entity?.metadata.name; @@ -134,7 +143,7 @@ export const OngoingTask = () => { - + {logsVisible ? ( diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index b9c18f9d02..6ac8bbff65 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, @@ -47,9 +48,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; + }; }; /** @@ -58,10 +71,13 @@ export type NextRouterProps = { * @alpha */ export const Router = (props: PropsWithChildren) => { - const { components: { TemplateCardComponent } = {} } = props; + const { + components: { TemplateCardComponent, TemplateOutputsComponent } = {}, + } = props; const outlet = useOutlet() || props.children; const customFieldExtensions = useCustomFieldExtensions(outlet); + const fieldExtensions = [ ...customFieldExtensions, ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( @@ -81,6 +97,7 @@ export const Router = (props: PropsWithChildren) => { element={ } @@ -97,7 +114,12 @@ export const Router = (props: PropsWithChildren) => { } /> - } /> + + } + /> } diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 4eae84b866..3097bd98be 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 { ScaffolderPageContextMenu } from '../../components/ScaffolderPage/ScaffolderPageContextMenu'; 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" - /> + > + + Date: Mon, 6 Feb 2023 21:54:20 +0100 Subject: [PATCH 22/26] chore: fixing api-reports Signed-off-by: blam --- plugins/badges-backend/api-report.md | 2 +- plugins/bitbucket-cloud-common/api-report.md | 14 +++++++------- plugins/scaffolder-react/api-report.md | 5 +++++ plugins/scaffolder/api-report.md | 8 ++++++++ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md index 8ca4b561a2..0ecf41ee85 100644 --- a/plugins/badges-backend/api-report.md +++ b/plugins/badges-backend/api-report.md @@ -79,7 +79,7 @@ export type BadgeSpec = { }; // @public (undocumented) -export type BadgeStyle = (typeof BADGE_STYLES)[number]; +export type BadgeStyle = typeof BADGE_STYLES[number]; // @public (undocumented) export const createDefaultBadgeFactories: () => BadgeFactories; diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 95e6f36836..5182579a4b 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -132,7 +132,7 @@ export namespace Models { readonly Plaintext: 'plaintext'; }; export type BaseCommitSummaryMarkupEnum = - (typeof BaseCommitSummaryMarkupEnum)[keyof typeof BaseCommitSummaryMarkupEnum]; + typeof BaseCommitSummaryMarkupEnum[keyof typeof BaseCommitSummaryMarkupEnum]; export interface Branch { default_merge_strategy?: string; // (undocumented) @@ -150,7 +150,7 @@ export namespace Models { readonly FastForward: 'fast_forward'; }; export type BranchMergeStrategiesEnum = - (typeof BranchMergeStrategiesEnum)[keyof typeof BranchMergeStrategiesEnum]; + typeof BranchMergeStrategiesEnum[keyof typeof BranchMergeStrategiesEnum]; export interface Commit extends BaseCommit { // (undocumented) participants?: Array; @@ -179,7 +179,7 @@ export namespace Models { }; // (undocumented) export type CommitFileAttributesEnum = - (typeof CommitFileAttributesEnum)[keyof typeof CommitFileAttributesEnum]; + typeof CommitFileAttributesEnum[keyof typeof CommitFileAttributesEnum]; export interface Link { // (undocumented) href?: string; @@ -221,7 +221,7 @@ export namespace Models { }; // (undocumented) export type ParticipantRoleEnum = - (typeof ParticipantRoleEnum)[keyof typeof ParticipantRoleEnum]; + typeof ParticipantRoleEnum[keyof typeof ParticipantRoleEnum]; const // (undocumented) ParticipantStateEnum: { readonly Approved: 'approved'; @@ -230,7 +230,7 @@ export namespace Models { }; // (undocumented) export type ParticipantStateEnum = - (typeof ParticipantStateEnum)[keyof typeof ParticipantStateEnum]; + typeof ParticipantStateEnum[keyof typeof ParticipantStateEnum]; export interface Project extends ModelObject { // (undocumented) created_on?: string; @@ -306,7 +306,7 @@ export namespace Models { readonly NoForks: 'no_forks'; }; export type RepositoryForkPolicyEnum = - (typeof RepositoryForkPolicyEnum)[keyof typeof RepositoryForkPolicyEnum]; + typeof RepositoryForkPolicyEnum[keyof typeof RepositoryForkPolicyEnum]; const // (undocumented) RepositoryScmEnum: { readonly Git: 'git'; @@ -336,7 +336,7 @@ export namespace Models { } // (undocumented) export type RepositoryScmEnum = - (typeof RepositoryScmEnum)[keyof typeof RepositoryScmEnum]; + typeof RepositoryScmEnum[keyof typeof RepositoryScmEnum]; // (undocumented) export interface SearchCodeSearchResult { // (undocumented) diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index deead4366c..72a0b92eef 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -85,6 +85,11 @@ export type CustomFieldValidator = ( }, ) => void | Promise; +// @alpha +export const DefaultTemplateOutputs: (props: { + output?: ScaffolderTaskOutput; +}) => JSX.Element | null; + // @alpha (undocumented) export const EmbeddableWorkflow: (props: WorkflowProps) => JSX.Element; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 1fb72b0fac..43b885b81a 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -197,9 +197,17 @@ export type NextRouterProps = { template: TemplateEntityV1beta3; }>; TaskPageComponent?: React_2.ComponentType<{}>; + TemplateOutputsComponent?: React_2.ComponentType<{ + output?: ScaffolderTaskOutput_2; + }>; }; groups?: TemplateGroupFilter[]; FormProps?: FormProps_2; + contextMenu?: { + editor?: boolean; + actions?: boolean; + tasks?: boolean; + }; }; // @alpha From 01b6f56c8072ee155008943ae652d9dc0d3a2a36 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Feb 2023 10:49:48 +0100 Subject: [PATCH 23/26] chore: fixing api-reports Signed-off-by: blam --- plugins/scaffolder/src/components/Router.tsx | 5 +- plugins/scaffolder/src/next/Router/Router.tsx | 9 ++ .../src/next/TemplateListPage/ContextMenu.tsx | 123 ++++++++++++++++++ .../TemplateListPage/TemplateListPage.tsx | 4 +- plugins/scaffolder/src/next/routes.ts | 21 +++ 5 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index ccc2bd283b..06c28682c5 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -149,12 +149,13 @@ export const Router = (props: RouterProps) => { } /> + + } /> + } /> } /> - } /> - } /> ) => { } /> + } /> + } + /> } 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 && ( + navigate(editLink())}> + + + + + + )} + {showActions && ( + navigate(actionsLink())}> + + + + + + )} + {showTasks && ( + navigate(tasksLink())}> + + + + + + )} + + + + ); +} diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 3097bd98be..b701ceb3c4 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -37,7 +37,7 @@ import { RegisterExistingButton } from './RegisterExistingButton'; import { useRouteRef } from '@backstage/core-plugin-api'; import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups'; import { registerComponentRouteRef } from '../../routes'; -import { ScaffolderPageContextMenu } from '../../components/ScaffolderPage/ScaffolderPageContextMenu'; +import { ContextMenu } from './ContextMenu'; export type TemplateListPageProps = { TemplateCardComponent?: React.ComponentType<{ @@ -68,7 +68,7 @@ export const TemplateListPage = (props: TemplateListPageProps) => { title="Create a new component" subtitle="Create new software components using standard templates in your organization" > - + 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', +}); From 419aaafd18a9ba2ae26eef93cba1248df1192b5e Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Feb 2023 11:20:59 +0100 Subject: [PATCH 24/26] chore: fixing the prettier of the api-reports Signed-off-by: blam --- plugins/badges-backend/api-report.md | 2 +- plugins/bitbucket-cloud-common/api-report.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md index 0ecf41ee85..8ca4b561a2 100644 --- a/plugins/badges-backend/api-report.md +++ b/plugins/badges-backend/api-report.md @@ -79,7 +79,7 @@ export type BadgeSpec = { }; // @public (undocumented) -export type BadgeStyle = typeof BADGE_STYLES[number]; +export type BadgeStyle = (typeof BADGE_STYLES)[number]; // @public (undocumented) export const createDefaultBadgeFactories: () => BadgeFactories; diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 5182579a4b..95e6f36836 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -132,7 +132,7 @@ export namespace Models { readonly Plaintext: 'plaintext'; }; export type BaseCommitSummaryMarkupEnum = - typeof BaseCommitSummaryMarkupEnum[keyof typeof BaseCommitSummaryMarkupEnum]; + (typeof BaseCommitSummaryMarkupEnum)[keyof typeof BaseCommitSummaryMarkupEnum]; export interface Branch { default_merge_strategy?: string; // (undocumented) @@ -150,7 +150,7 @@ export namespace Models { readonly FastForward: 'fast_forward'; }; export type BranchMergeStrategiesEnum = - typeof BranchMergeStrategiesEnum[keyof typeof BranchMergeStrategiesEnum]; + (typeof BranchMergeStrategiesEnum)[keyof typeof BranchMergeStrategiesEnum]; export interface Commit extends BaseCommit { // (undocumented) participants?: Array; @@ -179,7 +179,7 @@ export namespace Models { }; // (undocumented) export type CommitFileAttributesEnum = - typeof CommitFileAttributesEnum[keyof typeof CommitFileAttributesEnum]; + (typeof CommitFileAttributesEnum)[keyof typeof CommitFileAttributesEnum]; export interface Link { // (undocumented) href?: string; @@ -221,7 +221,7 @@ export namespace Models { }; // (undocumented) export type ParticipantRoleEnum = - typeof ParticipantRoleEnum[keyof typeof ParticipantRoleEnum]; + (typeof ParticipantRoleEnum)[keyof typeof ParticipantRoleEnum]; const // (undocumented) ParticipantStateEnum: { readonly Approved: 'approved'; @@ -230,7 +230,7 @@ export namespace Models { }; // (undocumented) export type ParticipantStateEnum = - typeof ParticipantStateEnum[keyof typeof ParticipantStateEnum]; + (typeof ParticipantStateEnum)[keyof typeof ParticipantStateEnum]; export interface Project extends ModelObject { // (undocumented) created_on?: string; @@ -306,7 +306,7 @@ export namespace Models { readonly NoForks: 'no_forks'; }; export type RepositoryForkPolicyEnum = - typeof RepositoryForkPolicyEnum[keyof typeof RepositoryForkPolicyEnum]; + (typeof RepositoryForkPolicyEnum)[keyof typeof RepositoryForkPolicyEnum]; const // (undocumented) RepositoryScmEnum: { readonly Git: 'git'; @@ -336,7 +336,7 @@ export namespace Models { } // (undocumented) export type RepositoryScmEnum = - typeof RepositoryScmEnum[keyof typeof RepositoryScmEnum]; + (typeof RepositoryScmEnum)[keyof typeof RepositoryScmEnum]; // (undocumented) export interface SearchCodeSearchResult { // (undocumented) From 0f0f48cb821ef397e5e61ef9d6b264c4310439e7 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Feb 2023 12:40:05 +0100 Subject: [PATCH 25/26] chore: reset router Signed-off-by: blam --- plugins/scaffolder/src/components/Router.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 06c28682c5..ccc2bd283b 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -149,13 +149,12 @@ export const Router = (props: RouterProps) => { } /> - - } /> - } /> } /> + } /> + } /> Date: Tue, 7 Feb 2023 12:50:38 +0100 Subject: [PATCH 26/26] chore: prefer useMountEffect over use memo Signed-off-by: blam --- .../src/next/OngoingTask/TaskSteps/StepTime.tsx | 5 +++-- plugins/scaffolder/src/next/Router/Router.tsx | 10 ++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx index 2ec89f385a..653fe6e942 100644 --- a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, useMemo, useState } from 'react'; +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: { @@ -48,7 +49,7 @@ export const StepTime = (props: { setTime(humanizeDuration(formatted, { round: true })); }, [step.endedAt, step.startedAt]); - useMemo(() => calculate(), [calculate]); + useMountEffect(() => calculate()); useInterval(() => !step.endedAt && calculate(), 1000); diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index 0ca9e452cd..049579e16c 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -76,7 +76,11 @@ export type NextRouterProps = { */ export const Router = (props: PropsWithChildren) => { const { - components: { TemplateCardComponent, TemplateOutputsComponent } = {}, + components: { + TemplateCardComponent, + TemplateOutputsComponent, + TaskPageComponent = OngoingTask, + } = {}, } = props; const outlet = useOutlet() || props.children; const customFieldExtensions = @@ -121,7 +125,9 @@ export const Router = (props: PropsWithChildren) => { + } /> } />