Capturing more event clicks for scaffolder

Signed-off-by: bnechyporenko <bnechyporenko@bol.com>
This commit is contained in:
bnechyporenko
2024-04-27 20:59:32 +02:00
parent 3102a99f48
commit 9156654290
7 changed files with 74 additions and 33 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-react': patch
'@backstage/plugin-scaffolder': patch
---
Capturing more event clicks for scaffolder
@@ -141,19 +141,25 @@
},
"presentation": {
"type": "object",
"description": "A way to redefine the labels for actionable buttons.",
"description": "A way to redefine the presentation of the scaffolder.",
"properties": {
"backButtonText": {
"type": "string",
"description": "A button which return the user to one step back."
},
"createButtonText": {
"type": "string",
"description": "A button which start the execution of the template."
},
"reviewButtonText": {
"type": "string",
"description": "A button which open the review step to verify the input prior to start the execution."
"buttonLabels": {
"type": "object",
"description": "A way to redefine the labels for actionable buttons.",
"properties": {
"backButtonText": {
"type": "string",
"description": "A button which return the user to one step back."
},
"createButtonText": {
"type": "string",
"description": "A button which start the execution of the template."
},
"reviewButtonText": {
"type": "string",
"description": "A button which open the review step to verify the input prior to start the execution."
}
}
}
}
},
@@ -35,9 +35,8 @@ import {
type FormValidation,
} from './createAsyncValidators';
import { ReviewState, type ReviewStateProps } from '../ReviewState';
import { useTemplateSchema } from '../../hooks/useTemplateSchema';
import { useTemplateSchema, useFormDataFromQuery } from '../../hooks';
import validator from '@rjsf/validator-ajv8';
import { useFormDataFromQuery } from '../../hooks';
import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps';
import { hasErrors } from './utils';
import * as FieldOverrides from './FieldOverrides';
@@ -115,6 +114,13 @@ export const Stepper = (stepperProps: StepperProps) => {
const [errors, setErrors] = useState<undefined | FormValidation>();
const styles = useStyles();
const backLabel =
presentation?.buttonLabels?.backButtonText ?? backButtonText;
const createLabel =
presentation?.buttonLabels?.createButtonText ?? createButtonText;
const reviewLabel =
presentation?.buttonLabels?.reviewButtonText ?? reviewButtonText;
const extensions = useMemo(() => {
return Object.fromEntries(
props.extensions.map(({ name, component }) => [name, component]),
@@ -150,7 +156,8 @@ export const Stepper = (stepperProps: StepperProps) => {
const handleCreate = useCallback(() => {
props.onCreate(formState);
}, [props, formState]);
analytics.captureEvent('click', `${createLabel}`);
}, [props, formState, analytics, createLabel]);
const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts });
@@ -175,19 +182,13 @@ export const Stepper = (stepperProps: StepperProps) => {
setActiveStep(prevActiveStep => {
const stepNum = prevActiveStep + 1;
analytics.captureEvent('click', `Next Step (${stepNum})`);
analytics.captureEvent('click', `Next Step (${stepNum})`);
return stepNum;
});
}
setFormState(current => ({ ...current, ...formData }));
};
const backLabel =
presentation?.buttonLabels?.backButtonText ?? backButtonText;
const createLabel =
presentation?.buttonLabels?.createButtonText ?? createButtonText;
const reviewLabel =
presentation?.buttonLabels?.reviewButtonText ?? reviewButtonText;
return (
<>
{isValidating && <LinearProgress variant="indeterminate" />}
@@ -214,7 +215,7 @@ export const Stepper = (stepperProps: StepperProps) => {
);
})}
<MuiStep>
<MuiStepLabel>Review</MuiStepLabel>
<MuiStepLabel>${reviewLabel}</MuiStepLabel>
</MuiStep>
</MuiStepper>
<div className={styles.formWrapper}>
@@ -274,7 +275,7 @@ export const Stepper = (stepperProps: StepperProps) => {
className={styles.backButton}
disabled={activeStep < 1}
>
Back
{backLabel}
</Button>
<Button
variant="contained"
@@ -16,7 +16,11 @@
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { MarkdownContent, UserIcon } from '@backstage/core-components';
import { IconComponent, useApp } from '@backstage/core-plugin-api';
import {
IconComponent,
useAnalytics,
useApp,
} from '@backstage/core-plugin-api';
import {
EntityRefLinks,
getEntityRelations,
@@ -32,7 +36,7 @@ import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import { makeStyles, Theme } from '@material-ui/core/styles';
import LanguageIcon from '@material-ui/icons/Language';
import React from 'react';
import React, { useCallback } from 'react';
import { CardHeader } from './CardHeader';
import { CardLink } from './CardLink';
@@ -92,8 +96,9 @@ export interface TemplateCardProps {
* @alpha
*/
export const TemplateCard = (props: TemplateCardProps) => {
const { template } = props;
const { onSelected, template } = props;
const styles = useStyles();
const analytics = useAnalytics();
const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY);
const app = useApp();
const iconResolver = (key?: string): IconComponent =>
@@ -103,6 +108,11 @@ export const TemplateCard = (props: TemplateCardProps) => {
!!props.additionalLinks?.length || !!template.metadata.links?.length;
const displayDefaultDivider = !hasTags && !hasLinks;
const handleChoose = useCallback(() => {
analytics.captureEvent('click', `Template has been opened`);
onSelected?.(template);
}, [analytics, onSelected, template]);
return (
<Card>
<CardHeader template={template} />
@@ -190,7 +200,7 @@ export const TemplateCard = (props: TemplateCardProps) => {
size="small"
variant="outlined"
color="primary"
onClick={() => props.onSelected?.(template)}
onClick={handleChoose}
>
Choose
</Button>
@@ -28,7 +28,7 @@ import Toc from '@material-ui/icons/Toc';
import ControlPointIcon from '@material-ui/icons/ControlPoint';
import MoreVert from '@material-ui/icons/MoreVert';
import React, { useState } from 'react';
import { useApi } from '@backstage/core-plugin-api';
import { useAnalytics, useApi } from '@backstage/core-plugin-api';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
type ContextMenuProps = {
@@ -61,10 +61,12 @@ export const ContextMenu = (props: ContextMenuProps) => {
const pageTheme = getPageTheme({ themeId: 'website' });
const classes = useStyles({ fontColor: pageTheme.fontColor });
const scaffolderApi = useApi(scaffolderApiRef);
const analytics = useAnalytics();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
const [{ status: cancelStatus }, { execute: cancel }] = useAsync(async () => {
if (taskId) {
analytics.captureEvent('cancelled', 'Template has been cancelled');
await scaffolderApi.cancelTask(taskId);
}
});
@@ -26,7 +26,7 @@ import {
useTaskEventStream,
} from '@backstage/plugin-scaffolder-react';
import { selectedTemplateRouteRef } from '../../routes';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import { useAnalytics, useApi, useRouteRef } from '@backstage/core-plugin-api';
import qs from 'qs';
import { ContextMenu } from './ContextMenu';
import {
@@ -66,6 +66,7 @@ export const OngoingTask = (props: {
const { taskId } = useParams();
const templateRouteRef = useRouteRef(selectedTemplateRouteRef);
const navigate = useNavigate();
const analytics = useAnalytics();
const scaffolderApi = useApi(scaffolderApiRef);
const taskStream = useTaskEventStream(taskId!);
const classes = useStyles();
@@ -113,6 +114,8 @@ export const OngoingTask = (props: {
return;
}
analytics.captureEvent('click', `Task has been started over`);
navigate({
pathname: templateRouteRef({
namespace,
@@ -121,6 +124,7 @@ export const OngoingTask = (props: {
search: `?${qs.stringify({ formData: JSON.stringify(formData) })}`,
});
}, [
analytics,
navigate,
taskStream.task?.spec.parameters,
taskStream.task?.spec.templateInfo?.entity?.metadata,
@@ -130,6 +134,7 @@ export const OngoingTask = (props: {
const [{ status: cancelStatus }, { execute: triggerCancel }] = useAsync(
async () => {
if (taskId) {
analytics.captureEvent('cancelled', 'Template has been cancelled');
await scaffolderApi.cancelTask(taskId);
}
},
@@ -129,19 +129,30 @@ describe('TemplateWizardPage', () => {
fireEvent.click(await findByRole('button', { name: 'Create' }));
});
// The "Next Step" button should have fired an event
// The "Next Step" button should have fired few events
expect(analyticsMock.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'Next Step (1)',
context: { entityRef: 'template:default/test' },
});
// And the "Create" button should have fired an event
expect(analyticsMock.getEvents()[1]).toMatchObject({
action: 'click',
subject: 'Next Step (1)',
context: { entityRef: 'template:default/test' },
});
// And the "Create" button should have fired few event
expect(analyticsMock.getEvents()[2]).toMatchObject({
action: 'create',
subject: 'expected-name',
context: { entityRef: 'template:default/test' },
value: 120,
});
expect(analyticsMock.getEvents()[3]).toMatchObject({
action: 'click',
subject: 'Create',
context: { entityRef: 'template:default/test' },
});
});
describe('scaffolder page context menu', () => {