@backstage/plugin-catalog-react 1.7.0-next.2 → 1.7.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,55 @@
1
1
  # @backstage/plugin-catalog-react
2
2
 
3
+ ## 1.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cb4c15989b6b: The `EntityOwnerPicker` component has undergone improvements to enhance its performance.
8
+ The previous implementation inferred users and groups displayed by the `EntityOwnerPicker` component based on the entities available in the `EntityListContext`. The updated version no longer relies on the `EntityListContext` for inference, allowing for better decoupling and improved performance.
9
+
10
+ The component now loads entities asynchronously, resulting in improved performance and responsiveness. A new `mode` prop has been introduced which provides two different behaviours:
11
+
12
+ - `<EntityOwnerPicker mode="owners-only" />`: loads the owners data asynchronously using the facets endpoint. The data is kept in memory and rendered asynchronously as the user scrolls. This is the default mode and is supposed to be retro-compatible with the previous implementation.
13
+
14
+ - `<EntityOwnerPicker mode="all" />` loads all users and groups present in the catalog asynchronously. The data is loaded in batches as the user scrolls. This is more efficient than `owners-only`, but has the drawback of displaying users and groups who aren't owner of any entity.
15
+
16
+ ### Patch Changes
17
+
18
+ - d68692aee97e: Make `useRelatedEntities` use `getEntitiesByRefs` under the hood
19
+ - 429319d080cd: `EntityAutocompletePicker` add `initialSelectedOptions` prop
20
+ - 429319d080cd: `EntityLifecycleFilter` loads data using the facets endpoint
21
+ - Updated dependencies
22
+ - @backstage/core-plugin-api@1.5.2
23
+ - @backstage/catalog-client@1.4.2
24
+ - @backstage/core-components@0.13.2
25
+ - @backstage/types@1.1.0
26
+ - @backstage/theme@0.4.0
27
+ - @backstage/integration@1.5.0
28
+ - @backstage/catalog-model@1.4.0
29
+ - @backstage/errors@1.2.0
30
+ - @backstage/plugin-permission-react@0.4.13
31
+ - @backstage/version-bridge@1.0.4
32
+ - @backstage/plugin-catalog-common@1.0.14
33
+ - @backstage/plugin-permission-common@0.7.6
34
+
35
+ ## 1.7.0-next.3
36
+
37
+ ### Patch Changes
38
+
39
+ - Updated dependencies
40
+ - @backstage/core-components@0.13.2-next.3
41
+ - @backstage/catalog-model@1.4.0-next.1
42
+ - @backstage/catalog-client@1.4.2-next.2
43
+ - @backstage/core-plugin-api@1.5.2-next.0
44
+ - @backstage/errors@1.2.0-next.0
45
+ - @backstage/integration@1.5.0-next.0
46
+ - @backstage/theme@0.4.0-next.1
47
+ - @backstage/types@1.0.2
48
+ - @backstage/version-bridge@1.0.4
49
+ - @backstage/plugin-catalog-common@1.0.14-next.1
50
+ - @backstage/plugin-permission-common@0.7.6-next.0
51
+ - @backstage/plugin-permission-react@0.4.13-next.0
52
+
3
53
  ## 1.7.0-next.2
4
54
 
5
55
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-react",
3
- "version": "1.7.0-next.2",
3
+ "version": "1.7.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
@@ -95,8 +95,14 @@ declare const EntityLifecyclePicker: (props: {
95
95
 
96
96
  /** @public */
97
97
  type CatalogReactEntityOwnerPickerClassKey = 'input';
98
+ /**
99
+ * @public
100
+ */
101
+ type EntityOwnerPickerProps = {
102
+ mode?: 'owners-only' | 'all';
103
+ };
98
104
  /** @public */
99
- declare const EntityOwnerPicker: () => JSX.Element | null;
105
+ declare const EntityOwnerPicker: (props?: EntityOwnerPickerProps) => JSX.Element | null;
100
106
 
101
107
  /**
102
108
  * Props for {@link EntityRefLink}.
@@ -673,4 +679,4 @@ type EntitySourceLocation = {
673
679
  /** @public */
674
680
  declare function getEntitySourceLocation(entity: Entity, scmIntegrationsApi: ScmIntegrationRegistry): EntitySourceLocation | undefined;
675
681
 
676
- export { AsyncEntityProvider, AsyncEntityProviderProps, BackstageOverrides, CatalogFilterLayout, CatalogReactComponentsNameToClassKey, CatalogReactEntityLifecyclePickerClassKey, CatalogReactEntityNamespacePickerClassKey, CatalogReactEntityOwnerPickerClassKey, CatalogReactEntityProcessingStatusPickerClassKey, CatalogReactEntitySearchBarClassKey, CatalogReactEntityTagPickerClassKey, CatalogReactUserListPickerClassKey, DefaultEntityFilters, EntityErrorFilter, EntityFilter, EntityKindFilter, EntityKindPicker, EntityKindPickerProps, EntityLifecycleFilter, EntityLifecyclePicker, EntityListContext, EntityListContextProps, EntityListProvider, EntityLoadingStatus, EntityNamespaceFilter, EntityNamespacePicker, EntityOrphanFilter, EntityOwnerFilter, EntityOwnerPicker, EntityPeekAheadPopover, EntityPeekAheadPopoverProps, EntityProcessingStatusPicker, EntityProvider, EntityProviderProps, EntityRefLink, EntityRefLinkProps, EntityRefLinks, EntityRefLinksProps, EntitySearchBar, EntitySourceLocation, EntityTable, EntityTableProps, EntityTagFilter, EntityTagPicker, EntityTagPickerProps, EntityTextFilter, EntityTypeFilter, EntityTypePicker, EntityTypePickerProps, FavoriteEntity, FavoriteEntityProps, InspectEntityDialog, MockEntityListContextProvider, MockStarredEntitiesApi, StarredEntitiesApi, UnregisterEntityDialog, UnregisterEntityDialogProps, UserListFilter, UserListFilterKind, UserListPicker, UserListPickerProps, catalogApiRef, columnFactories, entityRouteParams, entityRouteRef, getEntityRelations, getEntitySourceLocation, humanizeEntityRef, starredEntitiesApiRef, useAsyncEntity, useEntity, useEntityList, useEntityOwnership, useEntityTypeFilter, useRelatedEntities, useStarredEntities, useStarredEntity };
682
+ export { AsyncEntityProvider, AsyncEntityProviderProps, BackstageOverrides, CatalogFilterLayout, CatalogReactComponentsNameToClassKey, CatalogReactEntityLifecyclePickerClassKey, CatalogReactEntityNamespacePickerClassKey, CatalogReactEntityOwnerPickerClassKey, CatalogReactEntityProcessingStatusPickerClassKey, CatalogReactEntitySearchBarClassKey, CatalogReactEntityTagPickerClassKey, CatalogReactUserListPickerClassKey, DefaultEntityFilters, EntityErrorFilter, EntityFilter, EntityKindFilter, EntityKindPicker, EntityKindPickerProps, EntityLifecycleFilter, EntityLifecyclePicker, EntityListContext, EntityListContextProps, EntityListProvider, EntityLoadingStatus, EntityNamespaceFilter, EntityNamespacePicker, EntityOrphanFilter, EntityOwnerFilter, EntityOwnerPicker, EntityOwnerPickerProps, EntityPeekAheadPopover, EntityPeekAheadPopoverProps, EntityProcessingStatusPicker, EntityProvider, EntityProviderProps, EntityRefLink, EntityRefLinkProps, EntityRefLinks, EntityRefLinksProps, EntitySearchBar, EntitySourceLocation, EntityTable, EntityTableProps, EntityTagFilter, EntityTagPicker, EntityTagPickerProps, EntityTextFilter, EntityTypeFilter, EntityTypePicker, EntityTypePickerProps, FavoriteEntity, FavoriteEntityProps, InspectEntityDialog, MockEntityListContextProvider, MockStarredEntitiesApi, StarredEntitiesApi, UnregisterEntityDialog, UnregisterEntityDialogProps, UserListFilter, UserListFilterKind, UserListPicker, UserListPickerProps, catalogApiRef, columnFactories, entityRouteParams, entityRouteRef, getEntityRelations, getEntitySourceLocation, humanizeEntityRef, starredEntitiesApiRef, useAsyncEntity, useEntity, useEntityList, useEntityOwnership, useEntityTypeFilter, useRelatedEntities, useStarredEntities, useStarredEntity };
package/dist/index.esm.js CHANGED
@@ -23,7 +23,7 @@ import { Autocomplete, Alert } from '@material-ui/lab';
23
23
  import CheckBoxIcon from '@material-ui/icons/CheckBox';
24
24
  import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
25
25
  import classNames from 'classnames';
26
- import { useDebouncedEffect } from '@react-hookz/web';
26
+ import { useMountEffect, useDebouncedEffect } from '@react-hookz/web';
27
27
  import PersonIcon from '@material-ui/icons/Person';
28
28
  import GroupIcon from '@material-ui/icons/Group';
29
29
  import get from 'lodash/get';
@@ -875,42 +875,105 @@ function humanizeEntity(entity, defaultName) {
875
875
  return defaultName;
876
876
  }
877
877
 
878
- const useStyles$d = makeStyles(
879
- {
880
- input: {}
881
- },
882
- {
883
- name: "CatalogReactEntityOwnerPicker"
878
+ function useFacetsEntities({ enabled }) {
879
+ const catalogApi = useApi(catalogApiRef);
880
+ const [facetsPromise] = useState(async () => {
881
+ if (!enabled) {
882
+ return [];
883
+ }
884
+ const facet = "relations.ownedBy";
885
+ return catalogApi.getEntityFacets({ facets: [facet] }).then(
886
+ (response) => response.facets[facet].map((e) => e.value).map((ref) => {
887
+ const { kind, name, namespace } = parseEntityRef(ref);
888
+ return {
889
+ apiVersion: "backstage.io/v1beta1",
890
+ kind,
891
+ metadata: { name, namespace }
892
+ };
893
+ }).sort(
894
+ (a, b) => (a.metadata.namespace || "").localeCompare(
895
+ b.metadata.namespace || "",
896
+ "en-US"
897
+ ) || a.metadata.name.localeCompare(b.metadata.name, "en-US") || a.kind.localeCompare(b.kind, "en-US")
898
+ )
899
+ ).catch(() => []);
900
+ });
901
+ return useAsyncFn(
902
+ async (request, options) => {
903
+ var _a;
904
+ const facets = await facetsPromise;
905
+ if (!facets) {
906
+ return {
907
+ items: []
908
+ };
909
+ }
910
+ const limit = (_a = options == null ? void 0 : options.limit) != null ? _a : 20;
911
+ const { text, start } = decodeCursor(request);
912
+ const filteredRefs = facets.filter((e) => filterEntity(text, e));
913
+ const end = start + limit;
914
+ return {
915
+ items: filteredRefs.slice(0, end),
916
+ ...encodeCursor({
917
+ entities: filteredRefs,
918
+ limit: end,
919
+ payload: {
920
+ text,
921
+ start: end
922
+ }
923
+ })
924
+ };
925
+ },
926
+ [facetsPromise],
927
+ { loading: true, value: { items: [] } }
928
+ );
929
+ }
930
+ function decodeCursor(request) {
931
+ if (isFacetsResponse(request) && request.cursor) {
932
+ return JSON.parse(atob(request.cursor));
884
933
  }
885
- );
886
- const icon$1 = /* @__PURE__ */ React.createElement(CheckBoxOutlineBlankIcon, { fontSize: "small" });
887
- const checkedIcon$1 = /* @__PURE__ */ React.createElement(CheckBoxIcon, { fontSize: "small" });
888
- const EntityOwnerPicker = () => {
889
- var _a, _b, _c;
890
- const classes = useStyles$d();
891
- const {
892
- updateFilters,
893
- filters,
894
- queryParameters: { owners: ownersParameter }
895
- } = useEntityList();
934
+ return {
935
+ text: request.text || "",
936
+ start: 0
937
+ };
938
+ }
939
+ function isFacetsResponse(request) {
940
+ return !!request.cursor;
941
+ }
942
+ function encodeCursor({
943
+ entities,
944
+ limit,
945
+ payload
946
+ }) {
947
+ if (entities.length > limit) {
948
+ return { cursor: btoa(JSON.stringify(payload)) };
949
+ }
950
+ return {};
951
+ }
952
+ function filterEntity(text, entity) {
953
+ var _a;
954
+ const normalizedText = text.trim();
955
+ return entity.kind.includes(normalizedText) || ((_a = entity.metadata.namespace) == null ? void 0 : _a.includes(normalizedText)) || entity.metadata.name.includes(normalizedText);
956
+ }
957
+
958
+ function useQueryEntities() {
896
959
  const catalogApi = useApi(catalogApiRef);
897
- const [text, setText] = useState("");
898
- const [{ value, loading }, handleFetch] = useAsyncFn(
899
- async (request) => {
960
+ return useAsyncFn(
961
+ async (request, options) => {
962
+ var _a;
900
963
  const initialRequest = request;
901
964
  const cursorRequest = request;
902
- const limit = 20;
965
+ const limit = (_a = options == null ? void 0 : options.limit) != null ? _a : 20;
903
966
  if (cursorRequest.cursor) {
904
- const response = await catalogApi.queryEntities({
967
+ const response2 = await catalogApi.queryEntities({
905
968
  cursor: cursorRequest.cursor,
906
969
  limit
907
970
  });
908
971
  return {
909
- ...response,
910
- items: [...cursorRequest.prev, ...response.items]
972
+ cursor: response2.pageInfo.nextCursor,
973
+ items: [...cursorRequest.items, ...response2.items]
911
974
  };
912
975
  }
913
- return catalogApi.queryEntities({
976
+ const response = await catalogApi.queryEntities({
914
977
  fullTextFilter: {
915
978
  term: initialRequest.text || "",
916
979
  fields: [
@@ -924,11 +987,84 @@ const EntityOwnerPicker = () => {
924
987
  orderFields: [{ field: "metadata.name", order: "asc" }],
925
988
  limit
926
989
  });
990
+ return {
991
+ cursor: response.pageInfo.nextCursor,
992
+ items: response.items
993
+ };
927
994
  },
928
- [text]
995
+ [],
996
+ { loading: true }
929
997
  );
930
- useDebouncedEffect(() => handleFetch({ text }), [text], 250);
931
- const availableOwners = (value == null ? void 0 : value.items) || [];
998
+ }
999
+
1000
+ function useFetchEntities({
1001
+ mode,
1002
+ initialSelectedOwnersRefs
1003
+ }) {
1004
+ const isOwnersOnlyMode = mode === "owners-only";
1005
+ const queryEntitiesResponse = useQueryEntities();
1006
+ const facetsEntitiesResponse = useFacetsEntities({
1007
+ enabled: isOwnersOnlyMode
1008
+ });
1009
+ const [state, handleFetch] = isOwnersOnlyMode ? facetsEntitiesResponse : queryEntitiesResponse;
1010
+ return [
1011
+ state,
1012
+ handleFetch,
1013
+ useSelectedOwners({
1014
+ enabled: !isOwnersOnlyMode,
1015
+ initialSelectedOwnersRefs
1016
+ })
1017
+ ];
1018
+ }
1019
+ function useSelectedOwners({
1020
+ enabled,
1021
+ initialSelectedOwnersRefs
1022
+ }) {
1023
+ const allEntities = useRef({});
1024
+ const catalogApi = useApi(catalogApiRef);
1025
+ const [, handleFetch] = useAsyncFn(async () => {
1026
+ const initialSelectedEntities = await catalogApi.getEntitiesByRefs({
1027
+ entityRefs: initialSelectedOwnersRefs
1028
+ });
1029
+ initialSelectedEntities.items.forEach((e) => {
1030
+ if (e) {
1031
+ allEntities.current[stringifyEntityRef(e)] = e;
1032
+ }
1033
+ });
1034
+ }, []);
1035
+ useMountEffect(() => {
1036
+ if (enabled && initialSelectedOwnersRefs.length > 0) {
1037
+ handleFetch();
1038
+ }
1039
+ });
1040
+ return {
1041
+ getEntity: (entityRef) => allEntities.current[entityRef],
1042
+ setEntity: (entity) => {
1043
+ allEntities.current[stringifyEntityRef(entity)] = entity;
1044
+ }
1045
+ };
1046
+ }
1047
+
1048
+ const useStyles$d = makeStyles(
1049
+ {
1050
+ input: {}
1051
+ },
1052
+ {
1053
+ name: "CatalogReactEntityOwnerPicker"
1054
+ }
1055
+ );
1056
+ const icon$1 = /* @__PURE__ */ React.createElement(CheckBoxOutlineBlankIcon, { fontSize: "small" });
1057
+ const checkedIcon$1 = /* @__PURE__ */ React.createElement(CheckBoxIcon, { fontSize: "small" });
1058
+ const EntityOwnerPicker = (props) => {
1059
+ var _a, _b, _c;
1060
+ const classes = useStyles$d();
1061
+ const { mode = "owners-only" } = props || {};
1062
+ const {
1063
+ updateFilters,
1064
+ filters,
1065
+ queryParameters: { owners: ownersParameter }
1066
+ } = useEntityList();
1067
+ const [text, setText] = useState("");
932
1068
  const queryParamOwners = useMemo(
933
1069
  () => [ownersParameter].flat().filter(Boolean),
934
1070
  [ownersParameter]
@@ -936,7 +1072,12 @@ const EntityOwnerPicker = () => {
936
1072
  const [selectedOwners, setSelectedOwners] = useState(
937
1073
  queryParamOwners.length ? queryParamOwners : (_b = (_a = filters.owners) == null ? void 0 : _a.values) != null ? _b : []
938
1074
  );
939
- const { getEntity, setEntity } = useSelectedOwners(selectedOwners);
1075
+ const [{ value, loading }, handleFetch, cache] = useFetchEntities({
1076
+ mode,
1077
+ initialSelectedOwnersRefs: selectedOwners
1078
+ });
1079
+ useDebouncedEffect(() => handleFetch({ text }), [text, handleFetch], 250);
1080
+ const availableOwners = (value == null ? void 0 : value.items) || [];
940
1081
  useEffect(() => {
941
1082
  if (queryParamOwners.length) {
942
1083
  const filter = new EntityOwnerFilter(queryParamOwners);
@@ -968,8 +1109,11 @@ const EntityOwnerPicker = () => {
968
1109
  return o === v;
969
1110
  },
970
1111
  getOptionLabel: (o) => {
971
- const entity = typeof o === "string" ? getEntity(o) || o : o;
972
- return typeof entity === "string" ? entity : humanizeEntity(entity, entity.metadata.name);
1112
+ const entity = typeof o === "string" ? cache.getEntity(o) || parseEntityRef(o, {
1113
+ defaultKind: "group",
1114
+ defaultNamespace: "default"
1115
+ }) : o;
1116
+ return humanizeEntity(entity, humanizeEntityRef(entity));
973
1117
  },
974
1118
  onChange: (_, owners) => {
975
1119
  setText("");
@@ -977,7 +1121,7 @@ const EntityOwnerPicker = () => {
977
1121
  owners.map((e) => {
978
1122
  const entityRef = typeof e === "string" ? e : stringifyEntityRef(e);
979
1123
  if (typeof e !== "string") {
980
- setEntity(e);
1124
+ cache.setEntity(e);
981
1125
  }
982
1126
  return entityRef;
983
1127
  })
@@ -985,7 +1129,7 @@ const EntityOwnerPicker = () => {
985
1129
  },
986
1130
  filterOptions: (x) => x,
987
1131
  renderOption: (entity, { selected }) => {
988
- const isGroup = entity.kind === "Group";
1132
+ const isGroup = entity.kind.toLocaleLowerCase("en-US") === "group";
989
1133
  return /* @__PURE__ */ React.createElement(
990
1134
  FormControlLabel,
991
1135
  {
@@ -998,7 +1142,10 @@ const EntityOwnerPicker = () => {
998
1142
  }
999
1143
  ),
1000
1144
  onClick: (event) => event.preventDefault(),
1001
- label: /* @__PURE__ */ React.createElement(Box, { display: "flex", flexWrap: "wrap", alignItems: "center" }, isGroup ? /* @__PURE__ */ React.createElement(GroupIcon, { fontSize: "small" }) : /* @__PURE__ */ React.createElement(PersonIcon, { fontSize: "small" }), "\xA0", humanizeEntity(entity, entity.metadata.name))
1145
+ label: /* @__PURE__ */ React.createElement(Box, { display: "flex", flexWrap: "wrap", alignItems: "center" }, isGroup ? /* @__PURE__ */ React.createElement(GroupIcon, { fontSize: "small" }) : /* @__PURE__ */ React.createElement(PersonIcon, { fontSize: "small" }), "\xA0", humanizeEntity(
1146
+ entity,
1147
+ humanizeEntityRef(entity, { defaultKind: entity.kind })
1148
+ ))
1002
1149
  }
1003
1150
  );
1004
1151
  },
@@ -1021,11 +1168,8 @@ const EntityOwnerPicker = () => {
1021
1168
  const hasReachedEnd = Math.abs(
1022
1169
  element.scrollHeight - element.clientHeight - element.scrollTop
1023
1170
  ) < 1;
1024
- if (hasReachedEnd && (value == null ? void 0 : value.pageInfo.nextCursor)) {
1025
- handleFetch({
1026
- cursor: value.pageInfo.nextCursor,
1027
- prev: value.items
1028
- });
1171
+ if (hasReachedEnd && (value == null ? void 0 : value.cursor)) {
1172
+ handleFetch({ items: value.items, cursor: value.cursor });
1029
1173
  }
1030
1174
  },
1031
1175
  "data-testid": "owner-picker-listbox"
@@ -1033,29 +1177,6 @@ const EntityOwnerPicker = () => {
1033
1177
  }
1034
1178
  )));
1035
1179
  };
1036
- function useSelectedOwners(initialSelectedOwnersRefs) {
1037
- const allEntities = useRef({});
1038
- const catalogApi = useApi(catalogApiRef);
1039
- useAsync(async () => {
1040
- if (initialSelectedOwnersRefs.length === 0) {
1041
- return;
1042
- }
1043
- const initialSelectedEntities = await catalogApi.getEntitiesByRefs({
1044
- entityRefs: initialSelectedOwnersRefs
1045
- });
1046
- initialSelectedEntities.items.forEach((e) => {
1047
- if (e) {
1048
- allEntities.current[stringifyEntityRef(e)] = e;
1049
- }
1050
- });
1051
- }, []);
1052
- return {
1053
- getEntity: (entityRef) => allEntities.current[entityRef],
1054
- setEntity: (entity) => {
1055
- allEntities.current[stringifyEntityRef(entity)] = entity;
1056
- }
1057
- };
1058
- }
1059
1180
 
1060
1181
  const entityRouteRef = getOrCreateGlobalSingleton(
1061
1182
  "catalog:entity-route-ref",