@backstage/plugin-catalog-react 2.1.5-next.1 → 3.0.1-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # @backstage/plugin-catalog-react
2
2
 
3
+ ## 3.0.1-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - d8757b1: The entity list provider now fetches the entity list and the total count as two separate parallel requests when using cursor or offset pagination. The list query skips the expensive count computation (using `totalItems: 'exclude'`), so the table populates immediately. The count arrives asynchronously and updates the title. A new `totalItemsLoading` field is exposed on `EntityListContextProps` so consumers can distinguish a stale count from a fresh one.
8
+
9
+ The catalog table now keeps stale rows visible during filter changes and page navigation instead of replacing the entire table body with a spinner. The full-table spinner is only shown on the very first load when no data exists yet. The entity count in the title is dimmed while the count is refreshing, and a small spinner appears next to the title while rows are loading.
10
+
11
+ - e0889a3: chore(deps): bump `qs` from 6.15.1 to 6.15.2
12
+ - 7c20545: Fixed redundant API calls during entity list initialization. Filter components that register their initial state in quick succession (e.g. `EntityKindPicker`, `UserListPicker`, `EntityTagPicker`) no longer trigger multiple identical fetches. Frontend-only filter changes such as toggling the user list are now applied synchronously without a network round-trip.
13
+ - Updated dependencies
14
+ - @backstage/catalog-client@1.16.0-next.0
15
+ - @backstage/core-components@0.18.11-next.0
16
+ - @backstage/frontend-test-utils@0.6.1-next.0
17
+ - @backstage/core-compat-api@0.5.12-next.0
18
+ - @backstage/integration-react@1.2.19-next.0
19
+
20
+ ## 3.0.0
21
+
22
+ ### Patch Changes
23
+
24
+ - f635139: Limited `@remixicon/react` dependency to versions below 4.9.0 due to a license change in that release.
25
+ - 744fa1f: Removed duplicated entries that appeared in both `dependencies` and `devDependencies`.
26
+ - cad156e: Replaced old config schema values from existing extensions and blueprints.
27
+ - 085133f: The `zod` dependency has been bumped from `^3.25.76 || ^4.0.0` to `^4.0.0`, since `configSchema` requires the full Zod v4 package for JSON Schema support.
28
+ - Updated dependencies
29
+ - @backstage/catalog-model@1.9.0
30
+ - @backstage/core-components@0.18.10
31
+ - @backstage/ui@0.15.0
32
+ - @backstage/errors@1.3.1
33
+ - @backstage/frontend-test-utils@0.6.0
34
+ - @backstage/frontend-plugin-api@0.17.0
35
+ - @backstage/core-plugin-api@1.12.6
36
+ - @backstage/filter-predicates@0.1.3
37
+ - @backstage/core-compat-api@0.5.11
38
+ - @backstage/plugin-permission-common@0.9.9
39
+ - @backstage/catalog-client@1.15.1
40
+ - @backstage/integration-react@1.2.18
41
+ - @backstage/plugin-permission-react@0.5.1
42
+
3
43
  ## 2.1.5-next.1
4
44
 
5
45
  ### Patch Changes
@@ -33,6 +33,7 @@ function MockEntityListContextProvider(props) {
33
33
  queryParameters: value?.queryParameters ?? defaultValues.queryParameters,
34
34
  error: value?.error,
35
35
  totalItems: value?.totalItems ?? (value?.entities ?? defaultValues.entities).length,
36
+ totalItemsLoading: value?.totalItemsLoading ?? false,
36
37
  limit: value?.limit ?? 20,
37
38
  offset: value?.offset,
38
39
  setLimit: value?.setLimit ?? (() => {
@@ -1 +1 @@
1
- {"version":3,"file":"deprecated.esm.js","sources":["../src/deprecated.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PropsWithChildren, useCallback, useMemo, useState } from 'react';\nimport { createVersionedValueMap } from '@backstage/version-bridge';\nimport {\n DefaultEntityFilters,\n EntityListContextProps,\n NewEntityListContext,\n OldEntityListContext,\n} from './hooks/useEntityListProvider';\n\n/**\n * @public\n * @deprecated Moved to `@backstage/plugin-catalog-react/testUtils`\n */\nexport function MockEntityListContextProvider<\n T extends DefaultEntityFilters = DefaultEntityFilters,\n>(\n props: PropsWithChildren<{\n value?: Partial<EntityListContextProps<T>>;\n }>,\n) {\n const { children, value } = props;\n\n // Provides a default implementation that stores filter state, for testing components that\n // reflect filter state.\n const [filters, setFilters] = useState<T>(value?.filters ?? ({} as T));\n\n const updateFilters = useCallback(\n (update: Partial<T> | ((prevFilters: T) => Partial<T>)) => {\n setFilters(prevFilters => {\n const newFilters =\n typeof update === 'function' ? update(prevFilters) : update;\n return { ...prevFilters, ...newFilters };\n });\n },\n [],\n );\n\n // Memoize the default values since pickers have useEffect triggers on these; naively defaulting\n // below with `?? <X>` breaks referential equality on subsequent updates.\n const defaultValues = useMemo(\n () => ({\n entities: [],\n backendEntities: [],\n queryParameters: {},\n }),\n [],\n );\n\n const resolvedValue: EntityListContextProps<T> = useMemo(\n () => ({\n entities: value?.entities ?? defaultValues.entities,\n backendEntities: value?.backendEntities ?? defaultValues.backendEntities,\n updateFilters: value?.updateFilters ?? updateFilters,\n filters,\n loading: value?.loading ?? false,\n queryParameters: value?.queryParameters ?? defaultValues.queryParameters,\n error: value?.error,\n totalItems:\n value?.totalItems ?? (value?.entities ?? defaultValues.entities).length,\n limit: value?.limit ?? 20,\n offset: value?.offset,\n setLimit: value?.setLimit ?? (() => {}),\n setOffset: value?.setOffset,\n paginationMode: value?.paginationMode ?? 'none',\n }),\n [value, defaultValues, filters, updateFilters],\n );\n\n return (\n <NewEntityListContext.Provider\n value={createVersionedValueMap({ 1: resolvedValue })}\n >\n {children}\n </NewEntityListContext.Provider>\n );\n}\n\n/**\n * Creates new context for entity listing and filtering.\n *\n * @public\n * @deprecated Please use `EntityListProvider` and `EntityListProvider` instead.\n */\nexport const EntityListContext = OldEntityListContext;\n"],"names":[],"mappings":";;;;;AA6BO,SAAS,8BAGd,KAAA,EAGA;AACA,EAAA,MAAM,EAAE,QAAA,EAAU,KAAA,EAAM,GAAI,KAAA;AAI5B,EAAA,MAAM,CAAC,SAAS,UAAU,CAAA,GAAI,SAAY,KAAA,EAAO,OAAA,IAAY,EAAQ,CAAA;AAErE,EAAA,MAAM,aAAA,GAAgB,WAAA;AAAA,IACpB,CAAC,MAAA,KAA0D;AACzD,MAAA,UAAA,CAAW,CAAA,WAAA,KAAe;AACxB,QAAA,MAAM,aACJ,OAAO,MAAA,KAAW,UAAA,GAAa,MAAA,CAAO,WAAW,CAAA,GAAI,MAAA;AACvD,QAAA,OAAO,EAAE,GAAG,WAAA,EAAa,GAAG,UAAA,EAAW;AAAA,MACzC,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA;AAAC,GACH;AAIA,EAAA,MAAM,aAAA,GAAgB,OAAA;AAAA,IACpB,OAAO;AAAA,MACL,UAAU,EAAC;AAAA,MACX,iBAAiB,EAAC;AAAA,MAClB,iBAAiB;AAAC,KACpB,CAAA;AAAA,IACA;AAAC,GACH;AAEA,EAAA,MAAM,aAAA,GAA2C,OAAA;AAAA,IAC/C,OAAO;AAAA,MACL,QAAA,EAAU,KAAA,EAAO,QAAA,IAAY,aAAA,CAAc,QAAA;AAAA,MAC3C,eAAA,EAAiB,KAAA,EAAO,eAAA,IAAmB,aAAA,CAAc,eAAA;AAAA,MACzD,aAAA,EAAe,OAAO,aAAA,IAAiB,aAAA;AAAA,MACvC,OAAA;AAAA,MACA,OAAA,EAAS,OAAO,OAAA,IAAW,KAAA;AAAA,MAC3B,eAAA,EAAiB,KAAA,EAAO,eAAA,IAAmB,aAAA,CAAc,eAAA;AAAA,MACzD,OAAO,KAAA,EAAO,KAAA;AAAA,MACd,YACE,KAAA,EAAO,UAAA,IAAA,CAAe,KAAA,EAAO,QAAA,IAAY,cAAc,QAAA,EAAU,MAAA;AAAA,MACnE,KAAA,EAAO,OAAO,KAAA,IAAS,EAAA;AAAA,MACvB,QAAQ,KAAA,EAAO,MAAA;AAAA,MACf,QAAA,EAAU,KAAA,EAAO,QAAA,KAAa,MAAM;AAAA,MAAC,CAAA,CAAA;AAAA,MACrC,WAAW,KAAA,EAAO,SAAA;AAAA,MAClB,cAAA,EAAgB,OAAO,cAAA,IAAkB;AAAA,KAC3C,CAAA;AAAA,IACA,CAAC,KAAA,EAAO,aAAA,EAAe,OAAA,EAAS,aAAa;AAAA,GAC/C;AAEA,EAAA,uBACE,GAAA;AAAA,IAAC,oBAAA,CAAqB,QAAA;AAAA,IAArB;AAAA,MACC,KAAA,EAAO,uBAAA,CAAwB,EAAE,CAAA,EAAG,eAAe,CAAA;AAAA,MAElD;AAAA;AAAA,GACH;AAEJ;AAQO,MAAM,iBAAA,GAAoB;;;;"}
1
+ {"version":3,"file":"deprecated.esm.js","sources":["../src/deprecated.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PropsWithChildren, useCallback, useMemo, useState } from 'react';\nimport { createVersionedValueMap } from '@backstage/version-bridge';\nimport {\n DefaultEntityFilters,\n EntityListContextProps,\n NewEntityListContext,\n OldEntityListContext,\n} from './hooks/useEntityListProvider';\n\n/**\n * @public\n * @deprecated Moved to `@backstage/plugin-catalog-react/testUtils`\n */\nexport function MockEntityListContextProvider<\n T extends DefaultEntityFilters = DefaultEntityFilters,\n>(\n props: PropsWithChildren<{\n value?: Partial<EntityListContextProps<T>>;\n }>,\n) {\n const { children, value } = props;\n\n // Provides a default implementation that stores filter state, for testing components that\n // reflect filter state.\n const [filters, setFilters] = useState<T>(value?.filters ?? ({} as T));\n\n const updateFilters = useCallback(\n (update: Partial<T> | ((prevFilters: T) => Partial<T>)) => {\n setFilters(prevFilters => {\n const newFilters =\n typeof update === 'function' ? update(prevFilters) : update;\n return { ...prevFilters, ...newFilters };\n });\n },\n [],\n );\n\n // Memoize the default values since pickers have useEffect triggers on these; naively defaulting\n // below with `?? <X>` breaks referential equality on subsequent updates.\n const defaultValues = useMemo(\n () => ({\n entities: [],\n backendEntities: [],\n queryParameters: {},\n }),\n [],\n );\n\n const resolvedValue: EntityListContextProps<T> = useMemo(\n () => ({\n entities: value?.entities ?? defaultValues.entities,\n backendEntities: value?.backendEntities ?? defaultValues.backendEntities,\n updateFilters: value?.updateFilters ?? updateFilters,\n filters,\n loading: value?.loading ?? false,\n queryParameters: value?.queryParameters ?? defaultValues.queryParameters,\n error: value?.error,\n totalItems:\n value?.totalItems ?? (value?.entities ?? defaultValues.entities).length,\n totalItemsLoading: value?.totalItemsLoading ?? false,\n limit: value?.limit ?? 20,\n offset: value?.offset,\n setLimit: value?.setLimit ?? (() => {}),\n setOffset: value?.setOffset,\n paginationMode: value?.paginationMode ?? 'none',\n }),\n [value, defaultValues, filters, updateFilters],\n );\n\n return (\n <NewEntityListContext.Provider\n value={createVersionedValueMap({ 1: resolvedValue })}\n >\n {children}\n </NewEntityListContext.Provider>\n );\n}\n\n/**\n * Creates new context for entity listing and filtering.\n *\n * @public\n * @deprecated Please use `EntityListProvider` and `EntityListProvider` instead.\n */\nexport const EntityListContext = OldEntityListContext;\n"],"names":[],"mappings":";;;;;AA6BO,SAAS,8BAGd,KAAA,EAGA;AACA,EAAA,MAAM,EAAE,QAAA,EAAU,KAAA,EAAM,GAAI,KAAA;AAI5B,EAAA,MAAM,CAAC,SAAS,UAAU,CAAA,GAAI,SAAY,KAAA,EAAO,OAAA,IAAY,EAAQ,CAAA;AAErE,EAAA,MAAM,aAAA,GAAgB,WAAA;AAAA,IACpB,CAAC,MAAA,KAA0D;AACzD,MAAA,UAAA,CAAW,CAAA,WAAA,KAAe;AACxB,QAAA,MAAM,aACJ,OAAO,MAAA,KAAW,UAAA,GAAa,MAAA,CAAO,WAAW,CAAA,GAAI,MAAA;AACvD,QAAA,OAAO,EAAE,GAAG,WAAA,EAAa,GAAG,UAAA,EAAW;AAAA,MACzC,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA;AAAC,GACH;AAIA,EAAA,MAAM,aAAA,GAAgB,OAAA;AAAA,IACpB,OAAO;AAAA,MACL,UAAU,EAAC;AAAA,MACX,iBAAiB,EAAC;AAAA,MAClB,iBAAiB;AAAC,KACpB,CAAA;AAAA,IACA;AAAC,GACH;AAEA,EAAA,MAAM,aAAA,GAA2C,OAAA;AAAA,IAC/C,OAAO;AAAA,MACL,QAAA,EAAU,KAAA,EAAO,QAAA,IAAY,aAAA,CAAc,QAAA;AAAA,MAC3C,eAAA,EAAiB,KAAA,EAAO,eAAA,IAAmB,aAAA,CAAc,eAAA;AAAA,MACzD,aAAA,EAAe,OAAO,aAAA,IAAiB,aAAA;AAAA,MACvC,OAAA;AAAA,MACA,OAAA,EAAS,OAAO,OAAA,IAAW,KAAA;AAAA,MAC3B,eAAA,EAAiB,KAAA,EAAO,eAAA,IAAmB,aAAA,CAAc,eAAA;AAAA,MACzD,OAAO,KAAA,EAAO,KAAA;AAAA,MACd,YACE,KAAA,EAAO,UAAA,IAAA,CAAe,KAAA,EAAO,QAAA,IAAY,cAAc,QAAA,EAAU,MAAA;AAAA,MACnE,iBAAA,EAAmB,OAAO,iBAAA,IAAqB,KAAA;AAAA,MAC/C,KAAA,EAAO,OAAO,KAAA,IAAS,EAAA;AAAA,MACvB,QAAQ,KAAA,EAAO,MAAA;AAAA,MACf,QAAA,EAAU,KAAA,EAAO,QAAA,KAAa,MAAM;AAAA,MAAC,CAAA,CAAA;AAAA,MACrC,WAAW,KAAA,EAAO,SAAA;AAAA,MAClB,cAAA,EAAgB,OAAO,cAAA,IAAkB;AAAA,KAC3C,CAAA;AAAA,IACA,CAAC,KAAA,EAAO,aAAA,EAAe,OAAA,EAAS,aAAa;AAAA,GAC/C;AAEA,EAAA,uBACE,GAAA;AAAA,IAAC,oBAAA,CAAqB,QAAA;AAAA,IAArB;AAAA,MACC,KAAA,EAAO,uBAAA,CAAwB,EAAE,CAAA,EAAG,eAAe,CAAA;AAAA,MAElD;AAAA;AAAA,GACH;AAEJ;AAQO,MAAM,iBAAA,GAAoB;;;;"}
@@ -3,7 +3,7 @@ import { useApi } from '@backstage/core-plugin-api';
3
3
  import { createVersionedContext, createVersionedValueMap, useVersionedContext } from '@backstage/version-bridge';
4
4
  import { compact, isEqual } from 'lodash';
5
5
  import qs from 'qs';
6
- import { createContext, useState, useMemo, useEffect, useCallback, useContext } from 'react';
6
+ import { createContext, useState, useMemo, useRef, useEffect, useCallback, useContext } from 'react';
7
7
  import { useLocation } from 'react-router-dom';
8
8
  import useAsyncFn from 'react-use/esm/useAsyncFn';
9
9
  import useDebounce from 'react-use/esm/useDebounce';
@@ -20,13 +20,12 @@ const EntityListProvider = (props) => {
20
20
  {}
21
21
  );
22
22
  const location = useLocation();
23
- const getPaginationMode = () => {
24
- if (props.pagination === true) {
25
- return "cursor";
26
- }
27
- return typeof props.pagination === "object" ? props.pagination.mode ?? "cursor" : "none";
28
- };
29
- const paginationMode = getPaginationMode();
23
+ let paginationMode = "none";
24
+ if (props.pagination === true) {
25
+ paginationMode = "cursor";
26
+ } else if (typeof props.pagination === "object") {
27
+ paginationMode = props.pagination.mode ?? "cursor";
28
+ }
30
29
  const paginationLimit = typeof props.pagination === "object" ? props.pagination.limit ?? 20 : 20;
31
30
  const {
32
31
  queryParameters,
@@ -56,153 +55,141 @@ const EntityListProvider = (props) => {
56
55
  const [cursor, setCursor] = useState(initialCursor);
57
56
  const [offset, setOffset] = useState(initialOffset);
58
57
  const [limit, setLimit] = useState(initialLimit);
59
- const [outputState, setOutputState] = useState(
60
- () => {
61
- return {
62
- appliedFilters: {},
63
- entities: [],
64
- backendEntities: [],
65
- pageInfo: {},
66
- offset,
67
- limit
68
- };
69
- }
70
- );
71
- const [{ value: resolvedValue, loading, error }, refresh] = useAsyncFn(
72
- async () => {
73
- const kindValue = requestedFilters.kind?.value?.toLocaleLowerCase("en-US");
74
- const adjustedFilters = kindValue === "user" || kindValue === "group" ? { ...requestedFilters, owners: void 0 } : requestedFilters;
75
- const compacted = compact(Object.values(adjustedFilters));
76
- const entityFilter = reduceEntityFilters(compacted);
77
- if (paginationMode !== "none") {
78
- if (cursor) {
79
- if (cursor !== outputState.appliedCursor) {
80
- const response = await catalogApi.queryEntities({
81
- cursor,
82
- limit
83
- });
84
- return {
85
- appliedFilters: requestedFilters,
86
- appliedCursor: cursor,
87
- backendEntities: response.items,
88
- entities: response.items.filter(entityFilter),
89
- pageInfo: response.pageInfo,
90
- totalItems: response.totalItems
91
- };
92
- }
93
- const entities3 = outputState.backendEntities.filter(entityFilter);
58
+ const [backendState, setBackendState] = useState({
59
+ backendEntities: []
60
+ });
61
+ const [loading, setLoading] = useState(true);
62
+ const [error, setError] = useState();
63
+ const lastFetchParamsRef = useRef(void 0);
64
+ const fetchGenRef = useRef(0);
65
+ const adjustedFilters = useMemo(() => {
66
+ const kindValue = requestedFilters.kind?.value?.toLocaleLowerCase("en-US");
67
+ return kindValue === "user" || kindValue === "group" ? { ...requestedFilters, owners: void 0 } : requestedFilters;
68
+ }, [requestedFilters]);
69
+ const refresh = async () => {
70
+ const compacted = compact(Object.values(adjustedFilters));
71
+ let fetchParams;
72
+ let doFetch;
73
+ if (paginationMode !== "none") {
74
+ if (cursor) {
75
+ fetchParams = { cursor, limit };
76
+ doFetch = async () => {
77
+ const response = await catalogApi.queryEntities({
78
+ cursor,
79
+ limit,
80
+ totalItems: "exclude"
81
+ });
94
82
  return {
95
- appliedFilters: requestedFilters,
96
- appliedCursor: outputState.appliedCursor,
97
- backendEntities: outputState.backendEntities,
98
- entities: entities3,
99
- pageInfo: outputState.pageInfo,
100
- totalItems: outputState.totalItems,
101
- limit: outputState.limit,
102
- offset: outputState.offset
83
+ backendEntities: response.items,
84
+ pageInfo: response.pageInfo
103
85
  };
104
- }
105
- const backendFilter2 = reduceCatalogFilters(compacted);
106
- const previousBackendFilter2 = reduceCatalogFilters(
107
- compact(Object.values(outputState.appliedFilters))
108
- );
109
- if (paginationMode === "offset" && (outputState.limit !== limit || outputState.offset !== offset) || !isEqual(previousBackendFilter2, backendFilter2)) {
86
+ };
87
+ } else {
88
+ const backendFilter = reduceCatalogFilters(compacted);
89
+ fetchParams = { ...backendFilter, limit, offset };
90
+ doFetch = async () => {
110
91
  const response = await catalogApi.queryEntities({
111
- ...backendFilter2,
92
+ ...backendFilter,
112
93
  limit,
113
- offset
94
+ offset,
95
+ totalItems: "exclude"
114
96
  });
115
97
  return {
116
- appliedFilters: requestedFilters,
117
98
  backendEntities: response.items,
118
- entities: response.items.filter(entityFilter),
119
- pageInfo: response.pageInfo,
120
- totalItems: response.totalItems,
121
- limit,
122
- offset
99
+ pageInfo: response.pageInfo
123
100
  };
124
- }
125
- const entities2 = outputState.backendEntities.filter(entityFilter);
126
- return {
127
- appliedFilters: requestedFilters,
128
- backendEntities: outputState.backendEntities,
129
- entities: entities2,
130
- pageInfo: outputState.pageInfo,
131
- totalItems: outputState.totalItems,
132
- limit: outputState.limit,
133
- offset: outputState.offset
134
101
  };
135
102
  }
103
+ } else {
136
104
  const backendFilter = reduceBackendCatalogFilters(compacted);
137
105
  const { orderFields } = reduceCatalogFilters(compacted);
138
- const previousBackendFilter = reduceBackendCatalogFilters(
139
- compact(Object.values(outputState.appliedFilters))
140
- );
141
- if (!isEqual(previousBackendFilter, backendFilter)) {
106
+ fetchParams = { filter: backendFilter, order: orderFields };
107
+ doFetch = async () => {
142
108
  const response = await catalogApi.getEntities({
143
109
  filter: backendFilter,
144
110
  order: orderFields
145
111
  });
146
- const entities2 = response.items.filter(entityFilter);
147
- return {
148
- appliedFilters: requestedFilters,
149
- backendEntities: response.items,
150
- entities: entities2,
151
- totalItems: entities2.length
152
- };
153
- }
154
- const entities = outputState.backendEntities.filter(entityFilter);
155
- return {
156
- appliedFilters: requestedFilters,
157
- backendEntities: outputState.backendEntities,
158
- entities,
159
- totalItems: entities.length
112
+ return { backendEntities: response.items };
160
113
  };
161
- },
162
- [
163
- catalogApi,
164
- queryParameters,
165
- requestedFilters,
166
- outputState,
167
- cursor,
168
- paginationMode,
169
- limit,
170
- offset
171
- ],
172
- { loading: true }
173
- );
174
- useDebounce(refresh, 10, [requestedFilters, cursor, limit, offset]);
175
- useEffect(() => {
176
- if (resolvedValue === void 0) {
114
+ }
115
+ if (compacted.length === 0 && lastFetchParamsRef.current === void 0 && !cursor) {
116
+ setLoading(false);
177
117
  return;
178
118
  }
179
- setOutputState(resolvedValue);
180
- if (isMounted()) {
181
- const queryParams = Object.keys(requestedFilters).reduce(
182
- (params, key) => {
183
- const filter = requestedFilters[key];
184
- if (filter?.toQueryValue) {
185
- params[key] = filter.toQueryValue();
186
- }
187
- return params;
188
- },
189
- {}
190
- );
191
- const oldParams = qs.parse(location.search, {
192
- ignoreQueryPrefix: true,
193
- arrayLimit: 1e4
119
+ if (isEqual(fetchParams, lastFetchParamsRef.current)) {
120
+ return;
121
+ }
122
+ lastFetchParamsRef.current = fetchParams;
123
+ const gen = ++fetchGenRef.current;
124
+ setLoading(true);
125
+ setError(void 0);
126
+ try {
127
+ const result = await doFetch();
128
+ if (gen === fetchGenRef.current) {
129
+ setBackendState(result);
130
+ }
131
+ } catch (e) {
132
+ if (gen === fetchGenRef.current) {
133
+ lastFetchParamsRef.current = void 0;
134
+ setError(e);
135
+ }
136
+ } finally {
137
+ if (gen === fetchGenRef.current) {
138
+ setLoading(false);
139
+ }
140
+ }
141
+ };
142
+ useDebounce(refresh, 10, [adjustedFilters, cursor, limit, offset]);
143
+ const [{ value: totalItems, loading: totalItemsLoading }, refreshCount] = useAsyncFn(async () => {
144
+ if (paginationMode === "none") {
145
+ return void 0;
146
+ }
147
+ const compacted = compact(Object.values(adjustedFilters));
148
+ if (compacted.length === 0) {
149
+ return void 0;
150
+ }
151
+ const backendFilter = reduceCatalogFilters(compacted);
152
+ try {
153
+ const response = await catalogApi.queryEntities({
154
+ ...backendFilter,
155
+ limit: 0
194
156
  });
195
- const newParams = qs.stringify(
196
- {
197
- ...oldParams,
198
- filters: queryParams,
199
- ...paginationMode === "none" ? {} : { cursor, limit, offset }
200
- },
201
- { addQueryPrefix: true, arrayFormat: "repeat" }
202
- );
203
- const newUrl = `${window.location.pathname}${newParams}`;
204
- window.history?.replaceState(null, document.title, newUrl);
157
+ return response.totalItems;
158
+ } catch {
159
+ return void 0;
160
+ }
161
+ }, [catalogApi, paginationMode, adjustedFilters]);
162
+ useDebounce(refreshCount, 10, [adjustedFilters]);
163
+ const entities = useMemo(() => {
164
+ const compacted = compact(Object.values(adjustedFilters));
165
+ const entityFilter = reduceEntityFilters(compacted);
166
+ return backendState.backendEntities.filter(entityFilter);
167
+ }, [adjustedFilters, backendState.backendEntities]);
168
+ useEffect(() => {
169
+ if (!isMounted() || Object.keys(requestedFilters).length === 0) {
170
+ return;
205
171
  }
172
+ const queryParams = Object.keys(requestedFilters).reduce((params, key) => {
173
+ const filter = requestedFilters[key];
174
+ if (filter?.toQueryValue) {
175
+ params[key] = filter.toQueryValue();
176
+ }
177
+ return params;
178
+ }, {});
179
+ const oldParams = qs.parse(location.search, {
180
+ ignoreQueryPrefix: true,
181
+ arrayLimit: 1e4
182
+ });
183
+ const newParams = qs.stringify(
184
+ {
185
+ ...oldParams,
186
+ filters: queryParams,
187
+ ...paginationMode === "none" ? {} : { cursor, limit, offset }
188
+ },
189
+ { addQueryPrefix: true, arrayFormat: "repeat" }
190
+ );
191
+ const newUrl = `${window.location.pathname}${newParams}`;
192
+ window.history?.replaceState(null, document.title, newUrl);
206
193
  }, [
207
194
  cursor,
208
195
  isMounted,
@@ -210,7 +197,6 @@ const EntityListProvider = (props) => {
210
197
  location.search,
211
198
  offset,
212
199
  requestedFilters,
213
- resolvedValue,
214
200
  paginationMode
215
201
  ]);
216
202
  const updateFilters = useCallback(
@@ -227,29 +213,29 @@ const EntityListProvider = (props) => {
227
213
  },
228
214
  [paginationMode]
229
215
  );
230
- const latestOutput = resolvedValue ?? outputState;
231
216
  const pageInfo = useMemo(() => {
232
217
  if (paginationMode !== "cursor") {
233
218
  return void 0;
234
219
  }
235
- const prevCursor = latestOutput.pageInfo?.prevCursor;
236
- const nextCursor = latestOutput.pageInfo?.nextCursor;
220
+ const prevCursor = backendState.pageInfo?.prevCursor;
221
+ const nextCursor = backendState.pageInfo?.nextCursor;
237
222
  return {
238
223
  prev: prevCursor ? () => setCursor(prevCursor) : void 0,
239
224
  next: nextCursor ? () => setCursor(nextCursor) : void 0
240
225
  };
241
- }, [paginationMode, latestOutput.pageInfo]);
226
+ }, [paginationMode, backendState.pageInfo]);
242
227
  const value = useMemo(
243
228
  () => ({
244
- filters: latestOutput.appliedFilters,
245
- entities: latestOutput.entities,
246
- backendEntities: latestOutput.backendEntities,
229
+ filters: requestedFilters,
230
+ entities,
231
+ backendEntities: backendState.backendEntities,
247
232
  updateFilters,
248
233
  queryParameters,
249
234
  loading,
250
235
  error,
251
236
  pageInfo,
252
- totalItems: latestOutput.totalItems,
237
+ totalItems: paginationMode === "none" ? entities.length : totalItems ?? backendState.totalItems,
238
+ totalItemsLoading: paginationMode !== "none" && totalItemsLoading,
253
239
  limit,
254
240
  offset,
255
241
  setLimit,
@@ -257,15 +243,19 @@ const EntityListProvider = (props) => {
257
243
  paginationMode
258
244
  }),
259
245
  [
260
- latestOutput,
246
+ requestedFilters,
247
+ entities,
248
+ backendState,
249
+ totalItems,
250
+ totalItemsLoading,
261
251
  updateFilters,
262
252
  queryParameters,
263
253
  loading,
264
254
  error,
265
255
  pageInfo,
256
+ paginationMode,
266
257
  limit,
267
258
  offset,
268
- paginationMode,
269
259
  setLimit,
270
260
  setOffset
271
261
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"useEntityListProvider.esm.js","sources":["../../src/hooks/useEntityListProvider.tsx"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { QueryEntitiesResponse } from '@backstage/catalog-client';\nimport { Entity } from '@backstage/catalog-model';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n createVersionedContext,\n createVersionedValueMap,\n useVersionedContext,\n} from '@backstage/version-bridge';\nimport { compact, isEqual } from 'lodash';\nimport qs from 'qs';\nimport {\n createContext,\n PropsWithChildren,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react';\nimport { useLocation } from 'react-router-dom';\nimport useAsyncFn from 'react-use/esm/useAsyncFn';\nimport useDebounce from 'react-use/esm/useDebounce';\nimport useMountedState from 'react-use/esm/useMountedState';\nimport { catalogApiRef } from '../api';\nimport {\n EntityErrorFilter,\n EntityKindFilter,\n EntityLifecycleFilter,\n EntityNamespaceFilter,\n EntityOrderFilter,\n EntityOrphanFilter,\n EntityOwnerFilter,\n EntityTagFilter,\n EntityTextFilter,\n EntityTypeFilter,\n EntityUserFilter,\n UserListFilter,\n} from '../filters';\nimport { EntityFilter, EntityListPagination } from '../types';\nimport {\n reduceBackendCatalogFilters,\n reduceCatalogFilters,\n reduceEntityFilters,\n} from '../utils/filters';\n\n/** @public */\nexport type DefaultEntityFilters = {\n kind?: EntityKindFilter;\n type?: EntityTypeFilter;\n user?: UserListFilter | EntityUserFilter;\n owners?: EntityOwnerFilter;\n lifecycles?: EntityLifecycleFilter;\n tags?: EntityTagFilter;\n text?: EntityTextFilter;\n orphan?: EntityOrphanFilter;\n error?: EntityErrorFilter;\n namespace?: EntityNamespaceFilter;\n order?: EntityOrderFilter;\n};\n\n/** @public */\nexport type PaginationMode = 'cursor' | 'offset' | 'none';\n\n/** @public */\nexport type EntityListContextProps<\n EntityFilters extends DefaultEntityFilters = DefaultEntityFilters,\n> = {\n /**\n * The currently registered filters, adhering to the shape of DefaultEntityFilters or an extension\n * of that default (to add custom filter types).\n */\n filters: EntityFilters;\n\n /**\n * The resolved list of catalog entities, after all filters are applied.\n */\n entities: Entity[];\n\n /**\n * The resolved list of catalog entities, after _only catalog-backend_ filters are applied.\n */\n backendEntities: Entity[];\n\n /**\n * Update one or more of the registered filters. Optional filters can be set to `undefined` to\n * reset the filter.\n */\n updateFilters: (\n filters:\n | Partial<EntityFilters>\n | ((prevFilters: EntityFilters) => Partial<EntityFilters>),\n ) => void;\n\n /**\n * Filter values from query parameters.\n */\n queryParameters: Partial<Record<keyof EntityFilters, string | string[]>>;\n\n loading: boolean;\n error?: Error;\n\n pageInfo?: {\n next?: () => void;\n prev?: () => void;\n };\n totalItems?: number;\n limit: number;\n offset?: number;\n setLimit: (limit: number) => void;\n setOffset?: (offset: number) => void;\n paginationMode: PaginationMode;\n};\n\n// This context has support for multiple concurrent versions of this package.\n// It is currently used in parallel with the old context in order to provide\n// a smooth transition, but will eventually be the only context we use.\nexport const NewEntityListContext = createVersionedContext<{\n 1: EntityListContextProps<any>;\n}>('entity-list-context');\n\n/**\n * Creates new context for entity listing and filtering.\n */\nexport const OldEntityListContext = createContext<\n EntityListContextProps<any> | undefined\n>(undefined);\n\ntype OutputState<EntityFilters extends DefaultEntityFilters> = {\n appliedFilters: EntityFilters;\n appliedCursor?: string;\n entities: Entity[];\n backendEntities: Entity[];\n pageInfo?: QueryEntitiesResponse['pageInfo'];\n totalItems?: number;\n offset?: number;\n limit?: number;\n};\n\n/**\n * @public\n */\nexport type EntityListProviderProps = PropsWithChildren<{\n pagination?: EntityListPagination;\n}>;\n\n/**\n * Provides entities and filters for a catalog listing.\n * @public\n */\nexport const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(\n props: EntityListProviderProps,\n) => {\n const isMounted = useMountedState();\n const catalogApi = useApi(catalogApiRef);\n const [requestedFilters, setRequestedFilters] = useState<EntityFilters>(\n {} as EntityFilters,\n );\n\n // We use react-router's useLocation hook so updates from external sources trigger an update to\n // the queryParameters in outputState. Updates from this hook use replaceState below and won't\n // trigger a useLocation change; this would instead come from an external source, such as a manual\n // update of the URL or two catalog sidebar links with different catalog filters.\n const location = useLocation();\n\n const getPaginationMode = (): PaginationMode => {\n if (props.pagination === true) {\n return 'cursor';\n }\n return typeof props.pagination === 'object'\n ? props.pagination.mode ?? 'cursor'\n : 'none';\n };\n\n const paginationMode = getPaginationMode();\n const paginationLimit =\n typeof props.pagination === 'object' ? props.pagination.limit ?? 20 : 20;\n\n const {\n queryParameters,\n cursor: initialCursor,\n offset: initialOffset,\n limit: initialLimit,\n } = useMemo(() => {\n const parsed = qs.parse(location.search, {\n ignoreQueryPrefix: true,\n arrayLimit: 10000,\n });\n\n let limit = paginationLimit;\n if (typeof parsed.limit === 'string') {\n const queryLimit = Number.parseInt(parsed.limit, 10);\n if (!isNaN(queryLimit)) {\n limit = queryLimit;\n }\n }\n\n const offset =\n typeof parsed.offset === 'string' && paginationMode === 'offset'\n ? Number.parseInt(parsed.offset, 10)\n : undefined;\n\n return {\n queryParameters: (parsed.filters ?? {}) as Record<\n string,\n string | string[]\n >,\n cursor:\n typeof parsed.cursor === 'string' && paginationMode === 'cursor'\n ? parsed.cursor\n : undefined,\n offset:\n paginationMode === 'offset' && offset && !isNaN(offset)\n ? offset\n : undefined,\n limit,\n };\n }, [paginationMode, location.search, paginationLimit]);\n\n const [cursor, setCursor] = useState(initialCursor);\n const [offset, setOffset] = useState<number | undefined>(initialOffset);\n const [limit, setLimit] = useState(initialLimit);\n\n const [outputState, setOutputState] = useState<OutputState<EntityFilters>>(\n () => {\n return {\n appliedFilters: {} as EntityFilters,\n entities: [],\n backendEntities: [],\n pageInfo: {},\n offset,\n limit,\n };\n },\n );\n\n // The main async filter worker. Note that while it has a lot of dependencies\n // in terms of its implementation, the triggering only happens (debounced)\n // based on the requested filters changing.\n const [{ value: resolvedValue, loading, error }, refresh] = useAsyncFn(\n async () => {\n const kindValue =\n requestedFilters.kind?.value?.toLocaleLowerCase('en-US');\n const adjustedFilters =\n kindValue === 'user' || kindValue === 'group'\n ? { ...requestedFilters, owners: undefined }\n : requestedFilters;\n const compacted = compact(Object.values(adjustedFilters));\n const entityFilter = reduceEntityFilters(compacted);\n\n if (paginationMode !== 'none') {\n if (cursor) {\n if (cursor !== outputState.appliedCursor) {\n const response = await catalogApi.queryEntities({\n cursor,\n limit,\n });\n return {\n appliedFilters: requestedFilters,\n appliedCursor: cursor,\n backendEntities: response.items,\n entities: response.items.filter(entityFilter),\n pageInfo: response.pageInfo,\n totalItems: response.totalItems,\n };\n }\n const entities = outputState.backendEntities.filter(entityFilter);\n return {\n appliedFilters: requestedFilters,\n appliedCursor: outputState.appliedCursor,\n backendEntities: outputState.backendEntities,\n entities,\n pageInfo: outputState.pageInfo,\n totalItems: outputState.totalItems,\n limit: outputState.limit,\n offset: outputState.offset,\n };\n }\n\n const backendFilter = reduceCatalogFilters(compacted);\n const previousBackendFilter = reduceCatalogFilters(\n compact(Object.values(outputState.appliedFilters)),\n );\n\n if (\n (paginationMode === 'offset' &&\n (outputState.limit !== limit || outputState.offset !== offset)) ||\n !isEqual(previousBackendFilter, backendFilter)\n ) {\n const response = await catalogApi.queryEntities({\n ...backendFilter,\n limit,\n offset,\n });\n return {\n appliedFilters: requestedFilters,\n backendEntities: response.items,\n entities: response.items.filter(entityFilter),\n pageInfo: response.pageInfo,\n totalItems: response.totalItems,\n limit,\n offset,\n };\n }\n const entities = outputState.backendEntities.filter(entityFilter);\n return {\n appliedFilters: requestedFilters,\n backendEntities: outputState.backendEntities,\n entities,\n pageInfo: outputState.pageInfo,\n totalItems: outputState.totalItems,\n limit: outputState.limit,\n offset: outputState.offset,\n };\n }\n\n const backendFilter = reduceBackendCatalogFilters(compacted);\n const { orderFields } = reduceCatalogFilters(compacted);\n const previousBackendFilter = reduceBackendCatalogFilters(\n compact(Object.values(outputState.appliedFilters)),\n );\n\n // TODO(mtlewis): currently entities will never be requested unless\n // there's at least one filter, we should allow an initial request\n // to happen with no filters.\n if (!isEqual(previousBackendFilter, backendFilter)) {\n // TODO(timbonicus): should limit fields here, but would need filter\n // fields + table columns\n const response = await catalogApi.getEntities({\n filter: backendFilter,\n order: orderFields,\n });\n const entities = response.items.filter(entityFilter);\n return {\n appliedFilters: requestedFilters,\n backendEntities: response.items,\n entities,\n totalItems: entities.length,\n };\n }\n const entities = outputState.backendEntities.filter(entityFilter);\n return {\n appliedFilters: requestedFilters,\n backendEntities: outputState.backendEntities,\n entities,\n totalItems: entities.length,\n };\n },\n [\n catalogApi,\n queryParameters,\n requestedFilters,\n outputState,\n cursor,\n paginationMode,\n limit,\n offset,\n ],\n { loading: true },\n );\n\n // Slight debounce on the refresh, since (especially on page load) several\n // filters will be calling this in rapid succession.\n useDebounce(refresh, 10, [requestedFilters, cursor, limit, offset]);\n\n useEffect(() => {\n if (resolvedValue === undefined) {\n return;\n }\n setOutputState(resolvedValue);\n if (isMounted()) {\n const queryParams = Object.keys(requestedFilters).reduce(\n (params, key) => {\n const filter = requestedFilters[key as keyof EntityFilters] as\n | EntityFilter\n | undefined;\n if (filter?.toQueryValue) {\n params[key] = filter.toQueryValue();\n }\n return params;\n },\n {} as Record<string, string | string[]>,\n );\n\n const oldParams = qs.parse(location.search, {\n ignoreQueryPrefix: true,\n arrayLimit: 10000,\n });\n const newParams = qs.stringify(\n {\n ...oldParams,\n filters: queryParams,\n ...(paginationMode === 'none' ? {} : { cursor, limit, offset }),\n },\n { addQueryPrefix: true, arrayFormat: 'repeat' },\n );\n const newUrl = `${window.location.pathname}${newParams}`;\n // We use direct history manipulation since useSearchParams and\n // useNavigate in react-router-dom cause unnecessary extra rerenders.\n // Also make sure to replace the state rather than pushing, since we\n // don't want there to be back/forward slots for every single filter\n // change.\n window.history?.replaceState(null, document.title, newUrl);\n }\n }, [\n cursor,\n isMounted,\n limit,\n location.search,\n offset,\n requestedFilters,\n resolvedValue,\n paginationMode,\n ]);\n\n const updateFilters = useCallback(\n (\n update:\n | Partial<EntityFilter>\n | ((prevFilters: EntityFilters) => Partial<EntityFilters>),\n ) => {\n // changing filters will affect pagination, so we need to reset\n // the cursor and start from the first page.\n // TODO(vinzscam): this is currently causing issues at page reload\n // where the state is not kept. Unfortunately we need to rethink\n // the way filters work in order to fix this.\n if (paginationMode === 'cursor') {\n setCursor(undefined);\n } else if (paginationMode === 'offset') {\n // Same thing with offset\n setOffset(0);\n }\n setRequestedFilters(prevFilters => {\n const newFilters =\n typeof update === 'function' ? update(prevFilters) : update;\n return { ...prevFilters, ...newFilters };\n });\n },\n [paginationMode],\n );\n\n // Use resolvedValue directly when available to avoid an extra render cycle.\n // Without this, there's a render where loading has flipped back to false but\n // outputState hasn't been updated yet (it syncs via useEffect), causing a\n // flash of stale data between the loading state and the new results.\n const latestOutput = resolvedValue ?? outputState;\n\n const pageInfo = useMemo(() => {\n if (paginationMode !== 'cursor') {\n return undefined;\n }\n\n const prevCursor = latestOutput.pageInfo?.prevCursor;\n const nextCursor = latestOutput.pageInfo?.nextCursor;\n return {\n prev: prevCursor ? () => setCursor(prevCursor) : undefined,\n next: nextCursor ? () => setCursor(nextCursor) : undefined,\n };\n }, [paginationMode, latestOutput.pageInfo]);\n\n const value = useMemo(\n () => ({\n filters: latestOutput.appliedFilters,\n entities: latestOutput.entities,\n backendEntities: latestOutput.backendEntities,\n updateFilters,\n queryParameters,\n loading,\n error,\n pageInfo,\n totalItems: latestOutput.totalItems,\n limit,\n offset,\n setLimit,\n setOffset,\n paginationMode,\n }),\n [\n latestOutput,\n updateFilters,\n queryParameters,\n loading,\n error,\n pageInfo,\n limit,\n offset,\n paginationMode,\n setLimit,\n setOffset,\n ],\n );\n\n return (\n <OldEntityListContext.Provider value={value}>\n <NewEntityListContext.Provider\n value={createVersionedValueMap({ 1: value })}\n >\n {props.children}\n </NewEntityListContext.Provider>\n </OldEntityListContext.Provider>\n );\n};\n\n/**\n * Hook for interacting with the entity list context provided by the {@link EntityListProvider}.\n * @public\n */\nexport function useEntityList<\n EntityFilters extends DefaultEntityFilters = DefaultEntityFilters,\n>(): EntityListContextProps<EntityFilters> {\n const versionedHolder = useVersionedContext<{\n 1: EntityListContextProps<any>;\n }>('entity-list-context');\n const oldContext = useContext(OldEntityListContext);\n\n if (versionedHolder) {\n const value = versionedHolder.atVersion(1);\n if (!value) {\n throw new Error('EntityListContext v1 not available');\n }\n return value;\n }\n\n if (oldContext) {\n return oldContext;\n }\n\n throw new Error('useEntityList must be used within EntityListProvider');\n}\n"],"names":["limit","offset","entities","backendFilter","previousBackendFilter"],"mappings":"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"}
1
+ {"version":3,"file":"useEntityListProvider.esm.js","sources":["../../src/hooks/useEntityListProvider.tsx"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { QueryEntitiesResponse } from '@backstage/catalog-client';\nimport { Entity } from '@backstage/catalog-model';\nimport { useApi } from '@backstage/core-plugin-api';\nimport {\n createVersionedContext,\n createVersionedValueMap,\n useVersionedContext,\n} from '@backstage/version-bridge';\nimport { compact, isEqual } from 'lodash';\nimport qs from 'qs';\nimport {\n createContext,\n PropsWithChildren,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useLocation } from 'react-router-dom';\nimport useAsyncFn from 'react-use/esm/useAsyncFn';\nimport useDebounce from 'react-use/esm/useDebounce';\nimport useMountedState from 'react-use/esm/useMountedState';\nimport { catalogApiRef } from '../api';\nimport {\n EntityErrorFilter,\n EntityKindFilter,\n EntityLifecycleFilter,\n EntityNamespaceFilter,\n EntityOrderFilter,\n EntityOrphanFilter,\n EntityOwnerFilter,\n EntityTagFilter,\n EntityTextFilter,\n EntityTypeFilter,\n EntityUserFilter,\n UserListFilter,\n} from '../filters';\nimport { EntityFilter, EntityListPagination } from '../types';\nimport {\n reduceBackendCatalogFilters,\n reduceCatalogFilters,\n reduceEntityFilters,\n} from '../utils/filters';\n\n/** @public */\nexport type DefaultEntityFilters = {\n kind?: EntityKindFilter;\n type?: EntityTypeFilter;\n user?: UserListFilter | EntityUserFilter;\n owners?: EntityOwnerFilter;\n lifecycles?: EntityLifecycleFilter;\n tags?: EntityTagFilter;\n text?: EntityTextFilter;\n orphan?: EntityOrphanFilter;\n error?: EntityErrorFilter;\n namespace?: EntityNamespaceFilter;\n order?: EntityOrderFilter;\n};\n\n/** @public */\nexport type PaginationMode = 'cursor' | 'offset' | 'none';\n\n/** @public */\nexport type EntityListContextProps<\n EntityFilters extends DefaultEntityFilters = DefaultEntityFilters,\n> = {\n /**\n * The currently registered filters, adhering to the shape of DefaultEntityFilters or an extension\n * of that default (to add custom filter types).\n */\n filters: EntityFilters;\n\n /**\n * The resolved list of catalog entities, after all filters are applied.\n */\n entities: Entity[];\n\n /**\n * The resolved list of catalog entities, after _only catalog-backend_ filters are applied.\n */\n backendEntities: Entity[];\n\n /**\n * Update one or more of the registered filters. Optional filters can be set to `undefined` to\n * reset the filter.\n */\n updateFilters: (\n filters:\n | Partial<EntityFilters>\n | ((prevFilters: EntityFilters) => Partial<EntityFilters>),\n ) => void;\n\n /**\n * Filter values from query parameters.\n */\n queryParameters: Partial<Record<keyof EntityFilters, string | string[]>>;\n\n loading: boolean;\n error?: Error;\n\n pageInfo?: {\n next?: () => void;\n prev?: () => void;\n };\n totalItems?: number;\n totalItemsLoading: boolean;\n limit: number;\n offset?: number;\n setLimit: (limit: number) => void;\n setOffset?: (offset: number) => void;\n paginationMode: PaginationMode;\n};\n\n// This context has support for multiple concurrent versions of this package.\n// It is currently used in parallel with the old context in order to provide\n// a smooth transition, but will eventually be the only context we use.\nexport const NewEntityListContext = createVersionedContext<{\n 1: EntityListContextProps<any>;\n}>('entity-list-context');\n\n/**\n * Creates new context for entity listing and filtering.\n */\nexport const OldEntityListContext = createContext<\n EntityListContextProps<any> | undefined\n>(undefined);\n\ntype BackendState = {\n backendEntities: Entity[];\n pageInfo?: QueryEntitiesResponse['pageInfo'];\n totalItems?: number;\n};\n\n/**\n * @public\n */\nexport type EntityListProviderProps = PropsWithChildren<{\n pagination?: EntityListPagination;\n}>;\n\n/**\n * Provides entities and filters for a catalog listing.\n * @public\n */\nexport const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(\n props: EntityListProviderProps,\n) => {\n const isMounted = useMountedState();\n const catalogApi = useApi(catalogApiRef);\n const [requestedFilters, setRequestedFilters] = useState<EntityFilters>(\n {} as EntityFilters,\n );\n\n // We use react-router's useLocation hook so updates from external sources trigger an update to\n // the queryParameters in outputState. Updates from this hook use replaceState below and won't\n // trigger a useLocation change; this would instead come from an external source, such as a manual\n // update of the URL or two catalog sidebar links with different catalog filters.\n const location = useLocation();\n\n let paginationMode: PaginationMode = 'none';\n if (props.pagination === true) {\n paginationMode = 'cursor';\n } else if (typeof props.pagination === 'object') {\n paginationMode = props.pagination.mode ?? 'cursor';\n }\n const paginationLimit =\n typeof props.pagination === 'object' ? props.pagination.limit ?? 20 : 20;\n\n const {\n queryParameters,\n cursor: initialCursor,\n offset: initialOffset,\n limit: initialLimit,\n } = useMemo(() => {\n const parsed = qs.parse(location.search, {\n ignoreQueryPrefix: true,\n arrayLimit: 10000,\n });\n\n let limit = paginationLimit;\n if (typeof parsed.limit === 'string') {\n const queryLimit = Number.parseInt(parsed.limit, 10);\n if (!isNaN(queryLimit)) {\n limit = queryLimit;\n }\n }\n\n const offset =\n typeof parsed.offset === 'string' && paginationMode === 'offset'\n ? Number.parseInt(parsed.offset, 10)\n : undefined;\n\n return {\n queryParameters: (parsed.filters ?? {}) as Record<\n string,\n string | string[]\n >,\n cursor:\n typeof parsed.cursor === 'string' && paginationMode === 'cursor'\n ? parsed.cursor\n : undefined,\n offset:\n paginationMode === 'offset' && offset && !isNaN(offset)\n ? offset\n : undefined,\n limit,\n };\n }, [paginationMode, location.search, paginationLimit]);\n\n const [cursor, setCursor] = useState(initialCursor);\n const [offset, setOffset] = useState<number | undefined>(initialOffset);\n const [limit, setLimit] = useState(initialLimit);\n\n const [backendState, setBackendState] = useState<BackendState>({\n backendEntities: [],\n });\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | undefined>();\n\n // Tracks the params of the last API call so identical requests are\n // skipped even when requestedFilters changes (e.g. a label change\n // or a frontend-only filter addition).\n const lastFetchParamsRef = useRef<unknown>(undefined);\n // Generation counter — only the most recent fetch updates state,\n // so out-of-order responses from overlapping requests are discarded.\n const fetchGenRef = useRef(0);\n\n // Adjusted filters remove the owners filter for user/group kinds,\n // since ownership is not meaningful for those entity types.\n const adjustedFilters = useMemo(() => {\n const kindValue = requestedFilters.kind?.value?.toLocaleLowerCase('en-US');\n return kindValue === 'user' || kindValue === 'group'\n ? { ...requestedFilters, owners: undefined }\n : requestedFilters;\n }, [requestedFilters]);\n\n const refresh = async () => {\n const compacted = compact(Object.values(adjustedFilters));\n\n let fetchParams: unknown;\n let doFetch: () => Promise<BackendState>;\n\n if (paginationMode !== 'none') {\n if (cursor) {\n fetchParams = { cursor, limit };\n doFetch = async () => {\n const response = await catalogApi.queryEntities({\n cursor,\n limit,\n totalItems: 'exclude',\n });\n return {\n backendEntities: response.items,\n pageInfo: response.pageInfo,\n };\n };\n } else {\n const backendFilter = reduceCatalogFilters(compacted);\n fetchParams = { ...backendFilter, limit, offset };\n doFetch = async () => {\n const response = await catalogApi.queryEntities({\n ...backendFilter,\n limit,\n offset,\n totalItems: 'exclude',\n });\n return {\n backendEntities: response.items,\n pageInfo: response.pageInfo,\n };\n };\n }\n } else {\n const backendFilter = reduceBackendCatalogFilters(compacted);\n const { orderFields } = reduceCatalogFilters(compacted);\n fetchParams = { filter: backendFilter, order: orderFields };\n doFetch = async () => {\n const response = await catalogApi.getEntities({\n filter: backendFilter,\n order: orderFields,\n });\n return { backendEntities: response.items };\n };\n }\n\n // No filters registered yet — wait for filter components to\n // call updateFilters before making the first request.\n // Exception: a cursor in the URL is a self-contained page reference\n // that doesn't need filter state to be valid.\n if (\n compacted.length === 0 &&\n lastFetchParamsRef.current === undefined &&\n !cursor\n ) {\n setLoading(false);\n return;\n }\n\n if (isEqual(fetchParams, lastFetchParamsRef.current)) {\n return;\n }\n lastFetchParamsRef.current = fetchParams;\n\n const gen = ++fetchGenRef.current;\n setLoading(true);\n setError(undefined);\n\n try {\n const result = await doFetch();\n if (gen === fetchGenRef.current) {\n setBackendState(result);\n }\n } catch (e) {\n if (gen === fetchGenRef.current) {\n // Clear the ref so the same params can be retried, and so\n // a response discarded due to a concurrent request (gen mismatch)\n // doesn't permanently block fetching those params again.\n lastFetchParamsRef.current = undefined;\n setError(e as Error);\n }\n } finally {\n if (gen === fetchGenRef.current) {\n setLoading(false);\n }\n }\n };\n\n // Slight debounce on the refresh, since (especially on page load)\n // several filters will be calling updateFilters in rapid succession.\n useDebounce(refresh, 10, [adjustedFilters, cursor, limit, offset]);\n\n // Fetch the total count separately, only when filters change. This is\n // decoupled from the main list fetch so that page navigation doesn't\n // re-run the expensive count query, and so that the count can arrive\n // asynchronously without blocking the list response.\n const [{ value: totalItems, loading: totalItemsLoading }, refreshCount] =\n useAsyncFn(async () => {\n if (paginationMode === 'none') {\n return undefined;\n }\n const compacted = compact(Object.values(adjustedFilters));\n if (compacted.length === 0) {\n return undefined;\n }\n const backendFilter = reduceCatalogFilters(compacted);\n try {\n const response = await catalogApi.queryEntities({\n ...backendFilter,\n limit: 0,\n });\n return response.totalItems;\n } catch {\n return undefined;\n }\n }, [catalogApi, paginationMode, adjustedFilters]);\n\n useDebounce(refreshCount, 10, [adjustedFilters]);\n\n // Frontend filtering — synchronous, no debounce needed. Updates\n // instantly when requestedFilters or backendEntities change.\n const entities = useMemo(() => {\n const compacted = compact(Object.values(adjustedFilters));\n const entityFilter = reduceEntityFilters(compacted);\n return backendState.backendEntities.filter(entityFilter);\n }, [adjustedFilters, backendState.backendEntities]);\n\n // Sync filter state to URL query parameters. We use direct history\n // manipulation since useSearchParams and useNavigate in\n // react-router-dom cause unnecessary extra rerenders. Also make sure\n // to replace the state rather than pushing, since we don't want\n // there to be back/forward slots for every single filter change.\n useEffect(() => {\n if (!isMounted() || Object.keys(requestedFilters).length === 0) {\n return;\n }\n const queryParams = Object.keys(requestedFilters).reduce((params, key) => {\n const filter = requestedFilters[key as keyof EntityFilters] as\n | EntityFilter\n | undefined;\n if (filter?.toQueryValue) {\n params[key] = filter.toQueryValue();\n }\n return params;\n }, {} as Record<string, string | string[]>);\n\n const oldParams = qs.parse(location.search, {\n ignoreQueryPrefix: true,\n arrayLimit: 10000,\n });\n const newParams = qs.stringify(\n {\n ...oldParams,\n filters: queryParams,\n ...(paginationMode === 'none' ? {} : { cursor, limit, offset }),\n },\n { addQueryPrefix: true, arrayFormat: 'repeat' },\n );\n const newUrl = `${window.location.pathname}${newParams}`;\n window.history?.replaceState(null, document.title, newUrl);\n }, [\n cursor,\n isMounted,\n limit,\n location.search,\n offset,\n requestedFilters,\n paginationMode,\n ]);\n\n const updateFilters = useCallback(\n (\n update:\n | Partial<EntityFilter>\n | ((prevFilters: EntityFilters) => Partial<EntityFilters>),\n ) => {\n // changing filters will affect pagination, so we need to reset\n // the cursor and start from the first page.\n // TODO(vinzscam): this is currently causing issues at page reload\n // where the state is not kept. Unfortunately we need to rethink\n // the way filters work in order to fix this.\n if (paginationMode === 'cursor') {\n setCursor(undefined);\n } else if (paginationMode === 'offset') {\n // Same thing with offset\n setOffset(0);\n }\n setRequestedFilters(prevFilters => {\n const newFilters =\n typeof update === 'function' ? update(prevFilters) : update;\n return { ...prevFilters, ...newFilters };\n });\n },\n [paginationMode],\n );\n\n const pageInfo = useMemo(() => {\n if (paginationMode !== 'cursor') {\n return undefined;\n }\n\n const prevCursor = backendState.pageInfo?.prevCursor;\n const nextCursor = backendState.pageInfo?.nextCursor;\n return {\n prev: prevCursor ? () => setCursor(prevCursor) : undefined,\n next: nextCursor ? () => setCursor(nextCursor) : undefined,\n };\n }, [paginationMode, backendState.pageInfo]);\n\n const value = useMemo(\n () => ({\n filters: requestedFilters,\n entities,\n backendEntities: backendState.backendEntities,\n updateFilters,\n queryParameters,\n loading,\n error,\n pageInfo,\n totalItems:\n paginationMode === 'none'\n ? entities.length\n : totalItems ?? backendState.totalItems,\n totalItemsLoading: paginationMode !== 'none' && totalItemsLoading,\n limit,\n offset,\n setLimit,\n setOffset,\n paginationMode,\n }),\n [\n requestedFilters,\n entities,\n backendState,\n totalItems,\n totalItemsLoading,\n updateFilters,\n queryParameters,\n loading,\n error,\n pageInfo,\n paginationMode,\n limit,\n offset,\n setLimit,\n setOffset,\n ],\n );\n\n return (\n <OldEntityListContext.Provider value={value}>\n <NewEntityListContext.Provider\n value={createVersionedValueMap({ 1: value })}\n >\n {props.children}\n </NewEntityListContext.Provider>\n </OldEntityListContext.Provider>\n );\n};\n\n/**\n * Hook for interacting with the entity list context provided by the {@link EntityListProvider}.\n * @public\n */\nexport function useEntityList<\n EntityFilters extends DefaultEntityFilters = DefaultEntityFilters,\n>(): EntityListContextProps<EntityFilters> {\n const versionedHolder = useVersionedContext<{\n 1: EntityListContextProps<any>;\n }>('entity-list-context');\n const oldContext = useContext(OldEntityListContext);\n\n if (versionedHolder) {\n const value = versionedHolder.atVersion(1);\n if (!value) {\n throw new Error('EntityListContext v1 not available');\n }\n return value;\n }\n\n if (oldContext) {\n return oldContext;\n }\n\n throw new Error('useEntityList must be used within EntityListProvider');\n}\n"],"names":["limit","offset"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -880,6 +880,7 @@ type EntityListContextProps<EntityFilters extends DefaultEntityFilters = Default
880
880
  prev?: () => void;
881
881
  };
882
882
  totalItems?: number;
883
+ totalItemsLoading: boolean;
883
884
  limit: number;
884
885
  offset?: number;
885
886
  setLimit: (limit: number) => void;
@@ -33,6 +33,7 @@ function MockEntityListContextProvider(props) {
33
33
  queryParameters: value?.queryParameters ?? defaultValues.queryParameters,
34
34
  error: value?.error,
35
35
  totalItems: value?.totalItems ?? (value?.entities ?? defaultValues.entities).length,
36
+ totalItemsLoading: value?.totalItemsLoading ?? false,
36
37
  limit: value?.limit ?? 20,
37
38
  offset: value?.offset,
38
39
  setLimit: value?.setLimit ?? (() => {
@@ -1 +1 @@
1
- {"version":3,"file":"MockEntityListContextProvider.esm.js","sources":["../../src/testUtils/MockEntityListContextProvider.tsx"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PropsWithChildren, useCallback, useMemo, useState } from 'react';\nimport {\n DefaultEntityFilters,\n EntityListContextProps,\n} from '@backstage/plugin-catalog-react';\nimport { createVersionedValueMap } from '@backstage/version-bridge';\nimport { NewEntityListContext } from '../hooks/useEntityListProvider';\n\n/**\n * Simplifies testing of code that uses the entity list hooks.\n *\n * @public\n */\nexport function MockEntityListContextProvider<\n T extends DefaultEntityFilters = DefaultEntityFilters,\n>(\n props: PropsWithChildren<{\n value?: Partial<EntityListContextProps<T>>;\n }>,\n) {\n const { children, value } = props;\n\n // Provides a default implementation that stores filter state, for testing components that\n // reflect filter state.\n const [filters, setFilters] = useState<T>(value?.filters ?? ({} as T));\n\n const updateFilters = useCallback(\n (update: Partial<T> | ((prevFilters: T) => Partial<T>)) => {\n setFilters(prevFilters => {\n const newFilters =\n typeof update === 'function' ? update(prevFilters) : update;\n return { ...prevFilters, ...newFilters };\n });\n },\n [],\n );\n\n // Memoize the default values since pickers have useEffect triggers on these; naively defaulting\n // below with `?? <X>` breaks referential equality on subsequent updates.\n const defaultValues = useMemo(\n () => ({\n entities: [],\n backendEntities: [],\n queryParameters: {},\n }),\n [],\n );\n\n const resolvedValue: EntityListContextProps<T> = useMemo(\n () => ({\n entities: value?.entities ?? defaultValues.entities,\n backendEntities: value?.backendEntities ?? defaultValues.backendEntities,\n updateFilters: value?.updateFilters ?? updateFilters,\n filters,\n loading: value?.loading ?? false,\n queryParameters: value?.queryParameters ?? defaultValues.queryParameters,\n error: value?.error,\n totalItems:\n value?.totalItems ?? (value?.entities ?? defaultValues.entities).length,\n limit: value?.limit ?? 20,\n offset: value?.offset,\n setLimit: value?.setLimit ?? (() => {}),\n setOffset: value?.setOffset,\n paginationMode: value?.paginationMode ?? 'none',\n }),\n [value, defaultValues, filters, updateFilters],\n );\n\n return (\n <NewEntityListContext.Provider\n value={createVersionedValueMap({ 1: resolvedValue })}\n >\n {children}\n </NewEntityListContext.Provider>\n );\n}\n"],"names":[],"mappings":";;;;;AA6BO,SAAS,8BAGd,KAAA,EAGA;AACA,EAAA,MAAM,EAAE,QAAA,EAAU,KAAA,EAAM,GAAI,KAAA;AAI5B,EAAA,MAAM,CAAC,SAAS,UAAU,CAAA,GAAI,SAAY,KAAA,EAAO,OAAA,IAAY,EAAQ,CAAA;AAErE,EAAA,MAAM,aAAA,GAAgB,WAAA;AAAA,IACpB,CAAC,MAAA,KAA0D;AACzD,MAAA,UAAA,CAAW,CAAA,WAAA,KAAe;AACxB,QAAA,MAAM,aACJ,OAAO,MAAA,KAAW,UAAA,GAAa,MAAA,CAAO,WAAW,CAAA,GAAI,MAAA;AACvD,QAAA,OAAO,EAAE,GAAG,WAAA,EAAa,GAAG,UAAA,EAAW;AAAA,MACzC,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA;AAAC,GACH;AAIA,EAAA,MAAM,aAAA,GAAgB,OAAA;AAAA,IACpB,OAAO;AAAA,MACL,UAAU,EAAC;AAAA,MACX,iBAAiB,EAAC;AAAA,MAClB,iBAAiB;AAAC,KACpB,CAAA;AAAA,IACA;AAAC,GACH;AAEA,EAAA,MAAM,aAAA,GAA2C,OAAA;AAAA,IAC/C,OAAO;AAAA,MACL,QAAA,EAAU,KAAA,EAAO,QAAA,IAAY,aAAA,CAAc,QAAA;AAAA,MAC3C,eAAA,EAAiB,KAAA,EAAO,eAAA,IAAmB,aAAA,CAAc,eAAA;AAAA,MACzD,aAAA,EAAe,OAAO,aAAA,IAAiB,aAAA;AAAA,MACvC,OAAA;AAAA,MACA,OAAA,EAAS,OAAO,OAAA,IAAW,KAAA;AAAA,MAC3B,eAAA,EAAiB,KAAA,EAAO,eAAA,IAAmB,aAAA,CAAc,eAAA;AAAA,MACzD,OAAO,KAAA,EAAO,KAAA;AAAA,MACd,YACE,KAAA,EAAO,UAAA,IAAA,CAAe,KAAA,EAAO,QAAA,IAAY,cAAc,QAAA,EAAU,MAAA;AAAA,MACnE,KAAA,EAAO,OAAO,KAAA,IAAS,EAAA;AAAA,MACvB,QAAQ,KAAA,EAAO,MAAA;AAAA,MACf,QAAA,EAAU,KAAA,EAAO,QAAA,KAAa,MAAM;AAAA,MAAC,CAAA,CAAA;AAAA,MACrC,WAAW,KAAA,EAAO,SAAA;AAAA,MAClB,cAAA,EAAgB,OAAO,cAAA,IAAkB;AAAA,KAC3C,CAAA;AAAA,IACA,CAAC,KAAA,EAAO,aAAA,EAAe,OAAA,EAAS,aAAa;AAAA,GAC/C;AAEA,EAAA,uBACE,GAAA;AAAA,IAAC,oBAAA,CAAqB,QAAA;AAAA,IAArB;AAAA,MACC,KAAA,EAAO,uBAAA,CAAwB,EAAE,CAAA,EAAG,eAAe,CAAA;AAAA,MAElD;AAAA;AAAA,GACH;AAEJ;;;;"}
1
+ {"version":3,"file":"MockEntityListContextProvider.esm.js","sources":["../../src/testUtils/MockEntityListContextProvider.tsx"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PropsWithChildren, useCallback, useMemo, useState } from 'react';\nimport {\n DefaultEntityFilters,\n EntityListContextProps,\n} from '@backstage/plugin-catalog-react';\nimport { createVersionedValueMap } from '@backstage/version-bridge';\nimport { NewEntityListContext } from '../hooks/useEntityListProvider';\n\n/**\n * Simplifies testing of code that uses the entity list hooks.\n *\n * @public\n */\nexport function MockEntityListContextProvider<\n T extends DefaultEntityFilters = DefaultEntityFilters,\n>(\n props: PropsWithChildren<{\n value?: Partial<EntityListContextProps<T>>;\n }>,\n) {\n const { children, value } = props;\n\n // Provides a default implementation that stores filter state, for testing components that\n // reflect filter state.\n const [filters, setFilters] = useState<T>(value?.filters ?? ({} as T));\n\n const updateFilters = useCallback(\n (update: Partial<T> | ((prevFilters: T) => Partial<T>)) => {\n setFilters(prevFilters => {\n const newFilters =\n typeof update === 'function' ? update(prevFilters) : update;\n return { ...prevFilters, ...newFilters };\n });\n },\n [],\n );\n\n // Memoize the default values since pickers have useEffect triggers on these; naively defaulting\n // below with `?? <X>` breaks referential equality on subsequent updates.\n const defaultValues = useMemo(\n () => ({\n entities: [],\n backendEntities: [],\n queryParameters: {},\n }),\n [],\n );\n\n const resolvedValue: EntityListContextProps<T> = useMemo(\n () => ({\n entities: value?.entities ?? defaultValues.entities,\n backendEntities: value?.backendEntities ?? defaultValues.backendEntities,\n updateFilters: value?.updateFilters ?? updateFilters,\n filters,\n loading: value?.loading ?? false,\n queryParameters: value?.queryParameters ?? defaultValues.queryParameters,\n error: value?.error,\n totalItems:\n value?.totalItems ?? (value?.entities ?? defaultValues.entities).length,\n totalItemsLoading: value?.totalItemsLoading ?? false,\n limit: value?.limit ?? 20,\n offset: value?.offset,\n setLimit: value?.setLimit ?? (() => {}),\n setOffset: value?.setOffset,\n paginationMode: value?.paginationMode ?? 'none',\n }),\n [value, defaultValues, filters, updateFilters],\n );\n\n return (\n <NewEntityListContext.Provider\n value={createVersionedValueMap({ 1: resolvedValue })}\n >\n {children}\n </NewEntityListContext.Provider>\n );\n}\n"],"names":[],"mappings":";;;;;AA6BO,SAAS,8BAGd,KAAA,EAGA;AACA,EAAA,MAAM,EAAE,QAAA,EAAU,KAAA,EAAM,GAAI,KAAA;AAI5B,EAAA,MAAM,CAAC,SAAS,UAAU,CAAA,GAAI,SAAY,KAAA,EAAO,OAAA,IAAY,EAAQ,CAAA;AAErE,EAAA,MAAM,aAAA,GAAgB,WAAA;AAAA,IACpB,CAAC,MAAA,KAA0D;AACzD,MAAA,UAAA,CAAW,CAAA,WAAA,KAAe;AACxB,QAAA,MAAM,aACJ,OAAO,MAAA,KAAW,UAAA,GAAa,MAAA,CAAO,WAAW,CAAA,GAAI,MAAA;AACvD,QAAA,OAAO,EAAE,GAAG,WAAA,EAAa,GAAG,UAAA,EAAW;AAAA,MACzC,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA;AAAC,GACH;AAIA,EAAA,MAAM,aAAA,GAAgB,OAAA;AAAA,IACpB,OAAO;AAAA,MACL,UAAU,EAAC;AAAA,MACX,iBAAiB,EAAC;AAAA,MAClB,iBAAiB;AAAC,KACpB,CAAA;AAAA,IACA;AAAC,GACH;AAEA,EAAA,MAAM,aAAA,GAA2C,OAAA;AAAA,IAC/C,OAAO;AAAA,MACL,QAAA,EAAU,KAAA,EAAO,QAAA,IAAY,aAAA,CAAc,QAAA;AAAA,MAC3C,eAAA,EAAiB,KAAA,EAAO,eAAA,IAAmB,aAAA,CAAc,eAAA;AAAA,MACzD,aAAA,EAAe,OAAO,aAAA,IAAiB,aAAA;AAAA,MACvC,OAAA;AAAA,MACA,OAAA,EAAS,OAAO,OAAA,IAAW,KAAA;AAAA,MAC3B,eAAA,EAAiB,KAAA,EAAO,eAAA,IAAmB,aAAA,CAAc,eAAA;AAAA,MACzD,OAAO,KAAA,EAAO,KAAA;AAAA,MACd,YACE,KAAA,EAAO,UAAA,IAAA,CAAe,KAAA,EAAO,QAAA,IAAY,cAAc,QAAA,EAAU,MAAA;AAAA,MACnE,iBAAA,EAAmB,OAAO,iBAAA,IAAqB,KAAA;AAAA,MAC/C,KAAA,EAAO,OAAO,KAAA,IAAS,EAAA;AAAA,MACvB,QAAQ,KAAA,EAAO,MAAA;AAAA,MACf,QAAA,EAAU,KAAA,EAAO,QAAA,KAAa,MAAM;AAAA,MAAC,CAAA,CAAA;AAAA,MACrC,WAAW,KAAA,EAAO,SAAA;AAAA,MAClB,cAAA,EAAgB,OAAO,cAAA,IAAkB;AAAA,KAC3C,CAAA;AAAA,IACA,CAAC,KAAA,EAAO,aAAA,EAAe,OAAA,EAAS,aAAa;AAAA,GAC/C;AAEA,EAAA,uBACE,GAAA;AAAA,IAAC,oBAAA,CAAqB,QAAA;AAAA,IAArB;AAAA,MACC,KAAA,EAAO,uBAAA,CAAwB,EAAE,CAAA,EAAG,eAAe,CAAA;AAAA,MAElD;AAAA;AAAA,GACH;AAEJ;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-react",
3
- "version": "2.1.5-next.1",
3
+ "version": "3.0.1-next.0",
4
4
  "description": "A frontend library that helps other Backstage plugins interact with the catalog",
5
5
  "backstage": {
6
6
  "role": "web-library",
@@ -73,19 +73,19 @@
73
73
  "test": "backstage-cli package test"
74
74
  },
75
75
  "dependencies": {
76
- "@backstage/catalog-client": "1.15.1-next.0",
77
- "@backstage/catalog-model": "1.8.1-next.1",
78
- "@backstage/core-compat-api": "0.5.11-next.0",
79
- "@backstage/core-components": "0.18.10-next.0",
80
- "@backstage/core-plugin-api": "1.12.6-next.1",
81
- "@backstage/errors": "1.3.1-next.0",
82
- "@backstage/filter-predicates": "0.1.3-next.0",
83
- "@backstage/frontend-plugin-api": "0.17.0-next.1",
84
- "@backstage/integration-react": "1.2.18-next.0",
85
- "@backstage/plugin-permission-common": "0.9.9-next.1",
86
- "@backstage/plugin-permission-react": "0.5.1-next.0",
76
+ "@backstage/catalog-client": "1.16.0-next.0",
77
+ "@backstage/catalog-model": "1.9.0",
78
+ "@backstage/core-compat-api": "0.5.12-next.0",
79
+ "@backstage/core-components": "0.18.11-next.0",
80
+ "@backstage/core-plugin-api": "1.12.6",
81
+ "@backstage/errors": "1.3.1",
82
+ "@backstage/filter-predicates": "0.1.3",
83
+ "@backstage/frontend-plugin-api": "0.17.0",
84
+ "@backstage/integration-react": "1.2.19-next.0",
85
+ "@backstage/plugin-permission-common": "0.9.9",
86
+ "@backstage/plugin-permission-react": "0.5.1",
87
87
  "@backstage/types": "1.2.2",
88
- "@backstage/ui": "0.15.0-next.1",
88
+ "@backstage/ui": "0.15.0",
89
89
  "@backstage/version-bridge": "1.0.12",
90
90
  "@material-ui/core": "^4.12.2",
91
91
  "@material-ui/icons": "^4.9.1",
@@ -95,19 +95,19 @@
95
95
  "classnames": "^2.2.6",
96
96
  "lodash": "^4.17.21",
97
97
  "material-ui-popup-state": "^5.3.6",
98
- "qs": "^6.9.4",
98
+ "qs": "^6.15.2",
99
99
  "react-use": "^17.2.4",
100
100
  "yaml": "^2.0.0",
101
101
  "zen-observable": "^0.10.0",
102
102
  "zod": "^4.0.0"
103
103
  },
104
104
  "devDependencies": {
105
- "@backstage/cli": "0.36.2-next.1",
106
- "@backstage/core-app-api": "1.20.1-next.0",
107
- "@backstage/frontend-test-utils": "0.5.3-next.1",
108
- "@backstage/plugin-catalog-common": "1.1.10-next.0",
109
- "@backstage/plugin-scaffolder-common": "2.1.1-next.0",
110
- "@backstage/test-utils": "1.7.18-next.0",
105
+ "@backstage/cli": "0.36.3-next.0",
106
+ "@backstage/core-app-api": "1.20.1",
107
+ "@backstage/frontend-test-utils": "0.6.1-next.0",
108
+ "@backstage/plugin-catalog-common": "1.1.10",
109
+ "@backstage/plugin-scaffolder-common": "2.2.1-next.0",
110
+ "@backstage/test-utils": "1.7.18",
111
111
  "@testing-library/dom": "^10.0.0",
112
112
  "@testing-library/jest-dom": "^6.0.0",
113
113
  "@testing-library/react": "^16.0.0",
@@ -120,7 +120,7 @@
120
120
  "react-test-renderer": "^16.13.1"
121
121
  },
122
122
  "peerDependencies": {
123
- "@backstage/frontend-test-utils": "0.5.3-next.1",
123
+ "@backstage/frontend-test-utils": "0.6.1-next.0",
124
124
  "@types/react": "^17.0.0 || ^18.0.0",
125
125
  "react": "^17.0.0 || ^18.0.0",
126
126
  "react-dom": "^17.0.0 || ^18.0.0",