@backstage/plugin-catalog-react 1.2.4-next.1 → 1.2.4-next.2

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,24 @@
1
1
  # @backstage/plugin-catalog-react
2
2
 
3
+ ## 1.2.4-next.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 516b2039b6: Add a reusable pop over `EntityPeekAheadPopover` component. It shows more details about the associated entity. See the playbook here https://backstage.io/storybook/?path=/story/catalog-entitypeekaheadpopover--default
8
+ - Updated dependencies
9
+ - @backstage/core-plugin-api@1.3.0-next.1
10
+ - @backstage/catalog-client@1.3.0-next.2
11
+ - @backstage/plugin-permission-react@0.4.9-next.1
12
+ - @backstage/catalog-model@1.1.5-next.1
13
+ - @backstage/core-components@0.12.3-next.2
14
+ - @backstage/errors@1.1.4
15
+ - @backstage/integration@1.4.2-next.0
16
+ - @backstage/theme@0.2.16
17
+ - @backstage/types@1.0.2
18
+ - @backstage/version-bridge@1.0.3
19
+ - @backstage/plugin-catalog-common@1.0.10-next.1
20
+ - @backstage/plugin-permission-common@0.7.3-next.0
21
+
3
22
  ## 1.2.4-next.1
4
23
 
5
24
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-react",
3
- "version": "1.2.4-next.1",
3
+ "version": "1.2.4-next.2",
4
4
  "main": "../dist/index.esm.js",
5
5
  "types": "../dist/index.alpha.d.ts"
6
6
  }
@@ -286,6 +286,23 @@ export declare class EntityOwnerFilter implements EntityFilter {
286
286
  /** @public */
287
287
  export declare const EntityOwnerPicker: () => JSX.Element | null;
288
288
 
289
+ /**
290
+ * Shows an entity popover on hover of a component.
291
+ *
292
+ * @public
293
+ */
294
+ export declare const EntityPeekAheadPopover: (props: EntityPeekAheadPopoverProps) => JSX.Element;
295
+
296
+ /**
297
+ * Properties for an entity popover on hover of a component.
298
+ *
299
+ * @public
300
+ */
301
+ export declare type EntityPeekAheadPopoverProps = PropsWithChildren<{
302
+ entityRef: string;
303
+ delayTime?: number;
304
+ }>;
305
+
289
306
  /** @public */
290
307
  export declare const EntityProcessingStatusPicker: () => JSX.Element;
291
308
 
@@ -286,6 +286,23 @@ export declare class EntityOwnerFilter implements EntityFilter {
286
286
  /** @public */
287
287
  export declare const EntityOwnerPicker: () => JSX.Element | null;
288
288
 
289
+ /**
290
+ * Shows an entity popover on hover of a component.
291
+ *
292
+ * @public
293
+ */
294
+ export declare const EntityPeekAheadPopover: (props: EntityPeekAheadPopoverProps) => JSX.Element;
295
+
296
+ /**
297
+ * Properties for an entity popover on hover of a component.
298
+ *
299
+ * @public
300
+ */
301
+ export declare type EntityPeekAheadPopoverProps = PropsWithChildren<{
302
+ entityRef: string;
303
+ delayTime?: number;
304
+ }>;
305
+
289
306
  /** @public */
290
307
  export declare const EntityProcessingStatusPicker: () => JSX.Element;
291
308
 
package/dist/index.d.ts CHANGED
@@ -286,6 +286,23 @@ export declare class EntityOwnerFilter implements EntityFilter {
286
286
  /** @public */
287
287
  export declare const EntityOwnerPicker: () => JSX.Element | null;
288
288
 
289
+ /**
290
+ * Shows an entity popover on hover of a component.
291
+ *
292
+ * @public
293
+ */
294
+ export declare const EntityPeekAheadPopover: (props: EntityPeekAheadPopoverProps) => JSX.Element;
295
+
296
+ /**
297
+ * Properties for an entity popover on hover of a component.
298
+ *
299
+ * @public
300
+ */
301
+ export declare type EntityPeekAheadPopoverProps = PropsWithChildren<{
302
+ entityRef: string;
303
+ delayTime?: number;
304
+ }>;
305
+
289
306
  /** @public */
290
307
  export declare const EntityProcessingStatusPicker: () => JSX.Element;
291
308
 
package/dist/index.esm.js CHANGED
@@ -1,14 +1,14 @@
1
1
  export { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
2
- import { createApiRef, createRouteRef, useRouteRef, useApi, AnalyticsContext, identityApiRef, alertApiRef, useApp, configApiRef } from '@backstage/core-plugin-api';
2
+ import { createApiRef, createRouteRef, useRouteRef, useApi, AnalyticsContext, identityApiRef, alertApiRef, useApiHolder, useApp, configApiRef } from '@backstage/core-plugin-api';
3
3
  import ObservableImpl from 'zen-observable';
4
4
  import React, { useState, forwardRef, createContext, useMemo, useCallback, useContext, useEffect, useRef, useLayoutEffect, Fragment } from 'react';
5
- import { Grid, useMediaQuery, useTheme, Button, Drawer, Box, Typography, Tooltip, makeStyles, FormControlLabel, Checkbox, TextField, Toolbar, FormControl, Input, InputAdornment, IconButton, withStyles, DialogContentText, ListItemText as ListItemText$1, ListSubheader as ListSubheader$1, Card, CardContent, ListItem, ListItemIcon, List, Dialog, DialogTitle, DialogContent, Tabs, Tab, DialogActions, Divider, MenuItem, ListItemSecondaryAction } from '@material-ui/core';
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';
6
6
  import FilterListIcon from '@material-ui/icons/FilterList';
7
- import { Link, Progress, ErrorPanel, Select, OverflowTooltip, Table, ResponseErrorPanel, DependencyGraph, DependencyGraphTypes, CodeSnippet } from '@backstage/core-components';
8
- import { DEFAULT_NAMESPACE, parseEntityRef, ANNOTATION_SOURCE_LOCATION, parseLocationRef, RELATION_MEMBER_OF, getCompoundEntityRef, stringifyEntityRef, RELATION_OWNED_BY, RELATION_PART_OF, ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model';
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_MEMBER_OF, getCompoundEntityRef, stringifyEntityRef, RELATION_OWNED_BY, isUserEntity, isGroupEntity, RELATION_PART_OF, ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model';
9
9
  import { getOrCreateGlobalSingleton, createVersionedContext, createVersionedValueMap, useVersionedContext } from '@backstage/version-bridge';
10
10
  import useAsync from 'react-use/lib/useAsync';
11
- import { compact, isEqual, groupBy, chunk } from 'lodash';
11
+ import { compact, isEqual, groupBy, chunk, debounce } from 'lodash';
12
12
  import qs from 'qs';
13
13
  import { useLocation, useNavigate } from 'react-router-dom';
14
14
  import useAsyncFn from 'react-use/lib/useAsyncFn';
@@ -22,6 +22,10 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
22
22
  import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
23
23
  import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
24
24
  import { Autocomplete, Alert } from '@material-ui/lab';
25
+ import HoverPopover from 'material-ui-popup-state/HoverPopover';
26
+ import { usePopupState, bindHover, bindPopover } from 'material-ui-popup-state/hooks';
27
+ import InfoIcon from '@material-ui/icons/Info';
28
+ import EmailIcon from '@material-ui/icons/Email';
25
29
  import Clear from '@material-ui/icons/Clear';
26
30
  import Search from '@material-ui/icons/Search';
27
31
  import Star from '@material-ui/icons/Star';
@@ -338,6 +342,7 @@ class EntityTypeFilter {
338
342
  constructor(value) {
339
343
  this.value = value;
340
344
  }
345
+ // Simplify `string | string[]` for consumers, always returns an array
341
346
  getTypes() {
342
347
  return Array.isArray(this.value) ? this.value : [this.value];
343
348
  }
@@ -725,6 +730,7 @@ function useRelatedEntities(entity, relationFilter) {
725
730
  const batchedRelationsByKindAndNamespace = [];
726
731
  for (const rs of relationsByKindAndNamespace) {
727
732
  batchedRelationsByKindAndNamespace.push({
733
+ // All relations in a group have the same kind and namespace, so its arbitrary which we pick
728
734
  kind: rs[0].target.kind,
729
735
  namespace: rs[0].target.namespace,
730
736
  nameBatches: chunk(
@@ -958,7 +964,7 @@ const EntityKindPicker = (props) => {
958
964
  ));
959
965
  };
960
966
 
961
- const useStyles$c = makeStyles(
967
+ const useStyles$d = makeStyles(
962
968
  {
963
969
  input: {}
964
970
  },
@@ -970,7 +976,7 @@ const icon$3 = /* @__PURE__ */ React.createElement(CheckBoxOutlineBlankIcon, { f
970
976
  const checkedIcon$3 = /* @__PURE__ */ React.createElement(CheckBoxIcon, { fontSize: "small" });
971
977
  const EntityLifecyclePicker = () => {
972
978
  var _a, _b;
973
- const classes = useStyles$c();
979
+ const classes = useStyles$d();
974
980
  const {
975
981
  updateFilters,
976
982
  backendEntities,
@@ -1042,7 +1048,7 @@ const EntityLifecyclePicker = () => {
1042
1048
  )));
1043
1049
  };
1044
1050
 
1045
- const useStyles$b = makeStyles(
1051
+ const useStyles$c = makeStyles(
1046
1052
  {
1047
1053
  input: {}
1048
1054
  },
@@ -1054,7 +1060,7 @@ const icon$2 = /* @__PURE__ */ React.createElement(CheckBoxOutlineBlankIcon, { f
1054
1060
  const checkedIcon$2 = /* @__PURE__ */ React.createElement(CheckBoxIcon, { fontSize: "small" });
1055
1061
  const EntityOwnerPicker = () => {
1056
1062
  var _a, _b;
1057
- const classes = useStyles$b();
1063
+ const classes = useStyles$c();
1058
1064
  const {
1059
1065
  updateFilters,
1060
1066
  backendEntities,
@@ -1127,6 +1133,133 @@ const EntityOwnerPicker = () => {
1127
1133
  )));
1128
1134
  };
1129
1135
 
1136
+ const EntityCardActions = ({ entity }) => {
1137
+ const entityRoute = useRouteRef(entityRouteRef);
1138
+ return /* @__PURE__ */ React.createElement(
1139
+ IconButton,
1140
+ {
1141
+ component: Link,
1142
+ "aria-label": "Show",
1143
+ title: "Show details",
1144
+ to: entityRoute({
1145
+ name: entity.metadata.name,
1146
+ namespace: entity.metadata.namespace || "default",
1147
+ kind: entity.kind.toLocaleLowerCase("en-US")
1148
+ })
1149
+ },
1150
+ /* @__PURE__ */ React.createElement(InfoIcon, null)
1151
+ );
1152
+ };
1153
+
1154
+ const EmailCardAction = ({ email }) => {
1155
+ return /* @__PURE__ */ React.createElement(
1156
+ IconButton,
1157
+ {
1158
+ component: Link,
1159
+ "aria-label": "Email",
1160
+ title: `Email ${email}`,
1161
+ to: `mailto:${email}`,
1162
+ target: "_blank"
1163
+ },
1164
+ /* @__PURE__ */ React.createElement(EmailIcon, null)
1165
+ );
1166
+ };
1167
+
1168
+ const GroupCardActions = ({ entity }) => {
1169
+ var _a;
1170
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, ((_a = entity.spec.profile) == null ? void 0 : _a.email) && /* @__PURE__ */ React.createElement(EmailCardAction, { email: entity.spec.profile.email }));
1171
+ };
1172
+
1173
+ const UserCardActions = ({ entity }) => {
1174
+ var _a;
1175
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, ((_a = entity.spec.profile) == null ? void 0 : _a.email) && /* @__PURE__ */ React.createElement(EmailCardAction, { email: entity.spec.profile.email }));
1176
+ };
1177
+
1178
+ const useStyles$b = makeStyles(() => {
1179
+ return {
1180
+ trigger: {
1181
+ display: "inline-block"
1182
+ },
1183
+ popoverPaper: {
1184
+ width: "30em"
1185
+ },
1186
+ descriptionTypography: {
1187
+ overflow: "hidden",
1188
+ textOverflow: "ellipsis",
1189
+ display: "-webkit-box",
1190
+ WebkitLineClamp: 2,
1191
+ WebkitBoxOrient: "vertical"
1192
+ }
1193
+ };
1194
+ });
1195
+ const maxTagChips = 4;
1196
+ const EntityPeekAheadPopover = (props) => {
1197
+ var _a, _b, _c;
1198
+ const { entityRef, children, delayTime = 500 } = props;
1199
+ const classes = useStyles$b();
1200
+ const apiHolder = useApiHolder();
1201
+ const popupState = usePopupState({
1202
+ variant: "popover",
1203
+ popupId: "entity-peek-ahead"
1204
+ });
1205
+ const compoundEntityRef = parseEntityRef(entityRef);
1206
+ const [isHovered, setIsHovered] = useState(false);
1207
+ const debouncedHandleMouseEnter = debounce(
1208
+ () => setIsHovered(true),
1209
+ delayTime
1210
+ );
1211
+ const [{ loading, error, value: entity }, load] = useAsyncFn(async () => {
1212
+ const catalogApi = apiHolder.get(catalogApiRef);
1213
+ if (catalogApi) {
1214
+ const retrievedEntity = await catalogApi.getEntityByRef(
1215
+ compoundEntityRef
1216
+ );
1217
+ if (!retrievedEntity) {
1218
+ throw new Error(`${compoundEntityRef.name} was not found`);
1219
+ }
1220
+ return retrievedEntity;
1221
+ }
1222
+ return void 0;
1223
+ }, [apiHolder, compoundEntityRef]);
1224
+ const handleOnMouseLeave = () => {
1225
+ setIsHovered(false);
1226
+ debouncedHandleMouseEnter.cancel();
1227
+ };
1228
+ useEffect(() => {
1229
+ if (popupState.isOpen && !entity && !error && !loading) {
1230
+ load();
1231
+ }
1232
+ }, [popupState.isOpen, load, entity, error, loading]);
1233
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { onMouseEnter: debouncedHandleMouseEnter }, /* @__PURE__ */ React.createElement("span", { "data-testid": "trigger", ...bindHover(popupState) }, children)), isHovered && /* @__PURE__ */ React.createElement(
1234
+ HoverPopover,
1235
+ {
1236
+ PaperProps: {
1237
+ className: classes.popoverPaper
1238
+ },
1239
+ ...bindPopover(popupState),
1240
+ anchorOrigin: {
1241
+ vertical: "bottom",
1242
+ horizontal: "center"
1243
+ },
1244
+ transformOrigin: {
1245
+ vertical: "top",
1246
+ horizontal: "center"
1247
+ },
1248
+ onMouseLeave: handleOnMouseLeave
1249
+ },
1250
+ /* @__PURE__ */ React.createElement(React.Fragment, null, error && /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error }), /* @__PURE__ */ React.createElement(Card, null, loading && /* @__PURE__ */ React.createElement(Progress, null), /* @__PURE__ */ React.createElement(CardContent, null, entity && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Typography, { color: "textSecondary" }, compoundEntityRef.namespace), /* @__PURE__ */ React.createElement(Typography, { variant: "h5", component: "div" }, compoundEntityRef.name), /* @__PURE__ */ React.createElement(Typography, { color: "textSecondary" }, entity.kind), /* @__PURE__ */ React.createElement(
1251
+ Typography,
1252
+ {
1253
+ className: classes.descriptionTypography,
1254
+ paragraph: true
1255
+ },
1256
+ entity.metadata.description
1257
+ ), /* @__PURE__ */ React.createElement(Typography, null, (_a = entity.spec) == null ? void 0 : _a.type), /* @__PURE__ */ React.createElement(Box, { marginTop: "0.5em" }, (entity.metadata.tags || []).slice(0, maxTagChips).map((tag) => {
1258
+ return /* @__PURE__ */ React.createElement(Chip, { key: tag, size: "small", label: tag });
1259
+ }), ((_b = entity.metadata.tags) == null ? void 0 : _b.length) && ((_c = entity.metadata.tags) == null ? void 0 : _c.length) > maxTagChips && /* @__PURE__ */ React.createElement(Tooltip, { title: "Drill into the entity to see all of the tags." }, /* @__PURE__ */ React.createElement(Chip, { key: "other-tags", size: "small", label: "..." }))))), !error && /* @__PURE__ */ React.createElement(CardActions, null, entity && /* @__PURE__ */ React.createElement(React.Fragment, null, isUserEntity(entity) && /* @__PURE__ */ React.createElement(UserCardActions, { entity }), isGroupEntity(entity) && /* @__PURE__ */ React.createElement(GroupCardActions, { entity }), /* @__PURE__ */ React.createElement(EntityCardActions, { entity })))))
1260
+ ));
1261
+ };
1262
+
1130
1263
  const useStyles$a = makeStyles(
1131
1264
  (_theme) => ({
1132
1265
  searchToolbar: {
@@ -1337,6 +1470,7 @@ const EntityTable = (props) => {
1337
1470
  style: tableStyle,
1338
1471
  emptyContent: emptyContent && /* @__PURE__ */ React.createElement("div", { className: classes.empty }, emptyContent),
1339
1472
  options: {
1473
+ // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
1340
1474
  search: false,
1341
1475
  paging: false,
1342
1476
  actionsColumnIndex: -1,
@@ -2248,7 +2382,8 @@ const UserListPicker = (props) => {
2248
2382
  const filterGroups = getFilterGroups(orgName).map((filterGroup) => ({
2249
2383
  ...filterGroup,
2250
2384
  items: filterGroup.items.filter(
2251
- ({ id }) => ["group", "user"].some((kind) => kind === kindParameter) ? userAndGroupFilterIds.includes(id) : !availableFilters || availableFilters.includes(id)
2385
+ ({ id }) => // TODO: avoid hardcoding kinds here
2386
+ ["group", "user"].some((kind) => kind === kindParameter) ? userAndGroupFilterIds.includes(id) : !availableFilters || availableFilters.includes(id)
2252
2387
  )
2253
2388
  })).filter(({ items }) => !!items.length);
2254
2389
  const { isStarredEntity } = useStarredEntities();
@@ -2449,5 +2584,5 @@ const MockEntityListContextProvider = ({
2449
2584
  return /* @__PURE__ */ React.createElement(EntityListContext.Provider, { value: resolvedValue }, children);
2450
2585
  };
2451
2586
 
2452
- export { AsyncEntityProvider, CatalogFilterLayout, EntityErrorFilter, EntityKindFilter, EntityKindPicker, EntityLifecycleFilter, EntityLifecyclePicker, EntityListContext, EntityListProvider, EntityOrphanFilter, EntityOwnerFilter, EntityOwnerPicker, EntityProcessingStatusPicker, EntityProvider, EntityRefLink, EntityRefLinks, EntitySearchBar, EntityTable, EntityTagFilter, EntityTagPicker, EntityTextFilter, EntityTypeFilter, EntityTypePicker, FavoriteEntity, InspectEntityDialog, MockEntityListContextProvider, MockStarredEntitiesApi, UnregisterEntityDialog, UserListFilter, UserListPicker, catalogApiRef, columnFactories, entityRouteParams, entityRouteRef, getEntityRelations, getEntitySourceLocation, humanizeEntityRef, isOwnerOf, starredEntitiesApiRef, useAsyncEntity, useEntity, useEntityList, useEntityOwnership, useEntityPermission, useEntityTypeFilter, useRelatedEntities, useStarredEntities, useStarredEntity };
2587
+ export { AsyncEntityProvider, CatalogFilterLayout, EntityErrorFilter, EntityKindFilter, EntityKindPicker, EntityLifecycleFilter, EntityLifecyclePicker, EntityListContext, EntityListProvider, EntityOrphanFilter, EntityOwnerFilter, EntityOwnerPicker, EntityPeekAheadPopover, EntityProcessingStatusPicker, EntityProvider, EntityRefLink, EntityRefLinks, EntitySearchBar, EntityTable, EntityTagFilter, EntityTagPicker, EntityTextFilter, EntityTypeFilter, EntityTypePicker, FavoriteEntity, InspectEntityDialog, MockEntityListContextProvider, MockStarredEntitiesApi, UnregisterEntityDialog, UserListFilter, UserListPicker, catalogApiRef, columnFactories, entityRouteParams, entityRouteRef, getEntityRelations, getEntitySourceLocation, humanizeEntityRef, isOwnerOf, starredEntitiesApiRef, useAsyncEntity, useEntity, useEntityList, useEntityOwnership, useEntityPermission, useEntityTypeFilter, useRelatedEntities, useStarredEntities, useStarredEntity };
2453
2588
  //# sourceMappingURL=index.esm.js.map