@backstage/plugin-scaffolder 0.11.12 → 0.11.16

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,19 @@ 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';
6
- import { TextField, 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 } 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';
7
7
  import FormControl from '@material-ui/core/FormControl';
8
8
  import Autocomplete from '@material-ui/lab/Autocomplete';
9
- import React, { useCallback, useEffect } from 'react';
10
- import { useAsync } from 'react-use';
11
- import { KubernetesValidatorFunctions, RELATION_OWNED_BY, stringifyEntityRef } from '@backstage/catalog-model';
9
+ import React, { useCallback, useEffect, useState } from 'react';
10
+ import { useAsync, useEffectOnce } from 'react-use';
11
+ import { KubernetesValidatorFunctions, makeValidator, RELATION_OWNED_BY, stringifyEntityRef } from '@backstage/catalog-model';
12
+ import { Autocomplete as Autocomplete$1 } from '@material-ui/lab';
13
+ import { Progress, Select, ItemCardHeader, Button, ContentHeader, WarningPanel, Link as Link$1, Content, ItemCardGrid } from '@backstage/core-components';
12
14
  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
15
  import FormHelperText from '@material-ui/core/FormHelperText';
17
- import { Progress, ItemCardHeader, Button, WarningPanel, ItemCardGrid } from '@backstage/core-components';
16
+ import Input from '@material-ui/core/Input';
17
+ import InputLabel from '@material-ui/core/InputLabel';
18
18
  import Star from '@material-ui/icons/Star';
19
19
  import StarBorder from '@material-ui/icons/StarBorder';
20
20
  import WarningIcon from '@material-ui/icons/Warning';
@@ -23,11 +23,9 @@ import capitalize from 'lodash/capitalize';
23
23
  import CheckBoxIcon from '@material-ui/icons/CheckBox';
24
24
  import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
25
25
  import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
26
- 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,7 +84,7 @@ 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
90
  throw await ResponseError.fromResponse(response);
@@ -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,7 +168,7 @@ 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
174
  throw await ResponseError.fromResponse(response);
@@ -179,9 +177,13 @@ class ScaffolderClient {
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
 
@@ -257,8 +265,70 @@ const entityNamePickerValidation = (value, validation) => {
257
265
  }
258
266
  };
259
267
 
268
+ const EntityTagsPicker = ({
269
+ formData,
270
+ onChange,
271
+ uiSchema
272
+ }) => {
273
+ var _a;
274
+ const catalogApi = useApi(catalogApiRef);
275
+ const [inputValue, setInputValue] = useState("");
276
+ const [inputError, setInputError] = useState(false);
277
+ const tagValidator = makeValidator().isValidTag;
278
+ const kinds = (_a = uiSchema["ui:options"]) == null ? void 0 : _a.kinds;
279
+ const { loading, value: existingTags } = useAsync(async () => {
280
+ const tagsRequest = { fields: ["metadata.tags"] };
281
+ if (kinds) {
282
+ tagsRequest.filter = { kind: kinds };
283
+ }
284
+ const entities = await catalogApi.getEntities(tagsRequest);
285
+ return [
286
+ ...new Set(entities.items.flatMap((e) => {
287
+ var _a2;
288
+ return (_a2 = e.metadata) == null ? void 0 : _a2.tags;
289
+ }).filter(Boolean))
290
+ ].sort();
291
+ });
292
+ const setTags = (_, values) => {
293
+ let hasError = false;
294
+ let addDuplicate = false;
295
+ const currentTags = formData || [];
296
+ if ((values == null ? void 0 : values.length) && currentTags.length < values.length) {
297
+ const newTag = values[values.length - 1] = values[values.length - 1].toLocaleLowerCase("en-US").trim();
298
+ hasError = !tagValidator(newTag);
299
+ addDuplicate = currentTags.indexOf(newTag) !== -1;
300
+ }
301
+ setInputError(hasError);
302
+ setInputValue(!hasError ? "" : inputValue);
303
+ if (!hasError && !addDuplicate) {
304
+ onChange(values || []);
305
+ }
306
+ };
307
+ useEffectOnce(() => onChange(formData || []));
308
+ return /* @__PURE__ */ React.createElement(FormControl$1, {
309
+ margin: "normal"
310
+ }, /* @__PURE__ */ React.createElement(Autocomplete$1, {
311
+ multiple: true,
312
+ freeSolo: true,
313
+ filterSelectedOptions: true,
314
+ onChange: setTags,
315
+ value: formData || [],
316
+ inputValue,
317
+ loading,
318
+ options: existingTags || [],
319
+ ChipProps: { size: "small" },
320
+ renderInput: (params) => /* @__PURE__ */ React.createElement(TextField, {
321
+ ...params,
322
+ label: "Tags",
323
+ onChange: (e) => setInputValue(e.target.value),
324
+ error: inputError,
325
+ helperText: "Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters"
326
+ })
327
+ }));
328
+ };
329
+
260
330
  const OwnerPicker = ({
261
- schema: {title = "Owner", description = "The owner of the component"},
331
+ schema: { title = "Owner", description = "The owner of the component" },
262
332
  uiSchema,
263
333
  ...props
264
334
  }) => {
@@ -275,12 +345,12 @@ const OwnerPicker = ({
275
345
  };
276
346
  return /* @__PURE__ */ React.createElement(EntityPicker, {
277
347
  ...props,
278
- schema: {title, description},
348
+ schema: { title, description },
279
349
  uiSchema: ownerUiSchema
280
350
  });
281
351
  };
282
352
 
283
- function splitFormData(url) {
353
+ function splitFormData(url, allowedOwners) {
284
354
  let host = void 0;
285
355
  let owner = void 0;
286
356
  let repo = void 0;
@@ -291,7 +361,7 @@ function splitFormData(url) {
291
361
  if (url) {
292
362
  const parsed = new URL(`https://${url}`);
293
363
  host = parsed.host;
294
- owner = parsed.searchParams.get("owner") || void 0;
364
+ owner = parsed.searchParams.get("owner") || (allowedOwners == null ? void 0 : allowedOwners[0]);
295
365
  repo = parsed.searchParams.get("repo") || void 0;
296
366
  organization = parsed.searchParams.get("organization") || void 0;
297
367
  workspace = parsed.searchParams.get("workspace") || void 0;
@@ -299,7 +369,7 @@ function splitFormData(url) {
299
369
  }
300
370
  } catch {
301
371
  }
302
- return {host, owner, repo, organization, workspace, project};
372
+ return { host, owner, repo, organization, workspace, project };
303
373
  }
304
374
  function serializeFormData(data) {
305
375
  if (!data.host) {
@@ -329,17 +399,18 @@ const RepoUrlPicker = ({
329
399
  rawErrors,
330
400
  formData
331
401
  }) => {
332
- var _a, _b, _c;
402
+ var _a, _b, _c, _d, _e;
333
403
  const scaffolderApi = useApi(scaffolderApiRef);
334
404
  const integrationApi = useApi(scmIntegrationsApiRef);
335
405
  const allowedHosts = (_a = uiSchema["ui:options"]) == null ? void 0 : _a.allowedHosts;
336
- const {value: integrations, loading} = useAsync(async () => {
337
- return await scaffolderApi.getIntegrationsList({allowedHosts});
406
+ const allowedOwners = (_b = uiSchema["ui:options"]) == null ? void 0 : _b.allowedOwners;
407
+ const { value: integrations, loading } = useAsync(async () => {
408
+ return await scaffolderApi.getIntegrationsList({ allowedHosts });
338
409
  });
339
- const {host, owner, repo, organization, workspace, project} = splitFormData(formData);
340
- const updateHost = useCallback((evt) => {
410
+ const { host, owner, repo, organization, workspace, project } = splitFormData(formData, allowedOwners);
411
+ const updateHost = useCallback((value) => {
341
412
  onChange(serializeFormData({
342
- host: evt.target.value,
413
+ host: value,
343
414
  owner,
344
415
  repo,
345
416
  organization,
@@ -347,6 +418,14 @@ const RepoUrlPicker = ({
347
418
  project
348
419
  }));
349
420
  }, [onChange, owner, repo, organization, workspace, project]);
421
+ const updateOwnerSelect = useCallback((value) => onChange(serializeFormData({
422
+ host,
423
+ owner: value,
424
+ repo,
425
+ organization,
426
+ workspace,
427
+ project
428
+ })), [onChange, host, repo, organization, workspace, project]);
350
429
  const updateOwner = useCallback((evt) => onChange(serializeFormData({
351
430
  host,
352
431
  owner: evt.target.value,
@@ -411,21 +490,20 @@ const RepoUrlPicker = ({
411
490
  if (loading) {
412
491
  return /* @__PURE__ */ React.createElement(Progress, null);
413
492
  }
493
+ 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" }];
494
+ const ownersOptions = allowedOwners ? allowedOwners.map((i) => ({ label: i, value: i })) : [{ label: "Loading...", value: "loading" }];
414
495
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(FormControl, {
415
496
  margin: "normal",
416
497
  required: true,
417
498
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !host
418
- }, /* @__PURE__ */ React.createElement(InputLabel, {
419
- htmlFor: "hostInput"
420
- }, "Host"), /* @__PURE__ */ React.createElement(Select, {
499
+ }, /* @__PURE__ */ React.createElement(Select, {
421
500
  native: true,
422
- id: "hostInput",
501
+ disabled: hostsOptions.length === 1,
502
+ label: "Host",
423
503
  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, {
504
+ selected: host,
505
+ items: hostsOptions
506
+ }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The host where the repository will be created")), host === "dev.azure.com" && /* @__PURE__ */ React.createElement(FormControl, {
429
507
  margin: "normal",
430
508
  required: true,
431
509
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !organization
@@ -435,7 +513,7 @@ const RepoUrlPicker = ({
435
513
  id: "repoInput",
436
514
  onChange: updateOrganization,
437
515
  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, {
516
+ }), /* @__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
517
  margin: "normal",
440
518
  required: true,
441
519
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !workspace
@@ -455,7 +533,7 @@ const RepoUrlPicker = ({
455
533
  id: "wokrspaceInput",
456
534
  onChange: updateProject,
457
535
  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, {
536
+ }), /* @__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
537
  margin: "normal",
460
538
  required: true,
461
539
  error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !owner
@@ -465,6 +543,17 @@ const RepoUrlPicker = ({
465
543
  id: "ownerInput",
466
544
  onChange: updateOwner,
467
545
  value: owner
546
+ }), /* @__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, {
547
+ margin: "normal",
548
+ required: true,
549
+ error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !owner
550
+ }, /* @__PURE__ */ React.createElement(Select, {
551
+ native: true,
552
+ label: "Owner Available",
553
+ onChange: updateOwnerSelect,
554
+ disabled: ownersOptions.length === 1,
555
+ selected: owner,
556
+ items: ownersOptions
468
557
  }), /* @__PURE__ */ React.createElement(FormHelperText, null, "The organization, user or project that this repo will belong to"))), /* @__PURE__ */ React.createElement(FormControl, {
469
558
  margin: "normal",
470
559
  required: true,
@@ -481,7 +570,7 @@ const RepoUrlPicker = ({
481
570
  const repoPickerValidation = (value, validation, context) => {
482
571
  var _a;
483
572
  try {
484
- const {host, searchParams} = new URL(`https://${value}`);
573
+ const { host, searchParams } = new URL(`https://${value}`);
485
574
  const integrationApi = context.apiHolder.get(scmIntegrationsApiRef);
486
575
  if (!host) {
487
576
  validation.addError("Incomplete repository location provided, host not provided");
@@ -507,6 +596,47 @@ const repoPickerValidation = (value, validation, context) => {
507
596
  }
508
597
  };
509
598
 
599
+ const OwnedEntityPicker = ({
600
+ onChange,
601
+ schema: { title = "Entity", description = "An entity from the catalog" },
602
+ required,
603
+ uiSchema,
604
+ rawErrors,
605
+ formData,
606
+ idSchema
607
+ }) => {
608
+ var _a, _b;
609
+ const allowedKinds = (_a = uiSchema["ui:options"]) == null ? void 0 : _a.allowedKinds;
610
+ const defaultKind = (_b = uiSchema["ui:options"]) == null ? void 0 : _b.defaultKind;
611
+ const { ownedEntities, loading } = useOwnedEntities(allowedKinds);
612
+ const entityRefs = ownedEntities == null ? void 0 : ownedEntities.items.map((e) => formatEntityRefTitle(e, { defaultKind })).filter((n) => n);
613
+ const onSelect = (_, value) => {
614
+ onChange(value || "");
615
+ };
616
+ return /* @__PURE__ */ React.createElement(FormControl, {
617
+ margin: "normal",
618
+ required,
619
+ error: (rawErrors == null ? void 0 : rawErrors.length) > 0 && !formData
620
+ }, /* @__PURE__ */ React.createElement(Autocomplete, {
621
+ id: idSchema == null ? void 0 : idSchema.$id,
622
+ value: formData || "",
623
+ loading,
624
+ onChange: onSelect,
625
+ options: entityRefs || [],
626
+ autoSelect: true,
627
+ freeSolo: true,
628
+ renderInput: (params) => /* @__PURE__ */ React.createElement(TextField, {
629
+ ...params,
630
+ label: title,
631
+ margin: "normal",
632
+ helperText: description,
633
+ variant: "outlined",
634
+ required,
635
+ InputProps: params.InputProps
636
+ })
637
+ }));
638
+ };
639
+
510
640
  const FIELD_EXTENSION_WRAPPER_KEY = "scaffolder.extensions.wrapper.v1";
511
641
  const FIELD_EXTENSION_KEY = "scaffolder.extensions.field.v1";
512
642
  function createScaffolderFieldExtension(options) {
@@ -539,7 +669,7 @@ const scaffolderPlugin = createPlugin({
539
669
  identityApi: identityApiRef,
540
670
  scmIntegrationsApi: scmIntegrationsApiRef
541
671
  },
542
- factory: ({discoveryApi, identityApi, scmIntegrationsApi}) => new ScaffolderClient({discoveryApi, identityApi, scmIntegrationsApi})
672
+ factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) => new ScaffolderClient({ discoveryApi, identityApi, scmIntegrationsApi })
543
673
  })
544
674
  ],
545
675
  routes: {
@@ -569,9 +699,17 @@ const OwnerPickerFieldExtension = scaffolderPlugin.provide(createScaffolderField
569
699
  }));
570
700
  const ScaffolderPage = scaffolderPlugin.provide(createRoutableExtension({
571
701
  name: "ScaffolderPage",
572
- component: () => import('./Router-5919d585.esm.js').then((m) => m.Router),
702
+ component: () => import('./Router-a8e778fd.esm.js').then((m) => m.Router),
573
703
  mountPoint: rootRouteRef
574
704
  }));
705
+ const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide(createScaffolderFieldExtension({
706
+ component: OwnedEntityPicker,
707
+ name: "OwnedEntityPicker"
708
+ }));
709
+ const EntityTagsPickerFieldExtension = scaffolderPlugin.provide(createScaffolderFieldExtension({
710
+ component: EntityTagsPicker,
711
+ name: "EntityTagsPicker"
712
+ }));
575
713
 
576
714
  const YellowStar = withStyles({
577
715
  root: {
@@ -595,7 +733,7 @@ const favouriteTemplateTooltip = (isStarred) => isStarred ? "Remove from favorit
595
733
  const favouriteTemplateIcon = (isStarred) => isStarred ? /* @__PURE__ */ React.createElement(YellowStar, null) : /* @__PURE__ */ React.createElement(WhiteBorderStar, null);
596
734
  const FavouriteTemplate = (props) => {
597
735
  const classes = useStyles$1();
598
- const {toggleStarredEntity, isStarredEntity} = useStarredEntity(props.entity);
736
+ const { toggleStarredEntity, isStarredEntity } = useStarredEntity(props.entity);
599
737
  return /* @__PURE__ */ React.createElement(IconButton, {
600
738
  color: "inherit",
601
739
  className: classes.starButton,
@@ -611,7 +749,7 @@ const useStyles = makeStyles((theme) => ({
611
749
  position: "relative"
612
750
  },
613
751
  title: {
614
- backgroundImage: ({backgroundImage}) => backgroundImage
752
+ backgroundImage: ({ backgroundImage }) => backgroundImage
615
753
  },
616
754
  box: {
617
755
  overflow: "hidden",
@@ -659,7 +797,7 @@ const getTemplateCardProps = (template) => {
659
797
  const DeprecationWarning = () => {
660
798
  const styles = useDeprecationStyles();
661
799
  const Title = /* @__PURE__ */ React.createElement(Typography, {
662
- style: {padding: 10, maxWidth: 300}
800
+ style: { padding: 10, maxWidth: 300 }
663
801
  }, "This template syntax is deprecated. Click for more info.");
664
802
  return /* @__PURE__ */ React.createElement("div", {
665
803
  className: styles.deprecationIcon
@@ -670,15 +808,15 @@ const DeprecationWarning = () => {
670
808
  className: styles.link
671
809
  }, /* @__PURE__ */ React.createElement(WarningIcon, null))));
672
810
  };
673
- const TemplateCard = ({template, deprecated}) => {
811
+ const TemplateCard = ({ template, deprecated }) => {
674
812
  var _a;
675
813
  const backstageTheme = useTheme();
676
814
  const rootLink = useRouteRef(rootRouteRef);
677
815
  const templateProps = getTemplateCardProps(template);
678
816
  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});
817
+ const themeId = backstageTheme.getPageTheme({ themeId: templateProps.type }) ? templateProps.type : "other";
818
+ const theme = backstageTheme.getPageTheme({ themeId });
819
+ const classes = useStyles({ backgroundImage: theme.backgroundImage });
682
820
  const href = generatePath(`${rootLink()}/templates/:templateName`, {
683
821
  templateName: templateProps.name
684
822
  });
@@ -691,9 +829,9 @@ const TemplateCard = ({template, deprecated}) => {
691
829
  }), deprecated && /* @__PURE__ */ React.createElement(DeprecationWarning, null), /* @__PURE__ */ React.createElement(ItemCardHeader, {
692
830
  title: templateProps.title,
693
831
  subtitle: templateProps.type,
694
- classes: {root: classes.title}
832
+ classes: { root: classes.title }
695
833
  })), /* @__PURE__ */ React.createElement(CardContent, {
696
- style: {display: "grid"}
834
+ style: { display: "grid" }
697
835
  }, /* @__PURE__ */ React.createElement(Box, {
698
836
  className: classes.box
699
837
  }, /* @__PURE__ */ React.createElement(Typography, {
@@ -726,19 +864,31 @@ const TemplateCard = ({template, deprecated}) => {
726
864
  }, "Choose")));
727
865
  };
728
866
 
729
- const TemplateList = ({TemplateCardComponent}) => {
730
- const {loading, error, entities} = useEntityListProvider();
867
+ const TemplateList = ({
868
+ TemplateCardComponent,
869
+ group
870
+ }) => {
871
+ const { loading, error, entities } = useEntityListProvider();
731
872
  const Card = TemplateCardComponent || TemplateCard;
873
+ const maybeFilteredEntities = group ? entities.filter((e) => group.filter(e)) : entities;
874
+ const title = group ? group.titleComponent || /* @__PURE__ */ React.createElement(ContentHeader, {
875
+ title: group.title
876
+ }) : /* @__PURE__ */ React.createElement(ContentHeader, {
877
+ title: "Other Templates"
878
+ });
879
+ if (group && maybeFilteredEntities.length === 0) {
880
+ return null;
881
+ }
732
882
  return /* @__PURE__ */ React.createElement(React.Fragment, null, loading && /* @__PURE__ */ React.createElement(Progress, null), error && /* @__PURE__ */ React.createElement(WarningPanel, {
733
883
  title: "Oops! Something went wrong loading the templates"
734
884
  }, error.message), !error && !loading && !entities.length && /* @__PURE__ */ React.createElement(Typography, {
735
885
  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, {
886
+ }, "No templates found that match your filter. Learn more about", " ", /* @__PURE__ */ React.createElement(Link$1, {
887
+ to: "https://backstage.io/docs/features/software-templates/adding-templates"
888
+ }, "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
889
  key: stringifyEntityRef(template),
740
890
  template
741
- }))));
891
+ })))));
742
892
  };
743
893
 
744
894
  const icon = /* @__PURE__ */ React.createElement(CheckBoxOutlineBlankIcon, {
@@ -749,7 +899,7 @@ const checkedIcon = /* @__PURE__ */ React.createElement(CheckBoxIcon, {
749
899
  });
750
900
  const TemplateTypePicker = () => {
751
901
  const alertApi = useApi(alertApiRef);
752
- const {error, loading, availableTypes, selectedTypes, setSelectedTypes} = useEntityTypeFilter();
902
+ const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = useEntityTypeFilter();
753
903
  if (loading)
754
904
  return /* @__PURE__ */ React.createElement(Progress, null);
755
905
  if (!availableTypes)
@@ -772,7 +922,7 @@ const TemplateTypePicker = () => {
772
922
  options: availableTypes,
773
923
  value: selectedTypes,
774
924
  onChange: (_, value) => setSelectedTypes(value),
775
- renderOption: (option, {selected}) => /* @__PURE__ */ React.createElement(FormControlLabel, {
925
+ renderOption: (option, { selected }) => /* @__PURE__ */ React.createElement(FormControlLabel, {
776
926
  control: /* @__PURE__ */ React.createElement(Checkbox, {
777
927
  icon,
778
928
  checkedIcon,
@@ -791,5 +941,5 @@ const TemplateTypePicker = () => {
791
941
  }));
792
942
  };
793
943
 
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-9d7df4e7.esm.js.map
944
+ 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 };
945
+ //# sourceMappingURL=index-f378943f.esm.js.map