@backstage/plugin-catalog-react 1.4.1-next.2 → 1.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # @backstage/plugin-catalog-react
2
2
 
3
+ ## 1.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a49fb39df5a: Attempt to load entity owner names in the EntityOwnerPicker through the `by-refs` endpoint. If `spec.profile.displayName` or `metadata.title` are populated, we now attempt to show those before showing the `humanizeEntityRef`'d version.
8
+
9
+ **BREAKING**: `EntityOwnerFilter` now uses the full entity ref instead of the `humanizeEntityRef`. If you rely on `EntityOwnerFilter.values` or the `queryParameters.owners` of `useEntityList`, you will need to adjust your code like the following.
10
+
11
+ ```tsx
12
+ const { queryParameters: { owners } } = useEntityList();
13
+ // or
14
+ const { filter: { owners } } = useEntityList();
15
+
16
+ // Instead of,
17
+ if (owners.some(ref => ref === humanizeEntityRef(myEntity))) ...
18
+
19
+ // You'll need to use,
20
+ if (owners.some(ref => ref === stringifyEntityRef(myEntity))) ...
21
+ ```
22
+
23
+ ### Patch Changes
24
+
25
+ - 81bee24c5de: Fixed bug in catalog filters where you could not click on the text to select a value.
26
+ - 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
27
+ - d1f5324dff7: Reverted the check if the selected options list is different than the query parameters list before invoking `setSelectedOptions` method. This was preventing updating list items when a query string was already present in the URL when loading the page.
28
+ - 2898b6c8d52: Minor type tweaks for TypeScript 5.0
29
+ - e0c6e8b9c3c: Update peer dependencies
30
+ - Updated dependencies
31
+ - @backstage/core-components@0.13.0
32
+ - @backstage/catalog-client@1.4.1
33
+ - @backstage/plugin-permission-common@0.7.5
34
+ - @backstage/theme@0.2.19
35
+ - @backstage/core-plugin-api@1.5.1
36
+ - @backstage/catalog-model@1.3.0
37
+ - @backstage/plugin-permission-react@0.4.12
38
+ - @backstage/version-bridge@1.0.4
39
+ - @backstage/integration@1.4.4
40
+ - @backstage/errors@1.1.5
41
+ - @backstage/types@1.0.2
42
+ - @backstage/plugin-catalog-common@1.0.13
43
+
44
+ ## 1.5.0-next.3
45
+
46
+ ### Minor Changes
47
+
48
+ - a49fb39df5a: Attempt to load entity owner names in the EntityOwnerPicker through the `by-refs` endpoint. If `spec.profile.displayName` or `metadata.title` are populated, we now attempt to show those before showing the `humanizeEntityRef`'d version.
49
+
50
+ **BREAKING**: `EntityOwnerFilter` now uses the full entity ref instead of the `humanizeEntityRef`. If you rely on `EntityOwnerFilter.values` or the `queryParameters.owners` of `useEntityList`, you will need to adjust your code like the following.
51
+
52
+ ```tsx
53
+ const { queryParameters: { owners } } = useEntityList();
54
+ // or
55
+ const { filter: { owners } } = useEntityList();
56
+
57
+ // Instead of,
58
+ if (owners.some(ref => ref === humanizeEntityRef(myEntity))) ...
59
+
60
+ // You'll need to use,
61
+ if (owners.some(ref => ref === stringifyEntityRef(myEntity))) ...
62
+ ```
63
+
64
+ ### Patch Changes
65
+
66
+ - d1f5324dff7: Reverted the check if the selected options list is different than the query parameters list before invoking `setSelectedOptions` method. This was preventing updating list items when a query string was already present in the URL when loading the page.
67
+ - Updated dependencies
68
+ - @backstage/catalog-model@1.3.0-next.0
69
+ - @backstage/core-components@0.13.0-next.3
70
+ - @backstage/catalog-client@1.4.1-next.1
71
+ - @backstage/core-plugin-api@1.5.1-next.1
72
+ - @backstage/errors@1.1.5
73
+ - @backstage/integration@1.4.4-next.0
74
+ - @backstage/theme@0.2.19-next.0
75
+ - @backstage/types@1.0.2
76
+ - @backstage/version-bridge@1.0.4-next.0
77
+ - @backstage/plugin-catalog-common@1.0.13-next.1
78
+ - @backstage/plugin-permission-common@0.7.5-next.0
79
+ - @backstage/plugin-permission-react@0.4.12-next.1
80
+
3
81
  ## 1.4.1-next.2
4
82
 
5
83
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-react",
3
- "version": "1.4.1-next.2",
3
+ "version": "1.5.0",
4
4
  "main": "../dist/alpha.esm.js",
5
5
  "module": "../dist/alpha.esm.js",
6
6
  "types": "../dist/alpha.d.ts"
package/dist/index.d.ts CHANGED
@@ -430,11 +430,17 @@ declare class EntityTextFilter implements EntityFilter {
430
430
  /**
431
431
  * Filter matching entities that are owned by group.
432
432
  * @public
433
+ *
434
+ * CAUTION: This class may contain both full and partial entity refs.
433
435
  */
434
436
  declare class EntityOwnerFilter implements EntityFilter {
435
437
  readonly values: string[];
436
438
  constructor(values: string[]);
437
439
  filterEntity(entity: Entity): boolean;
440
+ /**
441
+ * Get the URL query parameter value. May be a mix of full and humanized entity refs.
442
+ * @returns list of entity refs.
443
+ */
438
444
  toQueryValue(): string[];
439
445
  }
440
446
  /**
package/dist/index.esm.js CHANGED
@@ -1,21 +1,20 @@
1
1
  export { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
2
- import { createApiRef, createRouteRef, useRouteRef, useApi, identityApiRef, alertApiRef, useApiHolder, useApp, configApiRef } from '@backstage/core-plugin-api';
2
+ import { createApiRef, useApi, identityApiRef, alertApiRef, errorApiRef, createRouteRef, useRouteRef, useApiHolder, useApp, configApiRef } from '@backstage/core-plugin-api';
3
3
  import ObservableImpl from 'zen-observable';
4
- import React, { useState, forwardRef, createContext, useMemo, useCallback, useContext, useEffect, useRef, memo, useLayoutEffect, Fragment } from 'react';
5
- import { Grid, useMediaQuery, useTheme, Button, Drawer, Box, Typography, Tooltip, makeStyles, FormControlLabel, Checkbox, TextField, IconButton, Card, CardContent, Chip, CardActions, Toolbar, FormControl, Input, InputAdornment, withStyles, DialogContentText, ListItemText as ListItemText$1, ListSubheader as ListSubheader$1, ListItem, ListItemIcon, List, Dialog, DialogTitle, DialogContent, Tabs, Tab, DialogActions, Divider, MenuItem, ListItemSecondaryAction } from '@material-ui/core';
4
+ import React, { useState, createContext, useMemo, useCallback, useContext, useEffect, useRef, forwardRef, memo, useLayoutEffect, Fragment } from 'react';
5
+ import { Grid, useMediaQuery, useTheme, Button, Drawer, Box, Typography, makeStyles, FormControlLabel, Checkbox, TextField, Tooltip, IconButton, Card, CardContent, Chip, CardActions, Toolbar, FormControl, Input, InputAdornment, withStyles, DialogContentText, ListItemText as ListItemText$1, ListSubheader as ListSubheader$1, ListItem, ListItemIcon, List, Dialog, DialogTitle, DialogContent, Tabs, Tab, DialogActions, Divider, MenuItem, ListItemSecondaryAction } from '@material-ui/core';
6
6
  import FilterListIcon from '@material-ui/icons/FilterList';
7
- import { Link, Progress, ErrorPanel, Select, ResponseErrorPanel, OverflowTooltip, Table, DependencyGraph, DependencyGraphTypes, CodeSnippet } from '@backstage/core-components';
8
- import { DEFAULT_NAMESPACE, parseEntityRef, ANNOTATION_SOURCE_LOCATION, parseLocationRef, RELATION_OWNED_BY, stringifyEntityRef, getCompoundEntityRef, isUserEntity, isGroupEntity, RELATION_PART_OF, ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model';
9
- import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
10
- import useAsync from 'react-use/lib/useAsync';
7
+ import { Select, Link, Progress, ErrorPanel, ResponseErrorPanel, OverflowTooltip, Table, DependencyGraph, DependencyGraphTypes, CodeSnippet } from '@backstage/core-components';
8
+ import { ANNOTATION_SOURCE_LOCATION, parseLocationRef, stringifyEntityRef, parseEntityRef, RELATION_OWNED_BY, DEFAULT_NAMESPACE, getCompoundEntityRef, isUserEntity, isGroupEntity, RELATION_PART_OF, ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model';
11
9
  import { g as getEntityRelations } from './esm/useEntity-de64059a.esm.js';
12
10
  export { A as AsyncEntityProvider, E as EntityProvider, g as getEntityRelations, a as useAsyncEntity, u as useEntity } from './esm/useEntity-de64059a.esm.js';
13
- import _, { compact, isEqual, groupBy, chunk, debounce } from 'lodash';
11
+ import { compact, isEqual, groupBy, chunk, debounce } from 'lodash';
14
12
  import qs from 'qs';
15
13
  import { useLocation, useNavigate } from 'react-router-dom';
16
14
  import useAsyncFn from 'react-use/lib/useAsyncFn';
17
15
  import useDebounce from 'react-use/lib/useDebounce';
18
16
  import useMountedState from 'react-use/lib/useMountedState';
17
+ import useAsync from 'react-use/lib/useAsync';
19
18
  import isEqual$1 from 'lodash/isEqual';
20
19
  import sortBy from 'lodash/sortBy';
21
20
  import useObservable from 'react-use/lib/useObservable';
@@ -23,6 +22,8 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
23
22
  import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
24
23
  import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
25
24
  import { Autocomplete, Alert } from '@material-ui/lab';
25
+ import get from 'lodash/get';
26
+ import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
26
27
  import HoverPopover from 'material-ui-popup-state/HoverPopover';
27
28
  import { usePopupState, bindHover, bindPopover } from 'material-ui-popup-state/hooks';
28
29
  import InfoIcon from '@material-ui/icons/Info';
@@ -118,158 +119,6 @@ const CatalogFilterLayout = (props) => {
118
119
  CatalogFilterLayout.Filters = Filters;
119
120
  CatalogFilterLayout.Content = Content;
120
121
 
121
- const entityRouteRef = getOrCreateGlobalSingleton(
122
- "catalog:entity-route-ref",
123
- () => createRouteRef({
124
- id: "catalog:entity",
125
- params: ["namespace", "kind", "name"]
126
- })
127
- );
128
- function entityRouteParams(entity) {
129
- var _a, _b;
130
- return {
131
- kind: entity.kind.toLocaleLowerCase("en-US"),
132
- namespace: (_b = (_a = entity.metadata.namespace) == null ? void 0 : _a.toLocaleLowerCase("en-US")) != null ? _b : DEFAULT_NAMESPACE,
133
- name: entity.metadata.name
134
- };
135
- }
136
-
137
- function humanizeEntityRef(entityRef, opts) {
138
- const defaultKind = opts == null ? void 0 : opts.defaultKind;
139
- let kind;
140
- let namespace;
141
- let name;
142
- if ("metadata" in entityRef) {
143
- kind = entityRef.kind;
144
- namespace = entityRef.metadata.namespace;
145
- name = entityRef.metadata.name;
146
- } else {
147
- kind = entityRef.kind;
148
- namespace = entityRef.namespace;
149
- name = entityRef.name;
150
- }
151
- if (namespace === void 0 || namespace === "") {
152
- namespace = DEFAULT_NAMESPACE;
153
- }
154
- if ((opts == null ? void 0 : opts.defaultNamespace) !== void 0) {
155
- if ((opts == null ? void 0 : opts.defaultNamespace) === namespace) {
156
- namespace = void 0;
157
- }
158
- } else if (namespace === DEFAULT_NAMESPACE) {
159
- namespace = void 0;
160
- }
161
- kind = kind.toLocaleLowerCase("en-US");
162
- kind = defaultKind && defaultKind.toLocaleLowerCase("en-US") === kind ? void 0 : kind;
163
- return `${kind ? `${kind}:` : ""}${namespace ? `${namespace}/` : ""}${name}`;
164
- }
165
-
166
- const EntityRefLink = forwardRef(
167
- (props, ref) => {
168
- var _a;
169
- const { entityRef, defaultKind, title, children, ...linkProps } = props;
170
- const entityRoute = useRouteRef(entityRouteRef);
171
- let kind;
172
- let namespace;
173
- let name;
174
- if (typeof entityRef === "string") {
175
- const parsed = parseEntityRef(entityRef);
176
- kind = parsed.kind;
177
- namespace = parsed.namespace;
178
- name = parsed.name;
179
- } else if ("metadata" in entityRef) {
180
- kind = entityRef.kind;
181
- namespace = entityRef.metadata.namespace;
182
- name = entityRef.metadata.name;
183
- } else {
184
- kind = entityRef.kind;
185
- namespace = entityRef.namespace;
186
- name = entityRef.name;
187
- }
188
- kind = kind.toLocaleLowerCase("en-US");
189
- namespace = (_a = namespace == null ? void 0 : namespace.toLocaleLowerCase("en-US")) != null ? _a : DEFAULT_NAMESPACE;
190
- const routeParams = { kind, namespace, name };
191
- const formattedEntityRefTitle = humanizeEntityRef(
192
- { kind, namespace, name },
193
- { defaultKind }
194
- );
195
- const link = /* @__PURE__ */ React.createElement(Link, { ...linkProps, ref, to: entityRoute(routeParams) }, children, !children && (title != null ? title : formattedEntityRefTitle));
196
- return title ? /* @__PURE__ */ React.createElement(Tooltip, { title: formattedEntityRefTitle }, link) : link;
197
- }
198
- );
199
-
200
- function FetchedEntityRefLinks(props) {
201
- const { entityRefs, defaultKind, getTitle, ...linkProps } = props;
202
- const catalogApi = useApi(catalogApiRef);
203
- const {
204
- value: entities = new Array(),
205
- loading,
206
- error
207
- } = useAsync(async () => {
208
- const refs = entityRefs.reduce((acc, current) => {
209
- if (typeof current === "object" && "metadata" in current) {
210
- return acc;
211
- }
212
- return [...acc, parseEntityRef(current)];
213
- }, new Array());
214
- const pureEntities = entityRefs.filter(
215
- (ref) => typeof ref === "object" && "metadata" in ref
216
- );
217
- return refs.length > 0 ? [
218
- ...(await catalogApi.getEntities({
219
- filter: refs.map((ref) => ({
220
- kind: ref.kind,
221
- "metadata.namespace": ref.namespace,
222
- "metadata.name": ref.name
223
- }))
224
- })).items,
225
- ...pureEntities
226
- ] : pureEntities;
227
- }, [entityRefs]);
228
- if (loading) {
229
- return /* @__PURE__ */ React.createElement(Progress, null);
230
- }
231
- if (error) {
232
- return /* @__PURE__ */ React.createElement(ErrorPanel, { error });
233
- }
234
- return /* @__PURE__ */ React.createElement(React.Fragment, null, entities.map((r, i) => {
235
- return /* @__PURE__ */ React.createElement(React.Fragment, { key: i }, i > 0 && ", ", /* @__PURE__ */ React.createElement(
236
- EntityRefLink,
237
- {
238
- ...linkProps,
239
- defaultKind,
240
- entityRef: r,
241
- title: getTitle(r)
242
- }
243
- ));
244
- }));
245
- }
246
-
247
- function EntityRefLinks(props) {
248
- const { entityRefs, defaultKind, fetchEntities, getTitle, ...linkProps } = props;
249
- if (fetchEntities) {
250
- return /* @__PURE__ */ React.createElement(
251
- FetchedEntityRefLinks,
252
- {
253
- ...linkProps,
254
- defaultKind,
255
- entityRefs,
256
- getTitle
257
- }
258
- );
259
- }
260
- return /* @__PURE__ */ React.createElement(React.Fragment, null, entityRefs.map((r, i) => {
261
- return /* @__PURE__ */ React.createElement(React.Fragment, { key: i }, i > 0 && ", ", /* @__PURE__ */ React.createElement(
262
- EntityRefLink,
263
- {
264
- ...linkProps,
265
- defaultKind,
266
- entityRef: r,
267
- title: getTitle ? getTitle(r) : void 0
268
- }
269
- ));
270
- }));
271
- }
272
-
273
122
  function reduceCatalogFilters(filters) {
274
123
  return filters.reduce((compoundFilter, filter) => {
275
124
  return {
@@ -366,15 +215,28 @@ class EntityTextFilter {
366
215
  }
367
216
  class EntityOwnerFilter {
368
217
  constructor(values) {
369
- this.values = values;
218
+ this.values = values.reduce((fullRefs, ref) => {
219
+ try {
220
+ fullRefs.push(
221
+ stringifyEntityRef(parseEntityRef(ref, { defaultKind: "Group" }))
222
+ );
223
+ return fullRefs;
224
+ } catch (err) {
225
+ return fullRefs;
226
+ }
227
+ }, []);
370
228
  }
371
229
  filterEntity(entity) {
372
230
  return this.values.some(
373
231
  (v) => getEntityRelations(entity, RELATION_OWNED_BY).some(
374
- (o) => humanizeEntityRef(o, { defaultKind: "group" }) === v
232
+ (o) => stringifyEntityRef(o) === v
375
233
  )
376
234
  );
377
235
  }
236
+ /**
237
+ * Get the URL query parameter value. May be a mix of full and humanized entity refs.
238
+ * @returns list of entity refs.
239
+ */
378
240
  toQueryValue() {
379
241
  return this.values;
380
242
  }
@@ -937,6 +799,44 @@ const EntityLifecyclePicker = (props) => {
937
799
  )));
938
800
  };
939
801
 
802
+ function humanizeEntityRef(entityRef, opts) {
803
+ const defaultKind = opts == null ? void 0 : opts.defaultKind;
804
+ let kind;
805
+ let namespace;
806
+ let name;
807
+ if ("metadata" in entityRef) {
808
+ kind = entityRef.kind;
809
+ namespace = entityRef.metadata.namespace;
810
+ name = entityRef.metadata.name;
811
+ } else {
812
+ kind = entityRef.kind;
813
+ namespace = entityRef.namespace;
814
+ name = entityRef.name;
815
+ }
816
+ if (namespace === void 0 || namespace === "") {
817
+ namespace = DEFAULT_NAMESPACE;
818
+ }
819
+ if ((opts == null ? void 0 : opts.defaultNamespace) !== void 0) {
820
+ if ((opts == null ? void 0 : opts.defaultNamespace) === namespace) {
821
+ namespace = void 0;
822
+ }
823
+ } else if (namespace === DEFAULT_NAMESPACE) {
824
+ namespace = void 0;
825
+ }
826
+ kind = kind.toLocaleLowerCase("en-US");
827
+ kind = defaultKind && defaultKind.toLocaleLowerCase("en-US") === kind ? void 0 : kind;
828
+ return `${kind ? `${kind}:` : ""}${namespace ? `${namespace}/` : ""}${name}`;
829
+ }
830
+ function humanizeEntity(entity, opts) {
831
+ for (const path of ["spec.profile.displayName", "metadata.title"]) {
832
+ const value = get(entity, path);
833
+ if (value && typeof value === "string") {
834
+ return value;
835
+ }
836
+ }
837
+ return humanizeEntityRef(entity, opts);
838
+ }
839
+
940
840
  const useStyles$d = makeStyles(
941
841
  {
942
842
  input: {}
@@ -948,7 +848,7 @@ const useStyles$d = makeStyles(
948
848
  const icon$2 = /* @__PURE__ */ React.createElement(CheckBoxOutlineBlankIcon, { fontSize: "small" });
949
849
  const checkedIcon$2 = /* @__PURE__ */ React.createElement(CheckBoxIcon, { fontSize: "small" });
950
850
  const EntityOwnerPicker = () => {
951
- var _a, _b;
851
+ var _a, _b, _c;
952
852
  const classes = useStyles$d();
953
853
  const {
954
854
  updateFilters,
@@ -956,45 +856,98 @@ const EntityOwnerPicker = () => {
956
856
  filters,
957
857
  queryParameters: { owners: ownersParameter }
958
858
  } = useEntityList();
859
+ const catalogApi = useApi(catalogApiRef);
860
+ const errorApi = useApi(errorApiRef);
959
861
  const queryParamOwners = useMemo(
960
862
  () => [ownersParameter].flat().filter(Boolean),
961
863
  [ownersParameter]
962
864
  );
963
865
  const [selectedOwners, setSelectedOwners] = useState(
964
- queryParamOwners.length ? queryParamOwners : (_b = (_a = filters.owners) == null ? void 0 : _a.values) != null ? _b : []
866
+ queryParamOwners.length ? new EntityOwnerFilter(queryParamOwners).values : (_b = (_a = filters.owners) == null ? void 0 : _a.values) != null ? _b : []
965
867
  );
966
- useEffect(() => {
967
- if (queryParamOwners.length) {
968
- setSelectedOwners(queryParamOwners);
969
- }
970
- }, [queryParamOwners]);
971
- const availableOwners = useMemo(
972
- () => [
868
+ const {
869
+ loading,
870
+ error,
871
+ value: ownerEntities
872
+ } = useAsync(async () => {
873
+ const ownerEntityRefs = [
973
874
  ...new Set(
974
875
  backendEntities.flatMap(
975
876
  (e) => getEntityRelations(e, RELATION_OWNED_BY).map(
976
- (o) => humanizeEntityRef(o, { defaultKind: "group" })
877
+ (o) => stringifyEntityRef(o)
977
878
  )
978
879
  ).filter(Boolean)
979
880
  )
980
- ].sort(),
981
- [backendEntities]
982
- );
983
- useEffect(() => {
984
- updateFilters({
985
- owners: selectedOwners.length && availableOwners.length ? new EntityOwnerFilter(selectedOwners) : void 0
881
+ ];
882
+ const { items: ownerEntitiesOrNull } = await catalogApi.getEntitiesByRefs({
883
+ entityRefs: ownerEntityRefs,
884
+ fields: [
885
+ "kind",
886
+ "metadata.name",
887
+ "metadata.title",
888
+ "metadata.namespace",
889
+ "spec.profile.displayName"
890
+ ]
891
+ });
892
+ const owners = ownerEntitiesOrNull.map((entity, index) => {
893
+ if (entity) {
894
+ return {
895
+ label: humanizeEntity(entity, { defaultKind: "Group" }),
896
+ entityRef: stringifyEntityRef(entity)
897
+ };
898
+ }
899
+ return {
900
+ label: humanizeEntityRef(parseEntityRef(ownerEntityRefs[index]), {
901
+ defaultKind: "group"
902
+ }),
903
+ entityRef: ownerEntityRefs[index]
904
+ };
986
905
  });
987
- }, [selectedOwners, updateFilters, availableOwners]);
988
- if (!availableOwners.length)
906
+ return owners.sort(
907
+ (a, b) => a.label.localeCompare(b.label, "en-US", {
908
+ ignorePunctuation: true,
909
+ caseFirst: "upper"
910
+ })
911
+ );
912
+ }, [backendEntities]);
913
+ useEffect(() => {
914
+ if (error) {
915
+ errorApi.post(
916
+ {
917
+ ...error,
918
+ message: `EntityOwnerPicker failed to initialize: ${error.message}`
919
+ },
920
+ {}
921
+ );
922
+ }
923
+ }, [error, errorApi]);
924
+ useEffect(() => {
925
+ if (queryParamOwners.length) {
926
+ const filter = new EntityOwnerFilter(queryParamOwners);
927
+ setSelectedOwners(filter.values);
928
+ }
929
+ }, [queryParamOwners]);
930
+ useEffect(() => {
931
+ if (!loading && ownerEntities) {
932
+ updateFilters({
933
+ owners: selectedOwners.length && ownerEntities.length ? new EntityOwnerFilter(selectedOwners) : void 0
934
+ });
935
+ }
936
+ }, [selectedOwners, updateFilters, ownerEntities, loading]);
937
+ if (!loading && !(ownerEntities == null ? void 0 : ownerEntities.length))
989
938
  return null;
990
939
  return /* @__PURE__ */ React.createElement(Box, { pb: 1, pt: 1 }, /* @__PURE__ */ React.createElement(Typography, { variant: "button", component: "label" }, "Owner", /* @__PURE__ */ React.createElement(
991
940
  Autocomplete,
992
941
  {
993
942
  multiple: true,
994
943
  disableCloseOnSelect: true,
995
- options: availableOwners,
996
- value: selectedOwners,
997
- onChange: (_, value) => setSelectedOwners(value),
944
+ loading,
945
+ options: ownerEntities || [],
946
+ value: (_c = ownerEntities == null ? void 0 : ownerEntities.filter(
947
+ (e) => selectedOwners.some((f) => f === e.entityRef)
948
+ )) != null ? _c : [],
949
+ onChange: (_, value) => setSelectedOwners(value.map((e) => e.entityRef)),
950
+ getOptionLabel: (option) => option.label,
998
951
  renderOption: (option, { selected }) => /* @__PURE__ */ React.createElement(
999
952
  FormControlLabel,
1000
953
  {
@@ -1007,7 +960,7 @@ const EntityOwnerPicker = () => {
1007
960
  }
1008
961
  ),
1009
962
  onClick: (event) => event.preventDefault(),
1010
- label: option
963
+ label: option.label
1011
964
  }
1012
965
  ),
1013
966
  size: "small",
@@ -1024,6 +977,129 @@ const EntityOwnerPicker = () => {
1024
977
  )));
1025
978
  };
1026
979
 
980
+ const entityRouteRef = getOrCreateGlobalSingleton(
981
+ "catalog:entity-route-ref",
982
+ () => createRouteRef({
983
+ id: "catalog:entity",
984
+ params: ["namespace", "kind", "name"]
985
+ })
986
+ );
987
+ function entityRouteParams(entity) {
988
+ var _a, _b;
989
+ return {
990
+ kind: entity.kind.toLocaleLowerCase("en-US"),
991
+ namespace: (_b = (_a = entity.metadata.namespace) == null ? void 0 : _a.toLocaleLowerCase("en-US")) != null ? _b : DEFAULT_NAMESPACE,
992
+ name: entity.metadata.name
993
+ };
994
+ }
995
+
996
+ const EntityRefLink = forwardRef(
997
+ (props, ref) => {
998
+ var _a;
999
+ const { entityRef, defaultKind, title, children, ...linkProps } = props;
1000
+ const entityRoute = useRouteRef(entityRouteRef);
1001
+ let kind;
1002
+ let namespace;
1003
+ let name;
1004
+ if (typeof entityRef === "string") {
1005
+ const parsed = parseEntityRef(entityRef);
1006
+ kind = parsed.kind;
1007
+ namespace = parsed.namespace;
1008
+ name = parsed.name;
1009
+ } else if ("metadata" in entityRef) {
1010
+ kind = entityRef.kind;
1011
+ namespace = entityRef.metadata.namespace;
1012
+ name = entityRef.metadata.name;
1013
+ } else {
1014
+ kind = entityRef.kind;
1015
+ namespace = entityRef.namespace;
1016
+ name = entityRef.name;
1017
+ }
1018
+ kind = kind.toLocaleLowerCase("en-US");
1019
+ namespace = (_a = namespace == null ? void 0 : namespace.toLocaleLowerCase("en-US")) != null ? _a : DEFAULT_NAMESPACE;
1020
+ const routeParams = { kind, namespace, name };
1021
+ const formattedEntityRefTitle = humanizeEntityRef(
1022
+ { kind, namespace, name },
1023
+ { defaultKind }
1024
+ );
1025
+ const link = /* @__PURE__ */ React.createElement(Link, { ...linkProps, ref, to: entityRoute(routeParams) }, children, !children && (title != null ? title : formattedEntityRefTitle));
1026
+ return title ? /* @__PURE__ */ React.createElement(Tooltip, { title: formattedEntityRefTitle }, link) : link;
1027
+ }
1028
+ );
1029
+
1030
+ function FetchedEntityRefLinks(props) {
1031
+ const { entityRefs, defaultKind, getTitle, ...linkProps } = props;
1032
+ const catalogApi = useApi(catalogApiRef);
1033
+ const {
1034
+ value: entities = new Array(),
1035
+ loading,
1036
+ error
1037
+ } = useAsync(async () => {
1038
+ const refs = entityRefs.reduce((acc, current) => {
1039
+ if (typeof current === "object" && "metadata" in current) {
1040
+ return acc;
1041
+ }
1042
+ return [...acc, parseEntityRef(current)];
1043
+ }, new Array());
1044
+ const pureEntities = entityRefs.filter(
1045
+ (ref) => typeof ref === "object" && "metadata" in ref
1046
+ );
1047
+ return refs.length > 0 ? [
1048
+ ...(await catalogApi.getEntities({
1049
+ filter: refs.map((ref) => ({
1050
+ kind: ref.kind,
1051
+ "metadata.namespace": ref.namespace,
1052
+ "metadata.name": ref.name
1053
+ }))
1054
+ })).items,
1055
+ ...pureEntities
1056
+ ] : pureEntities;
1057
+ }, [entityRefs]);
1058
+ if (loading) {
1059
+ return /* @__PURE__ */ React.createElement(Progress, null);
1060
+ }
1061
+ if (error) {
1062
+ return /* @__PURE__ */ React.createElement(ErrorPanel, { error });
1063
+ }
1064
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, entities.map((r, i) => {
1065
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: i }, i > 0 && ", ", /* @__PURE__ */ React.createElement(
1066
+ EntityRefLink,
1067
+ {
1068
+ ...linkProps,
1069
+ defaultKind,
1070
+ entityRef: r,
1071
+ title: getTitle(r)
1072
+ }
1073
+ ));
1074
+ }));
1075
+ }
1076
+
1077
+ function EntityRefLinks(props) {
1078
+ const { entityRefs, defaultKind, fetchEntities, getTitle, ...linkProps } = props;
1079
+ if (fetchEntities) {
1080
+ return /* @__PURE__ */ React.createElement(
1081
+ FetchedEntityRefLinks,
1082
+ {
1083
+ ...linkProps,
1084
+ defaultKind,
1085
+ entityRefs,
1086
+ getTitle
1087
+ }
1088
+ );
1089
+ }
1090
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, entityRefs.map((r, i) => {
1091
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: i }, i > 0 && ", ", /* @__PURE__ */ React.createElement(
1092
+ EntityRefLink,
1093
+ {
1094
+ ...linkProps,
1095
+ defaultKind,
1096
+ entityRef: r,
1097
+ title: getTitle ? getTitle(r) : void 0
1098
+ }
1099
+ ));
1100
+ }));
1101
+ }
1102
+
1027
1103
  const EntityCardActions = (props) => {
1028
1104
  const entityRoute = useRouteRef(entityRouteRef);
1029
1105
  return /* @__PURE__ */ React.createElement(
@@ -1441,10 +1517,10 @@ function EntityAutocompletePicker(props) {
1441
1517
  queryParameters.length ? queryParameters : (_b = (_a = filters[name]) == null ? void 0 : _a.values) != null ? _b : []
1442
1518
  );
1443
1519
  useEffect(() => {
1444
- if (queryParameters.length && !_.isEqual(selectedOptions, queryParameters)) {
1520
+ if (queryParameters.length) {
1445
1521
  setSelectedOptions(queryParameters);
1446
1522
  }
1447
- }, [selectedOptions, queryParameters]);
1523
+ }, [queryParameters]);
1448
1524
  const availableOptions = Object.keys(availableValues != null ? availableValues : {});
1449
1525
  const shouldAddFilter = selectedOptions.length && availableOptions.length;
1450
1526
  useEffect(() => {