From 2da89d12895d39a44002051d45f2abfb1c757ff2 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 10:00:42 +0200 Subject: [PATCH 1/9] scaffolder: refine analytics events data Signed-off-by: Vincenzo Scamporlino --- .../TemplateGroup/TemplateGroup.tsx | 18 ++++--- .../src/next/components/Workflow/Workflow.tsx | 6 +-- .../components/OngoingTask/ContextMenu.tsx | 28 ++-------- .../components/OngoingTask/OngoingTask.tsx | 51 ++++++++++++++++--- 4 files changed, 63 insertions(+), 40 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.tsx b/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.tsx index a70b822d13..74ade33344 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateGroup/TemplateGroup.tsx @@ -22,7 +22,7 @@ import { } from '@backstage/core-components'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { TemplateCardProps, TemplateCard } from '../TemplateCard'; -import { IconComponent } from '@backstage/core-plugin-api'; +import { AnalyticsContext, IconComponent } from '@backstage/core-plugin-api'; /** * The props for the {@link TemplateGroup} component. @@ -69,12 +69,18 @@ export const TemplateGroup = (props: TemplateGroupProps) => { {titleComponent} {templates.map(({ template, additionalLinks }) => ( - + > + + ))} diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx index 2aa4ee9b2d..95e2647b5a 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx @@ -97,13 +97,11 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { async (formState: Record) => { await onCreate(formState); - const name = - typeof formState.name === 'string' ? formState.name : undefined; - analytics.captureEvent('create', name ?? templateName ?? 'unknown', { + analytics.captureEvent('create', 'Task has been created', { value: minutesSaved, }); }, - [onCreate, analytics, templateName, minutesSaved], + [onCreate, analytics, minutesSaved], ); useEffect(() => { diff --git a/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx index 704d39a8fd..e4ca230c36 100644 --- a/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx @@ -29,11 +29,8 @@ import Toc from '@material-ui/icons/Toc'; import ControlPointIcon from '@material-ui/icons/ControlPoint'; import MoreVert from '@material-ui/icons/MoreVert'; import { SyntheticEvent, useState } from 'react'; -import { useAnalytics, useApi } from '@backstage/core-plugin-api'; -import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { usePermission } from '@backstage/plugin-permission-react'; import { - taskCancelPermission, taskReadPermission, taskCreatePermission, } from '@backstage/plugin-scaffolder-common/alpha'; @@ -50,7 +47,8 @@ type ContextMenuProps = { onStartOver?: () => void; onToggleLogs?: (state: boolean) => void; onToggleButtonBar?: (state: boolean) => void; - taskId?: string; + isCancelButtonDisabled: boolean; + onCancel: () => void; }; const useStyles = makeStyles(() => ({ @@ -70,27 +68,13 @@ export const ContextMenu = (props: ContextMenuProps) => { onStartOver, onToggleLogs, onToggleButtonBar, - taskId, } = props; const { getPageTheme } = useTheme(); const pageTheme = getPageTheme({ themeId: 'website' }); const classes = useStyles({ fontColor: pageTheme.fontColor }); - const scaffolderApi = useApi(scaffolderApiRef); - const analytics = useAnalytics(); const [anchorEl, setAnchorEl] = useState(); const { t } = useTranslationRef(scaffolderTranslationRef); - const [{ status: cancelStatus }, { execute: cancel }] = useAsync(async () => { - if (taskId) { - analytics.captureEvent('cancelled', 'Template has been cancelled'); - await scaffolderApi.cancelTask(taskId); - } - }); - - const { allowed: canCancelTask } = usePermission({ - permission: taskCancelPermission, - }); - const { allowed: canReadTask } = usePermission({ permission: taskReadPermission, }); @@ -171,12 +155,8 @@ export const ContextMenu = (props: ContextMenuProps) => { )} diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index b718a257b6..c9a119e4a1 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -32,7 +32,12 @@ import { useTaskEventStream, } from '@backstage/plugin-scaffolder-react'; import { selectedTemplateRouteRef } from '../../routes'; -import { useAnalytics, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { + AnalyticsContext, + useAnalytics, + useApi, + useRouteRef, +} from '@backstage/core-plugin-api'; import qs from 'qs'; import { ContextMenu } from './ContextMenu'; import { @@ -51,6 +56,7 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { scaffolderTranslationRef } from '../../translation'; import { entityPresentationApiRef } from '@backstage/plugin-catalog-react'; import { default as reactUseAsync } from 'react-use/esm/useAsync'; +import { stringifyEntityRef } from '@backstage/catalog-model'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -82,6 +88,36 @@ export const OngoingTask = (props: { }>; }) => { // todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. + const { taskId } = useParams(); + const taskStream = useTaskEventStream(taskId!); + const { namespace, name } = + taskStream.task?.spec.templateInfo?.entity?.metadata ?? {}; + + return ( + + + + + + ); +}; + +function OngoingTaskContent(props: { + TemplateOutputsComponent?: ComponentType<{ + output?: ScaffolderTaskOutput; + }>; +}) { const { taskId } = useParams(); const templateRouteRef = useRouteRef(selectedTemplateRouteRef); const navigate = useNavigate(); @@ -183,7 +219,7 @@ export const OngoingTask = (props: { templateRouteRef, ]); - const [{ status: _ }, { execute: triggerRetry }] = useAsync(async () => { + const [, { execute: triggerRetry }] = useAsync(async () => { if (taskId) { analytics.captureEvent('retried', 'Template has been retried'); await scaffolderApi.retry?.(taskId); @@ -202,9 +238,11 @@ export const OngoingTask = (props: { const Outputs = props.TemplateOutputsComponent ?? DefaultTemplateOutputs; const cancelEnabled = !(taskStream.cancelled || taskStream.completed); + const isCancelButtonDisabled = + !cancelEnabled || cancelStatus !== 'not-executed' || !canCancelTask; return ( - + <>
@@ -316,6 +355,6 @@ export const OngoingTask = (props: { ) : null} -
+ ); -}; +} From bea4f7f368f26992a7cd3b536900881c652906ad Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 10:01:33 +0200 Subject: [PATCH 2/9] scaffolder: do not error on render method Signed-off-by: Vincenzo Scamporlino --- .../next/components/TemplateGroups/TemplateGroups.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.tsx b/plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.tsx index a0070765fe..58b74dff9d 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.tsx @@ -22,7 +22,7 @@ import { } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; import Typography from '@material-ui/core/Typography'; -import { ComponentType, useCallback } from 'react'; +import { ComponentType, useCallback, useEffect } from 'react'; import { TemplateGroup } from '../TemplateGroup/TemplateGroup'; @@ -58,12 +58,17 @@ export const TemplateGroups = (props: TemplateGroupsProps) => { [onTemplateSelected], ); + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + if (loading) { return ; } if (error) { - errorApi.post(error); return null; } From 6c972fe17f8dfd5de10a2b32c862071f6c1ccd93 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 10:12:43 +0200 Subject: [PATCH 3/9] scaffolder: analytics changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/dull-cloths-act.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dull-cloths-act.md diff --git a/.changeset/dull-cloths-act.md b/.changeset/dull-cloths-act.md new file mode 100644 index 0000000000..82ba469c7d --- /dev/null +++ b/.changeset/dull-cloths-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added information about the `entityRef` and `taskId` to the analytics events whenever is possible. From 2c77bd52d7fb254e9aa00f1bc59414c4406f1f63 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 10:30:49 +0200 Subject: [PATCH 4/9] scaffolder: track steps Signed-off-by: Vincenzo Scamporlino --- .../src/next/components/Workflow/Workflow.tsx | 5 +- .../next/hooks/useFilteredSchemaProperties.ts | 85 ++++++++++--------- 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx index 95e2647b5a..527009037d 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx @@ -99,9 +99,12 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { analytics.captureEvent('create', 'Task has been created', { value: minutesSaved, + attributes: { + templateSteps: sortedManifest?.steps?.length ?? 0, + }, }); }, - [onCreate, analytics, minutesSaved], + [onCreate, analytics, minutesSaved, sortedManifest], ); useEffect(() => { diff --git a/plugins/scaffolder-react/src/next/hooks/useFilteredSchemaProperties.ts b/plugins/scaffolder-react/src/next/hooks/useFilteredSchemaProperties.ts index be6f581f87..cac696a745 100644 --- a/plugins/scaffolder-react/src/next/hooks/useFilteredSchemaProperties.ts +++ b/plugins/scaffolder-react/src/next/hooks/useFilteredSchemaProperties.ts @@ -16,6 +16,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { useApi, featureFlagsApiRef } from '@backstage/core-plugin-api'; import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; +import { useMemo } from 'react'; /** * Returns manifest of software templates with steps without a featureFlag tag. @@ -28,49 +29,51 @@ export const useFilteredSchemaProperties = ( const featureFlagKey = 'backstage:featureFlag'; const featureFlagApi = useApi(featureFlagsApiRef); - if (!manifest) { - return undefined; - } - - const filteredSteps = manifest?.steps - .filter(step => { - const featureFlag = step.schema[featureFlagKey]; - return ( - typeof featureFlag !== 'string' || featureFlagApi.isActive(featureFlag) - ); - }) - .map(step => { - const filteredStep = cloneDeep(step); - const removedPropertyKeys: Array = []; - if (filteredStep.schema.properties) { - filteredStep.schema.properties = Object.fromEntries( - Object.entries(filteredStep.schema.properties).filter( - ([key, value]) => { - if (value[featureFlagKey]) { - if (featureFlagApi.isActive(value[featureFlagKey])) { - return true; - } - - removedPropertyKeys.push(key); - return false; - } - return true; - }, - ), + return useMemo(() => { + if (!manifest) { + return undefined; + } + const filteredSteps = manifest?.steps + .filter(step => { + const featureFlag = step.schema[featureFlagKey]; + return ( + typeof featureFlag !== 'string' || + featureFlagApi.isActive(featureFlag) ); + }) + .map(step => { + const filteredStep = cloneDeep(step); + const removedPropertyKeys: Array = []; + if (filteredStep.schema.properties) { + filteredStep.schema.properties = Object.fromEntries( + Object.entries(filteredStep.schema.properties).filter( + ([key, value]) => { + if (value[featureFlagKey]) { + if (featureFlagApi.isActive(value[featureFlagKey])) { + return true; + } - // remove the feature flag property key from required if they are not active - filteredStep.schema.required = Array.isArray( - filteredStep.schema.required, - ) - ? filteredStep.schema.required?.filter( - r => !removedPropertyKeys.includes(r as string), - ) - : filteredStep.schema.required; - } + removedPropertyKeys.push(key); + return false; + } + return true; + }, + ), + ); - return filteredStep; - }); + // remove the feature flag property key from required if they are not active + filteredStep.schema.required = Array.isArray( + filteredStep.schema.required, + ) + ? filteredStep.schema.required?.filter( + r => !removedPropertyKeys.includes(r as string), + ) + : filteredStep.schema.required; + } - return { ...manifest, steps: filteredSteps }; + return filteredStep; + }); + + return { ...manifest, steps: filteredSteps }; + }, [manifest, featureFlagApi]); }; From 2977da8602e5b384e85a93b9819fb88c082b59ee Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 10:47:13 +0200 Subject: [PATCH 5/9] scaffolder: fix test Signed-off-by: Vincenzo Scamporlino --- .../TemplateWizardPage.test.tsx | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx index d3a02931de..6836259dd8 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -21,7 +21,7 @@ import { renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; -import { act, fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { ScaffolderApi, scaffolderApiRef, @@ -127,14 +127,10 @@ describe('TemplateWizardPage', () => { }); // Go to the final page - await act(async () => { - fireEvent.click(await findByRole('button', { name: 'Review' })); - }); + fireEvent.click(await findByRole('button', { name: 'Review' })); // Create the software - await act(async () => { - fireEvent.click(await findByRole('button', { name: 'Create' })); - }); + fireEvent.click(await findByRole('button', { name: 'Create' })); // The "Next Step" button should have fired an event expect(analyticsApi.captureEvent).toHaveBeenCalledWith( @@ -148,15 +144,20 @@ describe('TemplateWizardPage', () => { ); // And the "Create" button should have fired an event - expect(analyticsApi.captureEvent).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'create', - subject: 'expected-name', - context: expect.objectContaining({ - entityRef: 'template:default/test', + await waitFor(() => + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'create', + subject: 'Task has been created', + attributes: { + templateSteps: 1, + }, + context: expect.objectContaining({ + entityRef: 'template:default/test', + }), + value: 120, }), - value: 120, - }), + ), ); }); From 5f74715c81b9a92447e737cb10d73ae6b0930239 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 12:18:38 +0200 Subject: [PATCH 6/9] scaffolder: remove unused import Signed-off-by: Vincenzo Scamporlino --- plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx index e4ca230c36..e1a5f37ddc 100644 --- a/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx @@ -21,7 +21,6 @@ import MenuItem from '@material-ui/core/MenuItem'; import MenuList from '@material-ui/core/MenuList'; import Popover from '@material-ui/core/Popover'; import { makeStyles, Theme, useTheme } from '@material-ui/core/styles'; -import { useAsync } from '@react-hookz/web'; import Cancel from '@material-ui/icons/Cancel'; import Repeat from '@material-ui/icons/Repeat'; import Replay from '@material-ui/icons/Replay'; From 015af26f82a3be19271621bab95c67e65df51801 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 12:25:57 +0200 Subject: [PATCH 7/9] scaffolder: scaffolder-react changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/dull-cloths-act.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/dull-cloths-act.md b/.changeset/dull-cloths-act.md index 82ba469c7d..e49ee5a289 100644 --- a/.changeset/dull-cloths-act.md +++ b/.changeset/dull-cloths-act.md @@ -1,4 +1,5 @@ --- +'@backstage/plugin-scaffolder-react': minor '@backstage/plugin-scaffolder': minor --- From 90dc85d9d49c7dcc93f5987f58190623732fae5c Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 13:05:13 +0200 Subject: [PATCH 8/9] scaffolder: track edit link Signed-off-by: Vincenzo Scamporlino --- .../TemplateWizardPageContextMenu.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPageContextMenu.tsx b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPageContextMenu.tsx index 9a912f265f..72fa397a9e 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPageContextMenu.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPageContextMenu.tsx @@ -26,6 +26,7 @@ import MoreVert from '@material-ui/icons/MoreVert'; import { SyntheticEvent, useState } from 'react'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { scaffolderTranslationRef } from '../../../translation'; +import { Link } from '@backstage/core-components'; const useStyles = makeStyles(theme => ({ button: { @@ -82,16 +83,18 @@ export function TemplateWizardPageContextMenu( transformOrigin={{ vertical: 'top', horizontal: 'right' }} > - window.open(editUrl, '_blank')}> - - - - - + + + + + + + + From 5b2a34ab5881d8330f0df0efaa10f3c3a801a374 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 13 Jun 2025 15:38:44 +0200 Subject: [PATCH 9/9] scaffolder: adjust link component Signed-off-by: Vincenzo Scamporlino --- .../TemplateWizardPageContextMenu.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPageContextMenu.tsx b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPageContextMenu.tsx index 72fa397a9e..c15795de2c 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPageContextMenu.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPageContextMenu.tsx @@ -83,18 +83,16 @@ export function TemplateWizardPageContextMenu( transformOrigin={{ vertical: 'top', horizontal: 'right' }} > - - - - - - - - + + + + + +