@backstage/plugin-scaffolder 0.12.0 → 0.12.2-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,44 @@
1
- import { createApiRef, useApi, attachComponentData, createExternalRouteRef, createRouteRef, createPlugin, createApiFactory, discoveryApiRef, fetchApiRef, createRoutableExtension, useRouteRef, alertApiRef } from '@backstage/core-plugin-api';
1
+ import { createApiRef, useApi, attachComponentData, createExternalRouteRef, createRouteRef, createPlugin, createApiFactory, discoveryApiRef, fetchApiRef, createRoutableExtension, useRouteRef, alertApiRef, useApp } from '@backstage/core-plugin-api';
2
2
  import { ResponseError } from '@backstage/errors';
3
3
  import qs from 'qs';
4
4
  import ObservableImpl from 'zen-observable';
5
- import { catalogApiRef, formatEntityRefTitle, useOwnedEntities, useStarredEntity, getEntityRelations, getEntitySourceLocation, EntityRefLinks, useEntityListProvider, useEntityTypeFilter } from '@backstage/plugin-catalog-react';
6
- import { TextField, FormControl as FormControl$1, withStyles, makeStyles, IconButton, Tooltip, useTheme, Card, CardMedia, CardContent, Box, Typography, Chip, CardActions, Link, FormControlLabel, Checkbox } from '@material-ui/core';
5
+ import { catalogApiRef, formatEntityRefTitle, useOwnedEntities, useStarredEntity, getEntityRelations, getEntitySourceLocation, EntityRefLinks, useEntityListProvider, useEntityTypeFilter, entityRouteRef } from '@backstage/plugin-catalog-react';
6
+ import { TextField, FormControl as FormControl$1, withStyles, makeStyles, IconButton, Tooltip, useTheme, Card, CardMedia, CardContent, Box, Typography, Chip, CardActions, Link, FormControlLabel, Checkbox, Grid, StepButton, Paper, Button as Button$1, CircularProgress } from '@material-ui/core';
7
7
  import FormControl from '@material-ui/core/FormControl';
8
8
  import Autocomplete from '@material-ui/lab/Autocomplete';
9
- import React, { useCallback, useEffect, useState, useMemo } from 'react';
9
+ import React, { useCallback, useEffect, useState, createContext, useContext, useMemo, memo } from 'react';
10
10
  import useAsync from 'react-use/lib/useAsync';
11
- import { KubernetesValidatorFunctions, makeValidator, RELATION_OWNED_BY, stringifyEntityRef } from '@backstage/catalog-model';
11
+ import { KubernetesValidatorFunctions, makeValidator, RELATION_OWNED_BY, stringifyEntityRef, parseEntityName } from '@backstage/catalog-model';
12
12
  import useEffectOnce from 'react-use/lib/useEffectOnce';
13
13
  import { Autocomplete as Autocomplete$1 } from '@material-ui/lab';
14
- import { scmIntegrationsApiRef, ScmIntegrationIcon } from '@backstage/integration-react';
14
+ import { scmIntegrationsApiRef, scmAuthApiRef, ScmIntegrationIcon } from '@backstage/integration-react';
15
15
  import FormHelperText from '@material-ui/core/FormHelperText';
16
16
  import Input from '@material-ui/core/Input';
17
17
  import InputLabel from '@material-ui/core/InputLabel';
18
- import { Select, Progress, ItemCardHeader, Button, ContentHeader, WarningPanel, Link as Link$1, Content, ItemCardGrid } from '@backstage/core-components';
18
+ import { Select, Progress, ItemCardHeader, Button, ContentHeader, WarningPanel, Link as Link$1, Content, ItemCardGrid, Page, Header, Lifecycle, ErrorPage, LogViewer } from '@backstage/core-components';
19
+ import useDebounce from 'react-use/lib/useDebounce';
19
20
  import Star from '@material-ui/icons/Star';
20
21
  import StarBorder from '@material-ui/icons/StarBorder';
21
22
  import WarningIcon from '@material-ui/icons/Warning';
22
- import { generatePath } from 'react-router';
23
+ import { generatePath, useNavigate, useParams } from 'react-router';
23
24
  import capitalize from 'lodash/capitalize';
24
25
  import CheckBoxIcon from '@material-ui/icons/CheckBox';
25
26
  import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
26
27
  import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
28
+ import Grid$1 from '@material-ui/core/Grid';
29
+ import Step from '@material-ui/core/Step';
30
+ import StepLabel from '@material-ui/core/StepLabel';
31
+ import Stepper from '@material-ui/core/Stepper';
32
+ import { makeStyles as makeStyles$1, createStyles } from '@material-ui/core/styles';
33
+ import Typography$1 from '@material-ui/core/Typography';
34
+ import Cancel from '@material-ui/icons/Cancel';
35
+ import Check from '@material-ui/icons/Check';
36
+ import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
37
+ import classNames from 'classnames';
38
+ import { DateTime, Interval } from 'luxon';
39
+ import useInterval from 'react-use/lib/useInterval';
40
+ import { useImmerReducer } from 'use-immer';
41
+ import LanguageIcon from '@material-ui/icons/Language';
27
42
 
28
43
  const scaffolderApiRef = createApiRef({
29
44
  id: "plugin.scaffolder.service"
@@ -493,11 +508,15 @@ const RepoUrlPickerHost = (props) => {
493
508
  });
494
509
  });
495
510
  useEffect(() => {
496
- if (hosts && !host) {
497
- onChange(hosts[0]);
511
+ if (!host) {
512
+ if (hosts == null ? void 0 : hosts.length) {
513
+ onChange(hosts[0]);
514
+ } else if (integrations == null ? void 0 : integrations.length) {
515
+ onChange(integrations[0].host);
516
+ }
498
517
  }
499
- }, [hosts, host, onChange]);
500
- const hostsOptions = integrations ? integrations.filter((i) => hosts == null ? void 0 : hosts.includes(i.host)).map((i) => ({ label: i.title, value: i.host })) : [{ label: "Loading...", value: "loading" }];
518
+ }, [hosts, host, onChange, integrations]);
519
+ const hostsOptions = integrations ? integrations.filter((i) => (hosts == null ? void 0 : hosts.length) ? hosts == null ? void 0 : hosts.includes(i.host) : true).map((i) => ({ label: i.title, value: i.host })) : [{ label: "Loading...", value: "loading" }];
501
520
  if (loading) {
502
521
  return /* @__PURE__ */ React.createElement(Progress, null);
503
522
  }
@@ -539,32 +558,53 @@ function serializeRepoPickerUrl(data) {
539
558
  return `${data.host}?${params.toString()}`;
540
559
  }
541
560
  function parseRepoPickerUrl(url) {
542
- let host = void 0;
543
- let owner = void 0;
544
- let repoName = void 0;
545
- let organization = void 0;
546
- let workspace = void 0;
547
- let project = void 0;
561
+ let host = "";
562
+ let owner = "";
563
+ let repoName = "";
564
+ let organization = "";
565
+ let workspace = "";
566
+ let project = "";
548
567
  try {
549
568
  if (url) {
550
569
  const parsed = new URL(`https://${url}`);
551
570
  host = parsed.host;
552
- owner = parsed.searchParams.get("owner") || void 0;
553
- repoName = parsed.searchParams.get("repo") || void 0;
554
- organization = parsed.searchParams.get("organization") || void 0;
555
- workspace = parsed.searchParams.get("workspace") || void 0;
556
- project = parsed.searchParams.get("project") || void 0;
571
+ owner = parsed.searchParams.get("owner") || "";
572
+ repoName = parsed.searchParams.get("repo") || "";
573
+ organization = parsed.searchParams.get("organization") || "";
574
+ workspace = parsed.searchParams.get("workspace") || "";
575
+ project = parsed.searchParams.get("project") || "";
557
576
  }
558
577
  } catch {
559
578
  }
560
579
  return { host, owner, repoName, organization, workspace, project };
561
580
  }
562
581
 
582
+ const SecretsContext = createContext(void 0);
583
+ const SecretsContextProvider = ({ children }) => {
584
+ const [secrets, setSecrets] = useState({});
585
+ return /* @__PURE__ */ React.createElement(SecretsContext.Provider, {
586
+ value: { secrets, setSecrets }
587
+ }, children);
588
+ };
589
+ const useTemplateSecrets = () => {
590
+ const value = useContext(SecretsContext);
591
+ if (!value) {
592
+ throw new Error("useTemplateSecrets must be used within a SecretsContextProvider");
593
+ }
594
+ const { setSecrets } = value;
595
+ const setSecret = useCallback((input) => {
596
+ setSecrets((currentSecrets) => ({ ...currentSecrets, ...input }));
597
+ }, [setSecrets]);
598
+ return { setSecret };
599
+ };
600
+
563
601
  const RepoUrlPicker = (props) => {
564
602
  var _a, _b;
565
603
  const { uiSchema, onChange, rawErrors, formData } = props;
566
604
  const [state, setState] = useState(parseRepoPickerUrl(formData));
567
605
  const integrationApi = useApi(scmIntegrationsApiRef);
606
+ const scmAuthApi = useApi(scmAuthApiRef);
607
+ const { setSecret } = useTemplateSecrets();
568
608
  const allowedHosts = useMemo(() => {
569
609
  var _a2, _b2;
570
610
  return (_b2 = (_a2 = uiSchema == null ? void 0 : uiSchema["ui:options"]) == null ? void 0 : _a2.allowedHosts) != null ? _b2 : [];
@@ -584,6 +624,26 @@ const RepoUrlPicker = (props) => {
584
624
  const updateLocalState = useCallback((newState) => {
585
625
  setState((prevState) => ({ ...prevState, ...newState }));
586
626
  }, [setState]);
627
+ useDebounce(async () => {
628
+ var _a2;
629
+ const { requestUserCredentials } = (_a2 = uiSchema == null ? void 0 : uiSchema["ui:options"]) != null ? _a2 : {};
630
+ if (!requestUserCredentials || !(state.host && state.owner && state.repoName)) {
631
+ return;
632
+ }
633
+ const [host, owner, repoName] = [
634
+ state.host,
635
+ state.owner,
636
+ state.repoName
637
+ ].map(encodeURIComponent);
638
+ const { token } = await scmAuthApi.getCredentials({
639
+ url: `https://${host}/${owner}/${repoName}`,
640
+ additionalScope: {
641
+ repoWrite: true,
642
+ customScopes: requestUserCredentials.additionalScopes
643
+ }
644
+ });
645
+ setSecret({ [requestUserCredentials.secretsKey]: token });
646
+ }, 500, [state, uiSchema]);
587
647
  const hostType = (_b = state.host && ((_a = integrationApi.byHost(state.host)) == null ? void 0 : _a.type)) != null ? _b : null;
588
648
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(RepoUrlPickerHost, {
589
649
  host: state.host,
@@ -747,7 +807,7 @@ const OwnerPickerFieldExtension = scaffolderPlugin.provide(createScaffolderField
747
807
  }));
748
808
  const ScaffolderPage = scaffolderPlugin.provide(createRoutableExtension({
749
809
  name: "ScaffolderPage",
750
- component: () => import('./Router-5ca674eb.esm.js').then((m) => m.Router),
810
+ component: () => import('./Router-5df57206.esm.js').then((m) => m.Router),
751
811
  mountPoint: rootRouteRef
752
812
  }));
753
813
  const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide(createScaffolderFieldExtension({
@@ -769,7 +829,7 @@ const WhiteBorderStar = withStyles({
769
829
  color: "#ffffff"
770
830
  }
771
831
  })(StarBorder);
772
- const useStyles$1 = makeStyles((theme) => ({
832
+ const useStyles$3 = makeStyles((theme) => ({
773
833
  starButton: {
774
834
  position: "absolute",
775
835
  top: theme.spacing(0.5),
@@ -780,7 +840,7 @@ const useStyles$1 = makeStyles((theme) => ({
780
840
  const favouriteTemplateTooltip = (isStarred) => isStarred ? "Remove from favorites" : "Add to favorites";
781
841
  const favouriteTemplateIcon = (isStarred) => isStarred ? /* @__PURE__ */ React.createElement(YellowStar, null) : /* @__PURE__ */ React.createElement(WhiteBorderStar, null);
782
842
  const FavouriteTemplate = (props) => {
783
- const classes = useStyles$1();
843
+ const classes = useStyles$3();
784
844
  const { toggleStarredEntity, isStarredEntity } = useStarredEntity(props.entity);
785
845
  return /* @__PURE__ */ React.createElement(IconButton, {
786
846
  color: "inherit",
@@ -792,7 +852,7 @@ const FavouriteTemplate = (props) => {
792
852
  }, favouriteTemplateIcon(isStarredEntity)));
793
853
  };
794
854
 
795
- const useStyles = makeStyles((theme) => ({
855
+ const useStyles$2 = makeStyles((theme) => ({
796
856
  cardHeader: {
797
857
  position: "relative"
798
858
  },
@@ -864,7 +924,7 @@ const TemplateCard = ({ template, deprecated }) => {
864
924
  const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY);
865
925
  const themeId = backstageTheme.getPageTheme({ themeId: templateProps.type }) ? templateProps.type : "other";
866
926
  const theme = backstageTheme.getPageTheme({ themeId });
867
- const classes = useStyles({ backgroundImage: theme.backgroundImage });
927
+ const classes = useStyles$2({ backgroundImage: theme.backgroundImage });
868
928
  const href = generatePath(`${rootLink()}/templates/:templateName`, {
869
929
  templateName: templateProps.name
870
930
  });
@@ -989,5 +1049,407 @@ const TemplateTypePicker = () => {
989
1049
  }));
990
1050
  };
991
1051
 
992
- export { EntityPicker as E, FIELD_EXTENSION_WRAPPER_KEY as F, OwnerPicker as O, RepoUrlPicker as R, ScaffolderClient as S, TemplateTypePicker as T, EntityNamePicker as a, EntityTagsPicker as b, OwnedEntityPicker as c, registerComponentRouteRef as d, entityNamePickerValidation as e, TemplateList as f, rootRouteRef as g, FIELD_EXTENSION_KEY as h, createScaffolderFieldExtension as i, ScaffolderFieldExtensions as j, EntityPickerFieldExtension as k, EntityNamePickerFieldExtension as l, EntityTagsPickerFieldExtension as m, OwnerPickerFieldExtension as n, OwnedEntityPickerFieldExtension as o, RepoUrlPickerFieldExtension as p, ScaffolderPage as q, repoPickerValidation as r, scaffolderApiRef as s, scaffolderPlugin as t, TextValuePicker as u, FavouriteTemplate as v };
993
- //# sourceMappingURL=index-8b5d21c1.esm.js.map
1052
+ function reducer(draft, action) {
1053
+ var _a, _b, _c;
1054
+ switch (action.type) {
1055
+ case "INIT": {
1056
+ draft.steps = action.data.spec.steps.reduce((current, next) => {
1057
+ current[next.id] = { status: "open", id: next.id };
1058
+ return current;
1059
+ }, {});
1060
+ draft.stepLogs = action.data.spec.steps.reduce((current, next) => {
1061
+ current[next.id] = [];
1062
+ return current;
1063
+ }, {});
1064
+ draft.loading = false;
1065
+ draft.error = void 0;
1066
+ draft.completed = false;
1067
+ draft.task = action.data;
1068
+ return;
1069
+ }
1070
+ case "LOGS": {
1071
+ const entries = action.data;
1072
+ for (const entry of entries) {
1073
+ const logLine = `${entry.createdAt} ${entry.body.message}`;
1074
+ if (!entry.body.stepId || !((_a = draft.steps) == null ? void 0 : _a[entry.body.stepId])) {
1075
+ continue;
1076
+ }
1077
+ const currentStepLog = (_b = draft.stepLogs) == null ? void 0 : _b[entry.body.stepId];
1078
+ const currentStep = (_c = draft.steps) == null ? void 0 : _c[entry.body.stepId];
1079
+ if (entry.body.status && entry.body.status !== currentStep.status) {
1080
+ currentStep.status = entry.body.status;
1081
+ if (currentStep.status === "processing") {
1082
+ currentStep.startedAt = entry.createdAt;
1083
+ }
1084
+ if (["cancelled", "failed", "completed"].includes(currentStep.status)) {
1085
+ currentStep.endedAt = entry.createdAt;
1086
+ }
1087
+ }
1088
+ currentStepLog == null ? void 0 : currentStepLog.push(logLine);
1089
+ }
1090
+ return;
1091
+ }
1092
+ case "COMPLETED": {
1093
+ draft.completed = true;
1094
+ draft.output = action.data.body.output;
1095
+ return;
1096
+ }
1097
+ case "ERROR": {
1098
+ draft.error = action.data;
1099
+ draft.loading = false;
1100
+ draft.completed = true;
1101
+ return;
1102
+ }
1103
+ default:
1104
+ return;
1105
+ }
1106
+ }
1107
+ const useTaskEventStream = (taskId) => {
1108
+ const scaffolderApi = useApi(scaffolderApiRef);
1109
+ const [state, dispatch] = useImmerReducer(reducer, {
1110
+ loading: true,
1111
+ completed: false,
1112
+ stepLogs: {},
1113
+ steps: {}
1114
+ });
1115
+ useEffect(() => {
1116
+ let didCancel = false;
1117
+ let subscription;
1118
+ let logPusher;
1119
+ scaffolderApi.getTask(taskId).then((task) => {
1120
+ if (didCancel) {
1121
+ return;
1122
+ }
1123
+ dispatch({ type: "INIT", data: task });
1124
+ const observable = scaffolderApi.streamLogs({ taskId });
1125
+ const collectedLogEvents = new Array();
1126
+ function emitLogs() {
1127
+ if (collectedLogEvents.length) {
1128
+ const logs = collectedLogEvents.splice(0, collectedLogEvents.length);
1129
+ dispatch({ type: "LOGS", data: logs });
1130
+ }
1131
+ }
1132
+ logPusher = setInterval(emitLogs, 500);
1133
+ subscription = observable.subscribe({
1134
+ next: (event) => {
1135
+ switch (event.type) {
1136
+ case "log":
1137
+ return collectedLogEvents.push(event);
1138
+ case "completion":
1139
+ emitLogs();
1140
+ dispatch({ type: "COMPLETED", data: event });
1141
+ return void 0;
1142
+ default:
1143
+ throw new Error(`Unhandled event type ${event.type} in observer`);
1144
+ }
1145
+ },
1146
+ error: (error) => {
1147
+ emitLogs();
1148
+ dispatch({ type: "ERROR", data: error });
1149
+ }
1150
+ });
1151
+ }, (error) => {
1152
+ if (!didCancel) {
1153
+ dispatch({ type: "ERROR", data: error });
1154
+ }
1155
+ });
1156
+ return () => {
1157
+ didCancel = true;
1158
+ if (subscription) {
1159
+ subscription.unsubscribe();
1160
+ }
1161
+ if (logPusher) {
1162
+ clearInterval(logPusher);
1163
+ }
1164
+ };
1165
+ }, [scaffolderApi, dispatch, taskId]);
1166
+ return state;
1167
+ };
1168
+
1169
+ const useStyles$1 = makeStyles({
1170
+ svgIcon: {
1171
+ display: "inline-block",
1172
+ "& svg": {
1173
+ display: "inline-block",
1174
+ fontSize: "inherit",
1175
+ verticalAlign: "baseline"
1176
+ }
1177
+ }
1178
+ });
1179
+ const IconLink = (props) => {
1180
+ const { href, text, Icon, ...linkProps } = props;
1181
+ const classes = useStyles$1();
1182
+ return /* @__PURE__ */ React.createElement(Grid, {
1183
+ container: true,
1184
+ direction: "row",
1185
+ spacing: 1
1186
+ }, /* @__PURE__ */ React.createElement(Grid, {
1187
+ item: true
1188
+ }, /* @__PURE__ */ React.createElement(Typography, {
1189
+ component: "div",
1190
+ className: classes.svgIcon
1191
+ }, Icon ? /* @__PURE__ */ React.createElement(Icon, null) : /* @__PURE__ */ React.createElement(LanguageIcon, null))), /* @__PURE__ */ React.createElement(Grid, {
1192
+ item: true
1193
+ }, /* @__PURE__ */ React.createElement(Link$1, {
1194
+ to: href,
1195
+ ...linkProps
1196
+ }, text || href)));
1197
+ };
1198
+
1199
+ const TaskPageLinks = ({ output }) => {
1200
+ const { entityRef: entityRefOutput, remoteUrl } = output;
1201
+ let { links = [] } = output;
1202
+ const app = useApp();
1203
+ const entityRoute = useRouteRef(entityRouteRef);
1204
+ const iconResolver = (key) => {
1205
+ var _a;
1206
+ return key ? (_a = app.getSystemIcon(key)) != null ? _a : LanguageIcon : LanguageIcon;
1207
+ };
1208
+ if (remoteUrl) {
1209
+ links = [{ url: remoteUrl, title: "Repo" }, ...links];
1210
+ }
1211
+ if (entityRefOutput) {
1212
+ links = [
1213
+ {
1214
+ entityRef: entityRefOutput,
1215
+ title: "Open in catalog",
1216
+ icon: "catalog"
1217
+ },
1218
+ ...links
1219
+ ];
1220
+ }
1221
+ return /* @__PURE__ */ React.createElement(Box, {
1222
+ px: 3,
1223
+ pb: 3
1224
+ }, links.filter(({ url, entityRef }) => url || entityRef).map(({ url, entityRef, title, icon }) => {
1225
+ if (entityRef) {
1226
+ const entityName = parseEntityName(entityRef);
1227
+ const target = entityRoute(entityName);
1228
+ return { title, icon, url: target };
1229
+ }
1230
+ return { title, icon, url };
1231
+ }).map(({ url, title, icon }, i) => /* @__PURE__ */ React.createElement(IconLink, {
1232
+ key: `output-link-${i}`,
1233
+ href: url,
1234
+ text: title != null ? title : url,
1235
+ Icon: iconResolver(icon),
1236
+ target: "_blank"
1237
+ })));
1238
+ };
1239
+
1240
+ const humanizeDuration = require("humanize-duration");
1241
+ const useStyles = makeStyles$1((theme) => createStyles({
1242
+ root: {
1243
+ width: "100%"
1244
+ },
1245
+ button: {
1246
+ marginBottom: theme.spacing(2),
1247
+ marginLeft: theme.spacing(2)
1248
+ },
1249
+ actionsContainer: {
1250
+ marginBottom: theme.spacing(2)
1251
+ },
1252
+ resetContainer: {
1253
+ padding: theme.spacing(3)
1254
+ },
1255
+ labelWrapper: {
1256
+ display: "flex",
1257
+ flex: 1,
1258
+ flexDirection: "row",
1259
+ justifyContent: "space-between"
1260
+ },
1261
+ stepWrapper: {
1262
+ width: "100%"
1263
+ }
1264
+ }));
1265
+ const StepTimeTicker = ({ step }) => {
1266
+ const [time, setTime] = useState("");
1267
+ useInterval(() => {
1268
+ if (!step.startedAt) {
1269
+ setTime("");
1270
+ return;
1271
+ }
1272
+ const end = step.endedAt ? DateTime.fromISO(step.endedAt) : DateTime.local();
1273
+ const startedAt = DateTime.fromISO(step.startedAt);
1274
+ const formatted = Interval.fromDateTimes(startedAt, end).toDuration().valueOf();
1275
+ setTime(humanizeDuration(formatted, { round: true }));
1276
+ }, 1e3);
1277
+ return /* @__PURE__ */ React.createElement(Typography$1, {
1278
+ variant: "caption"
1279
+ }, time);
1280
+ };
1281
+ const useStepIconStyles = makeStyles$1((theme) => createStyles({
1282
+ root: {
1283
+ color: theme.palette.text.disabled,
1284
+ display: "flex",
1285
+ height: 22,
1286
+ alignItems: "center"
1287
+ },
1288
+ completed: {
1289
+ color: theme.palette.status.ok
1290
+ },
1291
+ error: {
1292
+ color: theme.palette.status.error
1293
+ }
1294
+ }));
1295
+ function TaskStepIconComponent(props) {
1296
+ const classes = useStepIconStyles();
1297
+ const { active, completed, error } = props;
1298
+ const getMiddle = () => {
1299
+ if (active) {
1300
+ return /* @__PURE__ */ React.createElement(CircularProgress, {
1301
+ size: "24px"
1302
+ });
1303
+ }
1304
+ if (completed) {
1305
+ return /* @__PURE__ */ React.createElement(Check, null);
1306
+ }
1307
+ if (error) {
1308
+ return /* @__PURE__ */ React.createElement(Cancel, null);
1309
+ }
1310
+ return /* @__PURE__ */ React.createElement(FiberManualRecordIcon, null);
1311
+ };
1312
+ return /* @__PURE__ */ React.createElement("div", {
1313
+ className: classNames(classes.root, {
1314
+ [classes.completed]: completed,
1315
+ [classes.error]: error
1316
+ })
1317
+ }, getMiddle());
1318
+ }
1319
+ const TaskStatusStepper = memo(({
1320
+ steps,
1321
+ currentStepId,
1322
+ onUserStepChange
1323
+ }) => {
1324
+ const classes = useStyles();
1325
+ return /* @__PURE__ */ React.createElement("div", {
1326
+ className: classes.root
1327
+ }, /* @__PURE__ */ React.createElement(Stepper, {
1328
+ activeStep: steps.findIndex((s) => s.id === currentStepId),
1329
+ orientation: "vertical",
1330
+ nonLinear: true
1331
+ }, steps.map((step, index) => {
1332
+ const isCompleted = step.status === "completed";
1333
+ const isFailed = step.status === "failed";
1334
+ const isActive = step.status === "processing";
1335
+ const isSkipped = step.status === "skipped";
1336
+ return /* @__PURE__ */ React.createElement(Step, {
1337
+ key: String(index),
1338
+ expanded: true
1339
+ }, /* @__PURE__ */ React.createElement(StepButton, {
1340
+ onClick: () => onUserStepChange(step.id)
1341
+ }, /* @__PURE__ */ React.createElement(StepLabel, {
1342
+ StepIconProps: {
1343
+ completed: isCompleted,
1344
+ error: isFailed,
1345
+ active: isActive
1346
+ },
1347
+ StepIconComponent: TaskStepIconComponent,
1348
+ className: classes.stepWrapper
1349
+ }, /* @__PURE__ */ React.createElement("div", {
1350
+ className: classes.labelWrapper
1351
+ }, /* @__PURE__ */ React.createElement(Typography$1, {
1352
+ variant: "subtitle2"
1353
+ }, step.name), isSkipped ? /* @__PURE__ */ React.createElement(Typography$1, {
1354
+ variant: "caption"
1355
+ }, "Skipped") : /* @__PURE__ */ React.createElement(StepTimeTicker, {
1356
+ step
1357
+ })))));
1358
+ })));
1359
+ });
1360
+ const hasLinks = ({ entityRef, remoteUrl, links = [] }) => !!(entityRef || remoteUrl || links.length > 0);
1361
+ const TaskPage = ({ loadingText }) => {
1362
+ const classes = useStyles();
1363
+ const navigate = useNavigate();
1364
+ const rootLink = useRouteRef(rootRouteRef);
1365
+ const [userSelectedStepId, setUserSelectedStepId] = useState(void 0);
1366
+ const [lastActiveStepId, setLastActiveStepId] = useState(void 0);
1367
+ const { taskId } = useParams();
1368
+ const taskStream = useTaskEventStream(taskId);
1369
+ const completed = taskStream.completed;
1370
+ const steps = useMemo(() => {
1371
+ var _a, _b;
1372
+ return (_b = (_a = taskStream.task) == null ? void 0 : _a.spec.steps.map((step) => {
1373
+ var _a2;
1374
+ return {
1375
+ ...step,
1376
+ ...(_a2 = taskStream == null ? void 0 : taskStream.steps) == null ? void 0 : _a2[step.id]
1377
+ };
1378
+ })) != null ? _b : [];
1379
+ }, [taskStream]);
1380
+ useEffect(() => {
1381
+ var _a;
1382
+ const mostRecentFailedOrActiveStep = steps.find((step) => ["failed", "processing"].includes(step.status));
1383
+ if (completed && !mostRecentFailedOrActiveStep) {
1384
+ setLastActiveStepId((_a = steps[steps.length - 1]) == null ? void 0 : _a.id);
1385
+ return;
1386
+ }
1387
+ setLastActiveStepId(mostRecentFailedOrActiveStep == null ? void 0 : mostRecentFailedOrActiveStep.id);
1388
+ }, [steps, completed]);
1389
+ const currentStepId = userSelectedStepId != null ? userSelectedStepId : lastActiveStepId;
1390
+ const logAsString = useMemo(() => {
1391
+ if (!currentStepId) {
1392
+ return loadingText ? loadingText : "Loading...";
1393
+ }
1394
+ const log = taskStream.stepLogs[currentStepId];
1395
+ if (!(log == null ? void 0 : log.length)) {
1396
+ return "Waiting for logs...";
1397
+ }
1398
+ return log.join("\n");
1399
+ }, [taskStream.stepLogs, currentStepId, loadingText]);
1400
+ const taskNotFound = taskStream.completed === true && taskStream.loading === false && !taskStream.task;
1401
+ const { output } = taskStream;
1402
+ const handleStartOver = () => {
1403
+ var _a, _b;
1404
+ if (!taskStream.task || !((_b = (_a = taskStream.task) == null ? void 0 : _a.spec.metadata) == null ? void 0 : _b.name)) {
1405
+ navigate(generatePath(rootLink()));
1406
+ }
1407
+ const formData = taskStream.task.spec.apiVersion === "backstage.io/v1beta2" ? taskStream.task.spec.values : taskStream.task.spec.parameters;
1408
+ navigate(generatePath(`${rootLink()}/templates/:templateName?${qs.stringify({
1409
+ formData: JSON.stringify(formData)
1410
+ })}`, {
1411
+ templateName: taskStream.task.spec.metadata.name
1412
+ }));
1413
+ };
1414
+ return /* @__PURE__ */ React.createElement(Page, {
1415
+ themeId: "home"
1416
+ }, /* @__PURE__ */ React.createElement(Header, {
1417
+ pageTitleOverride: `Task ${taskId}`,
1418
+ title: /* @__PURE__ */ React.createElement(React.Fragment, null, "Task Activity ", /* @__PURE__ */ React.createElement(Lifecycle, {
1419
+ alpha: true,
1420
+ shorthand: true
1421
+ })),
1422
+ subtitle: `Activity for task: ${taskId}`
1423
+ }), /* @__PURE__ */ React.createElement(Content, null, taskNotFound ? /* @__PURE__ */ React.createElement(ErrorPage, {
1424
+ status: "404",
1425
+ statusMessage: "Task not found",
1426
+ additionalInfo: "No task found with this ID"
1427
+ }) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(Grid$1, {
1428
+ container: true
1429
+ }, /* @__PURE__ */ React.createElement(Grid$1, {
1430
+ item: true,
1431
+ xs: 3
1432
+ }, /* @__PURE__ */ React.createElement(Paper, null, /* @__PURE__ */ React.createElement(TaskStatusStepper, {
1433
+ steps,
1434
+ currentStepId,
1435
+ onUserStepChange: setUserSelectedStepId
1436
+ }), output && hasLinks(output) && /* @__PURE__ */ React.createElement(TaskPageLinks, {
1437
+ output
1438
+ }), /* @__PURE__ */ React.createElement(Button$1, {
1439
+ className: classes.button,
1440
+ onClick: handleStartOver,
1441
+ disabled: !completed,
1442
+ variant: "contained",
1443
+ color: "primary"
1444
+ }, "Start Over"))), /* @__PURE__ */ React.createElement(Grid$1, {
1445
+ item: true,
1446
+ xs: 9
1447
+ }, !currentStepId && /* @__PURE__ */ React.createElement(Progress, null), /* @__PURE__ */ React.createElement("div", {
1448
+ style: { height: "80vh" }
1449
+ }, /* @__PURE__ */ React.createElement(LogViewer, {
1450
+ text: logAsString
1451
+ })))))));
1452
+ };
1453
+
1454
+ export { EntityPicker as E, FIELD_EXTENSION_WRAPPER_KEY as F, OwnerPicker as O, RepoUrlPicker as R, SecretsContext as S, TemplateTypePicker as T, EntityNamePicker as a, EntityTagsPicker as b, OwnedEntityPicker as c, registerComponentRouteRef as d, entityNamePickerValidation as e, TemplateList as f, rootRouteRef as g, FIELD_EXTENSION_KEY as h, SecretsContextProvider as i, TaskPage as j, ScaffolderClient as k, createScaffolderFieldExtension as l, ScaffolderFieldExtensions as m, EntityPickerFieldExtension as n, EntityNamePickerFieldExtension as o, EntityTagsPickerFieldExtension as p, OwnerPickerFieldExtension as q, repoPickerValidation as r, scaffolderApiRef as s, OwnedEntityPickerFieldExtension as t, RepoUrlPickerFieldExtension as u, ScaffolderPage as v, scaffolderPlugin as w, TextValuePicker as x, FavouriteTemplate as y, useTemplateSecrets as z };
1455
+ //# sourceMappingURL=index-71578268.esm.js.map