@backstage/plugin-scaffolder 0.11.11 → 0.11.15

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.
@@ -2,19 +2,18 @@ import { createApiRef, useApi, attachComponentData, createExternalRouteRef, crea
2
2
  import { ResponseError } from '@backstage/errors';
3
3
  import qs from 'qs';
4
4
  import ObservableImpl from 'zen-observable';
5
- import { catalogApiRef, formatEntityRefTitle, useStarredEntity, getEntityRelations, getEntitySourceLocation, EntityRefLinks, useEntityListProvider, useEntityTypeFilter } from '@backstage/plugin-catalog-react';
5
+ import { catalogApiRef, formatEntityRefTitle, useOwnedEntities, useStarredEntity, getEntityRelations, getEntitySourceLocation, EntityRefLinks, useEntityListProvider, useEntityTypeFilter } from '@backstage/plugin-catalog-react';
6
6
  import { TextField, withStyles, makeStyles, IconButton, Tooltip, useTheme, Card, CardMedia, CardContent, Box, Typography, Chip, CardActions, Link, FormControlLabel, Checkbox } from '@material-ui/core';
7
7
  import FormControl from '@material-ui/core/FormControl';
8
8
  import Autocomplete from '@material-ui/lab/Autocomplete';
9
9
  import React, { useCallback, useEffect } from 'react';
10
10
  import { useAsync } from 'react-use';
11
11
  import { KubernetesValidatorFunctions, RELATION_OWNED_BY, stringifyEntityRef } from '@backstage/catalog-model';
12
+ import { Progress, Select, ItemCardHeader, Button, ContentHeader, WarningPanel, Link as Link$1, Content, ItemCardGrid } from '@backstage/core-components';
12
13
  import { scmIntegrationsApiRef, ScmIntegrationIcon } from '@backstage/integration-react';
13
- import Select from '@material-ui/core/Select';
14
- import InputLabel from '@material-ui/core/InputLabel';
15
- import Input from '@material-ui/core/Input';
16
14
  import FormHelperText from '@material-ui/core/FormHelperText';
17
- import { Progress, ItemCardHeader, Button, WarningPanel, ItemCardGrid } from '@backstage/core-components';
15
+ import Input from '@material-ui/core/Input';
16
+ import InputLabel from '@material-ui/core/InputLabel';
18
17
  import Star from '@material-ui/icons/Star';
19
18
  import StarBorder from '@material-ui/icons/StarBorder';
20
19
  import WarningIcon from '@material-ui/icons/Warning';
@@ -26,8 +25,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
26
25
  import { Autocomplete as Autocomplete$1 } from '@material-ui/lab';
27
26
 
28
27
  const scaffolderApiRef = createApiRef({
29
- id: "plugin.scaffolder.service",
30
- description: "Used to make requests towards the scaffolder backend"
28
+ id: "plugin.scaffolder.service"
31
29
  });
32
30
  class ScaffolderClient {
33
31
  constructor(options) {
@@ -43,17 +41,17 @@ class ScaffolderClient {
43
41
  ...this.scmIntegrationsApi.bitbucket.list(),
44
42
  ...this.scmIntegrationsApi.github.list(),
45
43
  ...this.scmIntegrationsApi.gitlab.list()
46
- ].map((c) => ({type: c.type, title: c.title, host: c.config.host})).filter((c) => options.allowedHosts.includes(c.host));
44
+ ].map((c) => ({ type: c.type, title: c.title, host: c.config.host })).filter((c) => options.allowedHosts.includes(c.host));
47
45
  }
48
46
  async getTemplateParameterSchema(templateName) {
49
- const {namespace, kind, name} = templateName;
47
+ const { namespace, kind, name } = templateName;
50
48
  const token = await this.identityApi.getIdToken();
51
49
  const baseUrl = await this.discoveryApi.getBaseUrl("scaffolder");
52
50
  const templatePath = [namespace, kind, name].map((s) => encodeURIComponent(s)).join("/");
53
51
  const url = `${baseUrl}/v2/templates/${templatePath}/parameter-schema`;
54
52
  const response = await fetch(url, {
55
53
  headers: {
56
- ...token && {Authorization: `Bearer ${token}`}
54
+ ...token && { Authorization: `Bearer ${token}` }
57
55
  }
58
56
  });
59
57
  if (!response.ok) {
@@ -69,16 +67,16 @@ class ScaffolderClient {
69
67
  method: "POST",
70
68
  headers: {
71
69
  "Content-Type": "application/json",
72
- ...token && {Authorization: `Bearer ${token}`}
70
+ ...token && { Authorization: `Bearer ${token}` }
73
71
  },
74
- body: JSON.stringify({templateName, values: {...values}})
72
+ body: JSON.stringify({ templateName, values: { ...values } })
75
73
  });
76
74
  if (response.status !== 201) {
77
75
  const status = `${response.status} ${response.statusText}`;
78
76
  const body = await response.text();
79
77
  throw new Error(`Backend request failed, ${status} ${body.trim()}`);
80
78
  }
81
- const {id} = await response.json();
79
+ const { id } = await response.json();
82
80
  return id;
83
81
  }
84
82
  async getTask(taskId) {
@@ -86,10 +84,10 @@ class ScaffolderClient {
86
84
  const baseUrl = await this.discoveryApi.getBaseUrl("scaffolder");
87
85
  const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`;
88
86
  const response = await fetch(url, {
89
- headers: token ? {Authorization: `Bearer ${token}`} : {}
87
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
90
88
  });
91
89
  if (!response.ok) {
92
- throw ResponseError.fromResponse(response);
90
+ throw await ResponseError.fromResponse(response);
93
91
  }
94
92
  return await response.json();
95
93
  }
@@ -110,7 +108,7 @@ class ScaffolderClient {
110
108
  }
111
109
  this.discoveryApi.getBaseUrl("scaffolder").then((baseUrl) => {
112
110
  const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/eventstream`;
113
- const eventSource = new EventSource(url, {withCredentials: true});
111
+ const eventSource = new EventSource(url, { withCredentials: true });
114
112
  eventSource.addEventListener("log", (event) => {
115
113
  if (event.data) {
116
114
  try {
@@ -147,7 +145,7 @@ class ScaffolderClient {
147
145
  return new ObservableImpl((subscriber) => {
148
146
  this.discoveryApi.getBaseUrl("scaffolder").then(async (baseUrl) => {
149
147
  while (!subscriber.closed) {
150
- const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/events?${qs.stringify({after})}`;
148
+ const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/events?${qs.stringify({ after })}`;
151
149
  const response = await fetch(url);
152
150
  if (!response.ok) {
153
151
  await new Promise((resolve) => setTimeout(resolve, 1e3));
@@ -170,18 +168,22 @@ class ScaffolderClient {
170
168
  const baseUrl = await this.discoveryApi.getBaseUrl("scaffolder");
171
169
  const token = await this.identityApi.getIdToken();
172
170
  const response = await fetch(`${baseUrl}/v2/actions`, {
173
- headers: token ? {Authorization: `Bearer ${token}`} : {}
171
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
174
172
  });
175
173
  if (!response.ok) {
176
- throw ResponseError.fromResponse(response);
174
+ throw await ResponseError.fromResponse(response);
177
175
  }
178
176
  return await response.json();
179
177
  }
180
178
  }
181
179
 
180
+ const allowArbitraryValues = (uiSchema) => {
181
+ var _a, _b;
182
+ return (_b = (_a = uiSchema["ui:options"]) == null ? void 0 : _a.allowArbitraryValues) != null ? _b : true;
183
+ };
182
184
  const EntityPicker = ({
183
185
  onChange,
184
- schema: {title = "Entity", description = "An entity from the catalog"},
186
+ schema: { title = "Entity", description = "An entity from the catalog" },
185
187
  required,
186
188
  uiSchema,
187
189
  rawErrors,
@@ -192,23 +194,29 @@ const EntityPicker = ({
192
194
  const allowedKinds = (_a = uiSchema["ui:options"]) == null ? void 0 : _a.allowedKinds;
193
195
  const defaultKind = (_b = uiSchema["ui:options"]) == null ? void 0 : _b.defaultKind;
194
196
  const catalogApi = useApi(catalogApiRef);
195
- const {value: entities, loading} = useAsync(() => catalogApi.getEntities(allowedKinds ? {filter: {kind: allowedKinds}} : void 0));
196
- const entityRefs = entities == null ? void 0 : entities.items.map((e) => formatEntityRefTitle(e, {defaultKind}));
197
- const onSelect = (_, value) => {
197
+ const { value: entities, loading } = useAsync(() => catalogApi.getEntities(allowedKinds ? { filter: { kind: allowedKinds } } : void 0));
198
+ const entityRefs = entities == null ? void 0 : entities.items.map((e) => formatEntityRefTitle(e, { defaultKind }));
199
+ const onSelect = useCallback((_, value) => {
198
200
  onChange(value || "");
199
- };
201
+ }, [onChange]);
202
+ useEffect(() => {
203
+ if ((entityRefs == null ? void 0 : entityRefs.length) === 1) {
204
+ onChange(entityRefs[0]);
205
+ }
206
+ }, [entityRefs, onChange]);
200
207
  return /* @__PURE__ */ React.createElement(FormControl, {
201
208
  margin: "normal",
202
209
  required,
203
210
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !formData
204
211
  }, /* @__PURE__ */ React.createElement(Autocomplete, {
212
+ disabled: (entityRefs == null ? void 0 : entityRefs.length) === 1,
205
213
  id: idSchema == null ? void 0 : idSchema.$id,
206
214
  value: formData || "",
207
215
  loading,
208
216
  onChange: onSelect,
209
217
  options: entityRefs || [],
210
218
  autoSelect: true,
211
- freeSolo: true,
219
+ freeSolo: allowArbitraryValues(uiSchema),
212
220
  renderInput: (params) => /* @__PURE__ */ React.createElement(TextField, {
213
221
  ...params,
214
222
  label: title,
@@ -224,10 +232,10 @@ const EntityPicker = ({
224
232
  const TextValuePicker = ({
225
233
  onChange,
226
234
  required,
227
- schema: {title, description},
235
+ schema: { title, description },
228
236
  rawErrors,
229
237
  formData,
230
- uiSchema: {"ui:autofocus": autoFocus},
238
+ uiSchema: { "ui:autofocus": autoFocus },
231
239
  idSchema,
232
240
  placeholder
233
241
  }) => /* @__PURE__ */ React.createElement(TextField, {
@@ -237,17 +245,17 @@ const TextValuePicker = ({
237
245
  helperText: description,
238
246
  required,
239
247
  value: formData != null ? formData : "",
240
- onChange: ({target: {value}}) => onChange(value),
248
+ onChange: ({ target: { value } }) => onChange(value),
241
249
  margin: "normal",
242
250
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !formData,
243
- inputProps: {autoFocus}
251
+ inputProps: { autoFocus }
244
252
  });
245
253
 
246
254
  const EntityNamePicker = ({
247
- schema: {title = "Name", description = "Unique name of the component"},
255
+ schema: { title = "Name", description = "Unique name of the component" },
248
256
  ...props
249
257
  }) => /* @__PURE__ */ React.createElement(TextValuePicker, {
250
- schema: {title, description},
258
+ schema: { title, description },
251
259
  ...props
252
260
  });
253
261
 
@@ -258,7 +266,7 @@ const entityNamePickerValidation = (value, validation) => {
258
266
  };
259
267
 
260
268
  const OwnerPicker = ({
261
- schema: {title = "Owner", description = "The owner of the component"},
269
+ schema: { title = "Owner", description = "The owner of the component" },
262
270
  uiSchema,
263
271
  ...props
264
272
  }) => {
@@ -275,12 +283,12 @@ const OwnerPicker = ({
275
283
  };
276
284
  return /* @__PURE__ */ React.createElement(EntityPicker, {
277
285
  ...props,
278
- schema: {title, description},
286
+ schema: { title, description },
279
287
  uiSchema: ownerUiSchema
280
288
  });
281
289
  };
282
290
 
283
- function splitFormData(url) {
291
+ function splitFormData(url, allowedOwners) {
284
292
  let host = void 0;
285
293
  let owner = void 0;
286
294
  let repo = void 0;
@@ -291,7 +299,7 @@ function splitFormData(url) {
291
299
  if (url) {
292
300
  const parsed = new URL(`https://${url}`);
293
301
  host = parsed.host;
294
- owner = parsed.searchParams.get("owner") || void 0;
302
+ owner = parsed.searchParams.get("owner") || (allowedOwners == null ? void 0 : allowedOwners[0]);
295
303
  repo = parsed.searchParams.get("repo") || void 0;
296
304
  organization = parsed.searchParams.get("organization") || void 0;
297
305
  workspace = parsed.searchParams.get("workspace") || void 0;
@@ -299,7 +307,7 @@ function splitFormData(url) {
299
307
  }
300
308
  } catch {
301
309
  }
302
- return {host, owner, repo, organization, workspace, project};
310
+ return { host, owner, repo, organization, workspace, project };
303
311
  }
304
312
  function serializeFormData(data) {
305
313
  if (!data.host) {
@@ -329,17 +337,18 @@ const RepoUrlPicker = ({
329
337
  rawErrors,
330
338
  formData
331
339
  }) => {
332
- var _a, _b, _c;
340
+ var _a, _b, _c, _d, _e;
333
341
  const scaffolderApi = useApi(scaffolderApiRef);
334
342
  const integrationApi = useApi(scmIntegrationsApiRef);
335
343
  const allowedHosts = (_a = uiSchema["ui:options"]) == null ? void 0 : _a.allowedHosts;
336
- const {value: integrations, loading} = useAsync(async () => {
337
- return await scaffolderApi.getIntegrationsList({allowedHosts});
344
+ const allowedOwners = (_b = uiSchema["ui:options"]) == null ? void 0 : _b.allowedOwners;
345
+ const { value: integrations, loading } = useAsync(async () => {
346
+ return await scaffolderApi.getIntegrationsList({ allowedHosts });
338
347
  });
339
- const {host, owner, repo, organization, workspace, project} = splitFormData(formData);
340
- const updateHost = useCallback((evt) => {
348
+ const { host, owner, repo, organization, workspace, project } = splitFormData(formData, allowedOwners);
349
+ const updateHost = useCallback((value) => {
341
350
  onChange(serializeFormData({
342
- host: evt.target.value,
351
+ host: value,
343
352
  owner,
344
353
  repo,
345
354
  organization,
@@ -347,6 +356,14 @@ const RepoUrlPicker = ({
347
356
  project
348
357
  }));
349
358
  }, [onChange, owner, repo, organization, workspace, project]);
359
+ const updateOwnerSelect = useCallback((value) => onChange(serializeFormData({
360
+ host,
361
+ owner: value,
362
+ repo,
363
+ organization,
364
+ workspace,
365
+ project
366
+ })), [onChange, host, repo, organization, workspace, project]);
350
367
  const updateOwner = useCallback((evt) => onChange(serializeFormData({
351
368
  host,
352
369
  owner: evt.target.value,
@@ -411,21 +428,20 @@ const RepoUrlPicker = ({
411
428
  if (loading) {
412
429
  return /* @__PURE__ */ React.createElement(Progress, null);
413
430
  }
431
+ const hostsOptions = integrations ? integrations.filter((i) => allowedHosts == null ? void 0 : allowedHosts.includes(i.host)).map((i) => ({ label: i.title, value: i.host })) : [{ label: "Loading...", value: "loading" }];
432
+ const ownersOptions = allowedOwners ? allowedOwners.map((i) => ({ label: i, value: i })) : [{ label: "Loading...", value: "loading" }];
414
433
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(FormControl, {
415
434
  margin: "normal",
416
435
  required: true,
417
436
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !host
418
- }, /* @__PURE__ */ React.createElement(InputLabel, {
419
- htmlFor: "hostInput"
420
- }, "Host"), /* @__PURE__ */ React.createElement(Select, {
437
+ }, /* @__PURE__ */ React.createElement(Select, {
421
438
  native: true,
422
- id: "hostInput",
439
+ disabled: hostsOptions.length === 1,
440
+ label: "Host",
423
441
  onChange: updateHost,
424
- value: host
425
- }, integrations ? integrations.filter((i) => allowedHosts == null ? void 0 : allowedHosts.includes(i.host)).map((i) => /* @__PURE__ */ React.createElement("option", {
426
- key: i.host,
427
- value: i.host
428
- }, i.title)) : /* @__PURE__ */ React.createElement("p", null, "loading")), /* @__PURE__ */ React.createElement(FormHelperText, null, "The host where the repository will be created")), host === "dev.azure.com" && /* @__PURE__ */ React.createElement(FormControl, {
442
+ selected: host,
443
+ items: hostsOptions
444
+ }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The host where the repository will be created")), host === "dev.azure.com" && /* @__PURE__ */ React.createElement(FormControl, {
429
445
  margin: "normal",
430
446
  required: true,
431
447
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !organization
@@ -435,7 +451,7 @@ const RepoUrlPicker = ({
435
451
  id: "repoInput",
436
452
  onChange: updateOrganization,
437
453
  value: organization
438
- }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The name of the organization")), host && ((_b = integrationApi.byHost(host)) == null ? void 0 : _b.type) === "bitbucket" && /* @__PURE__ */ React.createElement(React.Fragment, null, host === "bitbucket.org" && /* @__PURE__ */ React.createElement(FormControl, {
454
+ }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The name of the organization")), host && ((_c = integrationApi.byHost(host)) == null ? void 0 : _c.type) === "bitbucket" && /* @__PURE__ */ React.createElement(React.Fragment, null, host === "bitbucket.org" && /* @__PURE__ */ React.createElement(FormControl, {
439
455
  margin: "normal",
440
456
  required: true,
441
457
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !workspace
@@ -455,7 +471,7 @@ const RepoUrlPicker = ({
455
471
  id: "wokrspaceInput",
456
472
  onChange: updateProject,
457
473
  value: project
458
- }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The project where the repository will be created"))), host && ((_c = integrationApi.byHost(host)) == null ? void 0 : _c.type) !== "bitbucket" && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(FormControl, {
474
+ }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The project where the repository will be created"))), host && ((_d = integrationApi.byHost(host)) == null ? void 0 : _d.type) !== "bitbucket" && !allowedOwners && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(FormControl, {
459
475
  margin: "normal",
460
476
  required: true,
461
477
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !owner
@@ -465,6 +481,17 @@ const RepoUrlPicker = ({
465
481
  id: "ownerInput",
466
482
  onChange: updateOwner,
467
483
  value: owner
484
+ }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The organization, user or project that this repo will belong to"))), host && ((_e = integrationApi.byHost(host)) == null ? void 0 : _e.type) !== "bitbucket" && allowedOwners && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(FormControl, {
485
+ margin: "normal",
486
+ required: true,
487
+ error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !owner
488
+ }, /* @__PURE__ */ React.createElement(Select, {
489
+ native: true,
490
+ label: "Owner Available",
491
+ onChange: updateOwnerSelect,
492
+ disabled: ownersOptions.length === 1,
493
+ selected: owner,
494
+ items: ownersOptions
468
495
  }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The organization, user or project that this repo will belong to"))), /* @__PURE__ */ React.createElement(FormControl, {
469
496
  margin: "normal",
470
497
  required: true,
@@ -481,7 +508,7 @@ const RepoUrlPicker = ({
481
508
  const repoPickerValidation = (value, validation, context) => {
482
509
  var _a;
483
510
  try {
484
- const {host, searchParams} = new URL(`https://${value}`);
511
+ const { host, searchParams } = new URL(`https://${value}`);
485
512
  const integrationApi = context.apiHolder.get(scmIntegrationsApiRef);
486
513
  if (!host) {
487
514
  validation.addError("Incomplete repository location provided, host not provided");
@@ -507,6 +534,47 @@ const repoPickerValidation = (value, validation, context) => {
507
534
  }
508
535
  };
509
536
 
537
+ const OwnedEntityPicker = ({
538
+ onChange,
539
+ schema: { title = "Entity", description = "An entity from the catalog" },
540
+ required,
541
+ uiSchema,
542
+ rawErrors,
543
+ formData,
544
+ idSchema
545
+ }) => {
546
+ var _a, _b;
547
+ const allowedKinds = (_a = uiSchema["ui:options"]) == null ? void 0 : _a.allowedKinds;
548
+ const defaultKind = (_b = uiSchema["ui:options"]) == null ? void 0 : _b.defaultKind;
549
+ const { ownedEntities, loading } = useOwnedEntities(allowedKinds);
550
+ const entityRefs = ownedEntities == null ? void 0 : ownedEntities.items.map((e) => formatEntityRefTitle(e, { defaultKind })).filter((n) => n);
551
+ const onSelect = (_, value) => {
552
+ onChange(value || "");
553
+ };
554
+ return /* @__PURE__ */ React.createElement(FormControl, {
555
+ margin: "normal",
556
+ required,
557
+ error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !formData
558
+ }, /* @__PURE__ */ React.createElement(Autocomplete, {
559
+ id: idSchema == null ? void 0 : idSchema.$id,
560
+ value: formData || "",
561
+ loading,
562
+ onChange: onSelect,
563
+ options: entityRefs || [],
564
+ autoSelect: true,
565
+ freeSolo: true,
566
+ renderInput: (params) => /* @__PURE__ */ React.createElement(TextField, {
567
+ ...params,
568
+ label: title,
569
+ margin: "normal",
570
+ helperText: description,
571
+ variant: "outlined",
572
+ required,
573
+ InputProps: params.InputProps
574
+ })
575
+ }));
576
+ };
577
+
510
578
  const FIELD_EXTENSION_WRAPPER_KEY = "scaffolder.extensions.wrapper.v1";
511
579
  const FIELD_EXTENSION_KEY = "scaffolder.extensions.field.v1";
512
580
  function createScaffolderFieldExtension(options) {
@@ -539,7 +607,7 @@ const scaffolderPlugin = createPlugin({
539
607
  identityApi: identityApiRef,
540
608
  scmIntegrationsApi: scmIntegrationsApiRef
541
609
  },
542
- factory: ({discoveryApi, identityApi, scmIntegrationsApi}) => new ScaffolderClient({discoveryApi, identityApi, scmIntegrationsApi})
610
+ factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) => new ScaffolderClient({ discoveryApi, identityApi, scmIntegrationsApi })
543
611
  })
544
612
  ],
545
613
  routes: {
@@ -569,9 +637,13 @@ const OwnerPickerFieldExtension = scaffolderPlugin.provide(createScaffolderField
569
637
  }));
570
638
  const ScaffolderPage = scaffolderPlugin.provide(createRoutableExtension({
571
639
  name: "ScaffolderPage",
572
- component: () => import('./Router-de99a5d2.esm.js').then((m) => m.Router),
640
+ component: () => import('./Router-6eee3fa9.esm.js').then((m) => m.Router),
573
641
  mountPoint: rootRouteRef
574
642
  }));
643
+ const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide(createScaffolderFieldExtension({
644
+ component: OwnedEntityPicker,
645
+ name: "OwnedEntityPicker"
646
+ }));
575
647
 
576
648
  const YellowStar = withStyles({
577
649
  root: {
@@ -595,7 +667,7 @@ const favouriteTemplateTooltip = (isStarred) => isStarred ? "Remove from favorit
595
667
  const favouriteTemplateIcon = (isStarred) => isStarred ? /* @__PURE__ */ React.createElement(YellowStar, null) : /* @__PURE__ */ React.createElement(WhiteBorderStar, null);
596
668
  const FavouriteTemplate = (props) => {
597
669
  const classes = useStyles$1();
598
- const {toggleStarredEntity, isStarredEntity} = useStarredEntity(props.entity);
670
+ const { toggleStarredEntity, isStarredEntity } = useStarredEntity(props.entity);
599
671
  return /* @__PURE__ */ React.createElement(IconButton, {
600
672
  color: "inherit",
601
673
  className: classes.starButton,
@@ -611,7 +683,7 @@ const useStyles = makeStyles((theme) => ({
611
683
  position: "relative"
612
684
  },
613
685
  title: {
614
- backgroundImage: ({backgroundImage}) => backgroundImage
686
+ backgroundImage: ({ backgroundImage }) => backgroundImage
615
687
  },
616
688
  box: {
617
689
  overflow: "hidden",
@@ -659,7 +731,7 @@ const getTemplateCardProps = (template) => {
659
731
  const DeprecationWarning = () => {
660
732
  const styles = useDeprecationStyles();
661
733
  const Title = /* @__PURE__ */ React.createElement(Typography, {
662
- style: {padding: 10, maxWidth: 300}
734
+ style: { padding: 10, maxWidth: 300 }
663
735
  }, "This template syntax is deprecated. Click for more info.");
664
736
  return /* @__PURE__ */ React.createElement("div", {
665
737
  className: styles.deprecationIcon
@@ -670,15 +742,15 @@ const DeprecationWarning = () => {
670
742
  className: styles.link
671
743
  }, /* @__PURE__ */ React.createElement(WarningIcon, null))));
672
744
  };
673
- const TemplateCard = ({template, deprecated}) => {
745
+ const TemplateCard = ({ template, deprecated }) => {
674
746
  var _a;
675
747
  const backstageTheme = useTheme();
676
748
  const rootLink = useRouteRef(rootRouteRef);
677
749
  const templateProps = getTemplateCardProps(template);
678
750
  const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY);
679
- const themeId = backstageTheme.getPageTheme({themeId: templateProps.type}) ? templateProps.type : "other";
680
- const theme = backstageTheme.getPageTheme({themeId});
681
- const classes = useStyles({backgroundImage: theme.backgroundImage});
751
+ const themeId = backstageTheme.getPageTheme({ themeId: templateProps.type }) ? templateProps.type : "other";
752
+ const theme = backstageTheme.getPageTheme({ themeId });
753
+ const classes = useStyles({ backgroundImage: theme.backgroundImage });
682
754
  const href = generatePath(`${rootLink()}/templates/:templateName`, {
683
755
  templateName: templateProps.name
684
756
  });
@@ -691,9 +763,9 @@ const TemplateCard = ({template, deprecated}) => {
691
763
  }), deprecated && /* @__PURE__ */ React.createElement(DeprecationWarning, null), /* @__PURE__ */ React.createElement(ItemCardHeader, {
692
764
  title: templateProps.title,
693
765
  subtitle: templateProps.type,
694
- classes: {root: classes.title}
766
+ classes: { root: classes.title }
695
767
  })), /* @__PURE__ */ React.createElement(CardContent, {
696
- style: {display: "grid"}
768
+ style: { display: "grid" }
697
769
  }, /* @__PURE__ */ React.createElement(Box, {
698
770
  className: classes.box
699
771
  }, /* @__PURE__ */ React.createElement(Typography, {
@@ -726,19 +798,31 @@ const TemplateCard = ({template, deprecated}) => {
726
798
  }, "Choose")));
727
799
  };
728
800
 
729
- const TemplateList = ({TemplateCardComponent}) => {
730
- const {loading, error, entities} = useEntityListProvider();
801
+ const TemplateList = ({
802
+ TemplateCardComponent,
803
+ group
804
+ }) => {
805
+ const { loading, error, entities } = useEntityListProvider();
731
806
  const Card = TemplateCardComponent || TemplateCard;
807
+ const maybeFilteredEntities = group ? entities.filter((e) => group.filter(e)) : entities;
808
+ const title = group ? group.titleComponent || /* @__PURE__ */ React.createElement(ContentHeader, {
809
+ title: group.title
810
+ }) : /* @__PURE__ */ React.createElement(ContentHeader, {
811
+ title: "Other Templates"
812
+ });
813
+ if (group && maybeFilteredEntities.length === 0) {
814
+ return null;
815
+ }
732
816
  return /* @__PURE__ */ React.createElement(React.Fragment, null, loading && /* @__PURE__ */ React.createElement(Progress, null), error && /* @__PURE__ */ React.createElement(WarningPanel, {
733
817
  title: "Oops! Something went wrong loading the templates"
734
818
  }, error.message), !error && !loading && !entities.length && /* @__PURE__ */ React.createElement(Typography, {
735
819
  variant: "body2"
736
- }, "No templates found that match your filter. Learn more about", " ", /* @__PURE__ */ React.createElement(Link, {
737
- href: "https://backstage.io/docs/features/software-templates/adding-templates"
738
- }, "adding templates"), "."), /* @__PURE__ */ React.createElement(ItemCardGrid, null, entities && (entities == null ? void 0 : entities.length) > 0 && entities.map((template) => /* @__PURE__ */ React.createElement(Card, {
820
+ }, "No templates found that match your filter. Learn more about", " ", /* @__PURE__ */ React.createElement(Link$1, {
821
+ to: "https://backstage.io/docs/features/software-templates/adding-templates"
822
+ }, "adding templates"), "."), /* @__PURE__ */ React.createElement(Content, null, title, /* @__PURE__ */ React.createElement(ItemCardGrid, null, maybeFilteredEntities && (maybeFilteredEntities == null ? void 0 : maybeFilteredEntities.length) > 0 && maybeFilteredEntities.map((template) => /* @__PURE__ */ React.createElement(Card, {
739
823
  key: stringifyEntityRef(template),
740
824
  template
741
- }))));
825
+ })))));
742
826
  };
743
827
 
744
828
  const icon = /* @__PURE__ */ React.createElement(CheckBoxOutlineBlankIcon, {
@@ -749,7 +833,7 @@ const checkedIcon = /* @__PURE__ */ React.createElement(CheckBoxIcon, {
749
833
  });
750
834
  const TemplateTypePicker = () => {
751
835
  const alertApi = useApi(alertApiRef);
752
- const {error, loading, availableTypes, selectedTypes, setSelectedTypes} = useEntityTypeFilter();
836
+ const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = useEntityTypeFilter();
753
837
  if (loading)
754
838
  return /* @__PURE__ */ React.createElement(Progress, null);
755
839
  if (!availableTypes)
@@ -772,7 +856,7 @@ const TemplateTypePicker = () => {
772
856
  options: availableTypes,
773
857
  value: selectedTypes,
774
858
  onChange: (_, value) => setSelectedTypes(value),
775
- renderOption: (option, {selected}) => /* @__PURE__ */ React.createElement(FormControlLabel, {
859
+ renderOption: (option, { selected }) => /* @__PURE__ */ React.createElement(FormControlLabel, {
776
860
  control: /* @__PURE__ */ React.createElement(Checkbox, {
777
861
  icon,
778
862
  checkedIcon,
@@ -791,5 +875,5 @@ const TemplateTypePicker = () => {
791
875
  }));
792
876
  };
793
877
 
794
- 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, registerComponentRouteRef as b, TemplateList as c, rootRouteRef as d, entityNamePickerValidation as e, FIELD_EXTENSION_KEY as f, createScaffolderFieldExtension as g, ScaffolderFieldExtensions as h, EntityPickerFieldExtension as i, EntityNamePickerFieldExtension as j, OwnerPickerFieldExtension as k, RepoUrlPickerFieldExtension as l, ScaffolderPage as m, scaffolderPlugin as n, TextValuePicker as o, FavouriteTemplate as p, repoPickerValidation as r, scaffolderApiRef as s };
795
- //# sourceMappingURL=index-8b883a7e.esm.js.map
878
+ 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, OwnedEntityPicker as b, registerComponentRouteRef as c, TemplateList as d, entityNamePickerValidation as e, rootRouteRef as f, FIELD_EXTENSION_KEY as g, createScaffolderFieldExtension as h, ScaffolderFieldExtensions as i, EntityPickerFieldExtension as j, EntityNamePickerFieldExtension as k, OwnerPickerFieldExtension as l, OwnedEntityPickerFieldExtension as m, RepoUrlPickerFieldExtension as n, ScaffolderPage as o, scaffolderPlugin as p, TextValuePicker as q, repoPickerValidation as r, scaffolderApiRef as s, FavouriteTemplate as t };
879
+ //# sourceMappingURL=index-210d6da0.esm.js.map