@backstage/plugin-home 0.8.14-next.0 → 0.8.14-next.1

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,15 @@
1
1
  # @backstage/plugin-home
2
2
 
3
+ ## 0.8.14-next.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2ac5d29: Allow customization of VisitList by adding optional enrichVisit, transformPathname, canSave functions to VisitsStorageApi, along with VisitDisplayProvider for colors, labels
8
+ - Updated dependencies
9
+ - @backstage/plugin-catalog-react@1.21.3-next.2
10
+ - @backstage/frontend-plugin-api@0.12.2-next.2
11
+ - @backstage/core-components@0.18.3-next.2
12
+
3
13
  ## 0.8.14-next.0
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -132,7 +132,7 @@ export const RandomJokeHomePageComponent = homePlugin.provide(
132
132
  );
133
133
  ```
134
134
 
135
- These settings can also be defined for components that use `createReactExtension` instead `createCardExtension` by using
135
+ These settings can also be defined for components that use `createReactExtension` instead of `createCardExtension` by using
136
136
  the data property:
137
137
 
138
138
  ```tsx
@@ -356,13 +356,189 @@ home:
356
356
 
357
357
  In order to validate the config you can use `backstage/cli config:check`
358
358
 
359
+ ### Customizing the VisitList
360
+
361
+ If you want more control over the recent and top visited lists, you can write your own functions to transform the pathnames and determine which visits to save. You can also enrich each visit with other fields and customize the chip colors/labels in the visit lists.
362
+
363
+ #### Transform Pathname Function
364
+
365
+ Provide a `transformPathname` function to transform the pathname before it's processed for visit tracking. This can be used for transforming the pathname for the visit (before any other consideration). As an example, you can treat multiple sub-path visits to be counted as a singular path, e.g. `/entity-path/sub1` , `/entity-path/sub-2`, `/entity-path/sub-2/sub-sub-2` can all be mapped to `/entity-path` so visits to any of those routes are all counted as the same.
366
+
367
+ ```tsx
368
+ import {
369
+ AnyApiFactory,
370
+ createApiFactory,
371
+ identityApiRef,
372
+ storageApiRef,
373
+ } from '@backstage/core-plugin-api';
374
+ import { VisitsStorageApi } from '@backstage/plugin-home';
375
+
376
+ const transformPathname = (pathname: string) => {
377
+ const pathnameParts = pathname.split('/').filter(part => part !== '');
378
+ const rootPathFromPathname = pathnameParts[0] ?? '';
379
+ if (rootPathFromPathname === 'catalog' && pathnameParts.length >= 4) {
380
+ return `/${pathnameParts.slice(0, 4).join('/')}`;
381
+ }
382
+ return pathname;
383
+ };
384
+
385
+ export const apis: AnyApiFactory[] = [
386
+ createApiFactory({
387
+ api: visitsApiRef,
388
+ deps: {
389
+ storageApi: storageApiRef,
390
+ identityApi: identityApiRef,
391
+ },
392
+ factory: ({ storageApi, identityApi }) =>
393
+ VisitsStorageApi.create({
394
+ storageApi,
395
+ identityApi,
396
+ transformPathname,
397
+ }),
398
+ }),
399
+ ];
400
+ ```
401
+
402
+ #### Can Save Function
403
+
404
+ Provide a `canSave` function to determine which visits should be tracked and saved. This allows you to conditionally save visits to the list:
405
+
406
+ ```tsx
407
+ import {
408
+ AnyApiFactory,
409
+ createApiFactory,
410
+ identityApiRef,
411
+ storageApiRef,
412
+ } from '@backstage/core-plugin-api';
413
+ import { VisitInput, VisitsStorageApi } from '@backstage/plugin-home';
414
+
415
+ const canSave = (visit: VisitInput) => {
416
+ // Don't save visits to admin or settings pages
417
+ return (
418
+ !visit.pathname.startsWith('/admin') &&
419
+ !visit.pathname.startsWith('/settings')
420
+ );
421
+ };
422
+
423
+ export const apis: AnyApiFactory[] = [
424
+ createApiFactory({
425
+ api: visitsApiRef,
426
+ deps: {
427
+ storageApi: storageApiRef,
428
+ identityApi: identityApiRef,
429
+ },
430
+ factory: ({ storageApi, identityApi }) =>
431
+ VisitsStorageApi.create({
432
+ storageApi,
433
+ identityApi,
434
+ canSave,
435
+ }),
436
+ }),
437
+ ];
438
+ ```
439
+
440
+ #### Enrich Visit Function
441
+
442
+ You can also add the `enrichVisit` function to put additional values on each `Visit`. The values could later be used to customize the chips in the `VisitList`. For example, you could add the entity `type` on the `Visit` so that `type` is used for labels instead of `kind`.
443
+
444
+ ```tsx
445
+ import {
446
+ AnyApiFactory,
447
+ createApiFactory,
448
+ identityApiRef,
449
+ storageApiRef,
450
+ } from '@backstage/core-plugin-api';
451
+ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
452
+ import { VisitsStorageApi } from '@backstage/plugin-home';
453
+
454
+ type EnrichedVisit = VisitInput & {
455
+ type?: string;
456
+ };
457
+
458
+ const createEnrichVisit =
459
+ (catalogApi: CatalogApi) =>
460
+ async (visit: VisitInput): Promise<EnrichedVisit> => {
461
+ if (!visit.entityRef) {
462
+ return visit;
463
+ }
464
+ try {
465
+ const entity = await catalogApi.getEntityByRef(visit.entityRef);
466
+ const type = entity?.spec?.type?.toString();
467
+ return { ...visit, type };
468
+ } catch (error) {
469
+ return visit;
470
+ }
471
+ };
472
+
473
+ export const apis: AnyApiFactory[] = [
474
+ createApiFactory({
475
+ api: visitsApiRef,
476
+ deps: {
477
+ storageApi: storageApiRef,
478
+ identityApi: identityApiRef,
479
+ catalogApi: catalogApiRef,
480
+ },
481
+ factory: ({ storageApi, identityApi, catalogApi }) =>
482
+ VisitsStorageApi.create({
483
+ storageApi,
484
+ identityApi,
485
+ enrichVisit: createEnrichVisit(catalogApi),
486
+ }),
487
+ }),
488
+ ];
489
+ ```
490
+
491
+ #### Custom Chip Colors and Labels
492
+
493
+ To provide your own chip colors and/or labels for the recent and top visited lists, wrap the components in `VisitDisplayProvider` with `getChipColor` and `getChipLabel` functions. The colors provided will be used instead of the hard coded [colorVariants](https://github.com/backstage/backstage/blob/2da352043425bcab4c4422e4d2820c26c0a83382/packages/theme/src/base/pageTheme.ts#L46) provided via `@backstage/theme`.
494
+
495
+ ```tsx
496
+ import {
497
+ CustomHomepageGrid,
498
+ HomePageTopVisited,
499
+ HomePageRecentlyVisited,
500
+ VisitDisplayProvider,
501
+ } from '@backstage/plugin-home';
502
+
503
+ const getChipColor = (visit: any) => {
504
+ const type = visit.type;
505
+ switch (type) {
506
+ case 'application':
507
+ return '#b39ddb';
508
+ case 'service':
509
+ return '#90caf9';
510
+ case 'account':
511
+ return '#a5d6a7';
512
+ case 'suite':
513
+ return '#fff59d';
514
+ default:
515
+ return '#ef9a9a';
516
+ }
517
+ };
518
+
519
+ const getChipLabel = (visit?: any) => {
520
+ return visit?.type ? visit.type : 'Other';
521
+ };
522
+
523
+ export default function HomePage() {
524
+ return (
525
+ <VisitDisplayProvider getChipColor={getChipColor} getLabel={getChipLabel}>
526
+ <CustomHomepageGrid title="Your Dashboard">
527
+ <HomePageRecentlyVisited />
528
+ <HomePageTopVisited />
529
+ </CustomHomepageGrid>
530
+ </VisitDisplayProvider>
531
+ );
532
+ }
533
+ ```
534
+
359
535
  ## Contributing
360
536
 
361
537
  ### Homepage Components
362
538
 
363
- We believe that people have great ideas for what makes a useful Home Page, and we want to make it easy for every to benefit from the effort you put in to create something cool for the Home Page. Therefore, a great way of contributing is by simply creating more Home Page Components, than can then be used by everyone when composing their own Home Page. If they are tightly coupled to an existing plugin, it is recommended to allow them to live within that plugin, for convenience and to limit complex dependencies. On the other hand, if there's no clear plugin that the component is based on, it's also fine to contribute them into the [home plugin](/plugins/home/src/homePageComponents)
539
+ We believe that people have great ideas for what makes a useful Home Page, and we want to make it easy for everyone to benefit from the effort you put in to create something cool for the Home Page. Therefore, a great way of contributing is by simply creating more Home Page Components that can then be used by everyone when composing their own Home Page. If they are tightly coupled to an existing plugin, it is recommended to allow them to live within that plugin, for convenience and to limit complex dependencies. On the other hand, if there's no clear plugin that the component is based on, it's also fine to contribute them into the [home plugin](/plugins/home/src/homePageComponents)
364
540
 
365
- Additionally, the API is at a very early state, so contributing with additional use cases may expose weaknesses in the current solution that we may iterate on, to provide more flexibility and ease of use for those who wish to develop components for the Home Page.
541
+ Additionally, the API is at a very early state, so contributing additional use cases may expose weaknesses in the current solution that we may iterate on to provide more flexibility and ease of use for those who wish to develop components for the Home Page.
366
542
 
367
543
  ### Homepage Templates
368
544
 
package/dist/alpha.esm.js CHANGED
@@ -4,6 +4,7 @@ import { compatWrapper } from '@backstage/core-compat-api';
4
4
  import 'react-router-dom';
5
5
  import './components/CustomHomepage/CustomHomepageGrid.esm.js';
6
6
  import { VisitListener } from './components/VisitListener.esm.js';
7
+ import './components/VisitList/Context.esm.js';
7
8
  import { VisitsStorageApi } from './api/VisitsStorageApi.esm.js';
8
9
  import '@backstage/core-app-api';
9
10
  import { visitsApiRef } from './api/VisitsApi.esm.js';
@@ -1 +1 @@
1
- {"version":3,"file":"alpha.esm.js","sources":["../src/alpha.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 {\n coreExtensionData,\n createExtensionDataRef,\n createExtensionInput,\n PageBlueprint,\n createFrontendPlugin,\n createRouteRef,\n AppRootElementBlueprint,\n identityApiRef,\n storageApiRef,\n ApiBlueprint,\n} from '@backstage/frontend-plugin-api';\nimport { compatWrapper } from '@backstage/core-compat-api';\nimport { VisitListener } from './components/';\nimport { visitsApiRef, VisitsStorageApi } from './api';\n\nconst rootRouteRef = createRouteRef();\n\n/**\n * @alpha\n */\nexport const titleExtensionDataRef = createExtensionDataRef<string>().with({\n id: 'title',\n});\n\nconst homePage = PageBlueprint.makeWithOverrides({\n inputs: {\n props: createExtensionInput(\n [\n coreExtensionData.reactElement.optional(),\n titleExtensionDataRef.optional(),\n ],\n {\n singleton: true,\n optional: true,\n },\n ),\n },\n factory: (originalFactory, { inputs }) => {\n return originalFactory({\n path: '/home',\n routeRef: rootRouteRef,\n loader: () =>\n import('./components/').then(m =>\n compatWrapper(\n <m.HomepageCompositionRoot\n children={inputs.props?.get(coreExtensionData.reactElement)}\n title={inputs.props?.get(titleExtensionDataRef)}\n />,\n ),\n ),\n });\n },\n});\n\nconst visitListenerAppRootElement = AppRootElementBlueprint.make({\n name: 'visit-listener',\n params: {\n element: <VisitListener />,\n },\n});\n\nconst visitsApi = ApiBlueprint.make({\n name: 'visits',\n params: defineParams =>\n defineParams({\n api: visitsApiRef,\n deps: {\n storageApi: storageApiRef,\n identityApi: identityApiRef,\n },\n factory: ({ storageApi, identityApi }) =>\n VisitsStorageApi.create({ storageApi, identityApi }),\n }),\n});\n\n/**\n * @alpha\n */\nexport default createFrontendPlugin({\n pluginId: 'home',\n info: { packageJson: () => import('../package.json') },\n extensions: [homePage, visitsApi, visitListenerAppRootElement],\n routes: {\n root: rootRouteRef,\n },\n});\n\nexport { homeTranslationRef } from './translation';\n"],"names":[],"mappings":";;;;;;;;;;;AAgCA,MAAM,eAAe,cAAA,EAAe;AAK7B,MAAM,qBAAA,GAAwB,sBAAA,EAA+B,CAAE,IAAA,CAAK;AAAA,EACzE,EAAA,EAAI;AACN,CAAC;AAED,MAAM,QAAA,GAAW,cAAc,iBAAA,CAAkB;AAAA,EAC/C,MAAA,EAAQ;AAAA,IACN,KAAA,EAAO,oBAAA;AAAA,MACL;AAAA,QACE,iBAAA,CAAkB,aAAa,QAAA,EAAS;AAAA,QACxC,sBAAsB,QAAA;AAAS,OACjC;AAAA,MACA;AAAA,QACE,SAAA,EAAW,IAAA;AAAA,QACX,QAAA,EAAU;AAAA;AACZ;AACF,GACF;AAAA,EACA,OAAA,EAAS,CAAC,eAAA,EAAiB,EAAE,QAAO,KAAM;AACxC,IAAA,OAAO,eAAA,CAAgB;AAAA,MACrB,IAAA,EAAM,OAAA;AAAA,MACN,QAAA,EAAU,YAAA;AAAA,MACV,MAAA,EAAQ,MACN,OAAO,2BAAe,CAAA,CAAE,IAAA;AAAA,QAAK,CAAA,CAAA,KAC3B,aAAA;AAAA,0BACE,GAAA;AAAA,YAAC,CAAA,CAAE,uBAAA;AAAA,YAAF;AAAA,cACC,QAAA,EAAU,MAAA,CAAO,KAAA,EAAO,GAAA,CAAI,kBAAkB,YAAY,CAAA;AAAA,cAC1D,KAAA,EAAO,MAAA,CAAO,KAAA,EAAO,GAAA,CAAI,qBAAqB;AAAA;AAAA;AAChD;AACF;AACF,KACH,CAAA;AAAA,EACH;AACF,CAAC,CAAA;AAED,MAAM,2BAAA,GAA8B,wBAAwB,IAAA,CAAK;AAAA,EAC/D,IAAA,EAAM,gBAAA;AAAA,EACN,MAAA,EAAQ;AAAA,IACN,OAAA,sBAAU,aAAA,EAAA,EAAc;AAAA;AAE5B,CAAC,CAAA;AAED,MAAM,SAAA,GAAY,aAAa,IAAA,CAAK;AAAA,EAClC,IAAA,EAAM,QAAA;AAAA,EACN,MAAA,EAAQ,kBACN,YAAA,CAAa;AAAA,IACX,GAAA,EAAK,YAAA;AAAA,IACL,IAAA,EAAM;AAAA,MACJ,UAAA,EAAY,aAAA;AAAA,MACZ,WAAA,EAAa;AAAA,KACf;AAAA,IACA,OAAA,EAAS,CAAC,EAAE,UAAA,EAAY,WAAA,EAAY,KAClC,gBAAA,CAAiB,MAAA,CAAO,EAAE,UAAA,EAAY,WAAA,EAAa;AAAA,GACtD;AACL,CAAC,CAAA;AAKD,YAAe,oBAAA,CAAqB;AAAA,EAClC,QAAA,EAAU,MAAA;AAAA,EACV,MAAM,EAAE,WAAA,EAAa,MAAM,OAAO,uBAAiB,CAAA,EAAE;AAAA,EACrD,UAAA,EAAY,CAAC,QAAA,EAAU,SAAA,EAAW,2BAA2B,CAAA;AAAA,EAC7D,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA;AAEV,CAAC,CAAA;;;;"}
1
+ {"version":3,"file":"alpha.esm.js","sources":["../src/alpha.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 {\n coreExtensionData,\n createExtensionDataRef,\n createExtensionInput,\n PageBlueprint,\n createFrontendPlugin,\n createRouteRef,\n AppRootElementBlueprint,\n identityApiRef,\n storageApiRef,\n ApiBlueprint,\n} from '@backstage/frontend-plugin-api';\nimport { compatWrapper } from '@backstage/core-compat-api';\nimport { VisitListener } from './components/';\nimport { visitsApiRef, VisitsStorageApi } from './api';\n\nconst rootRouteRef = createRouteRef();\n\n/**\n * @alpha\n */\nexport const titleExtensionDataRef = createExtensionDataRef<string>().with({\n id: 'title',\n});\n\nconst homePage = PageBlueprint.makeWithOverrides({\n inputs: {\n props: createExtensionInput(\n [\n coreExtensionData.reactElement.optional(),\n titleExtensionDataRef.optional(),\n ],\n {\n singleton: true,\n optional: true,\n },\n ),\n },\n factory: (originalFactory, { inputs }) => {\n return originalFactory({\n path: '/home',\n routeRef: rootRouteRef,\n loader: () =>\n import('./components/').then(m =>\n compatWrapper(\n <m.HomepageCompositionRoot\n children={inputs.props?.get(coreExtensionData.reactElement)}\n title={inputs.props?.get(titleExtensionDataRef)}\n />,\n ),\n ),\n });\n },\n});\n\nconst visitListenerAppRootElement = AppRootElementBlueprint.make({\n name: 'visit-listener',\n params: {\n element: <VisitListener />,\n },\n});\n\nconst visitsApi = ApiBlueprint.make({\n name: 'visits',\n params: defineParams =>\n defineParams({\n api: visitsApiRef,\n deps: {\n storageApi: storageApiRef,\n identityApi: identityApiRef,\n },\n factory: ({ storageApi, identityApi }) =>\n VisitsStorageApi.create({ storageApi, identityApi }),\n }),\n});\n\n/**\n * @alpha\n */\nexport default createFrontendPlugin({\n pluginId: 'home',\n info: { packageJson: () => import('../package.json') },\n extensions: [homePage, visitsApi, visitListenerAppRootElement],\n routes: {\n root: rootRouteRef,\n },\n});\n\nexport { homeTranslationRef } from './translation';\n"],"names":[],"mappings":";;;;;;;;;;;;AAgCA,MAAM,eAAe,cAAA,EAAe;AAK7B,MAAM,qBAAA,GAAwB,sBAAA,EAA+B,CAAE,IAAA,CAAK;AAAA,EACzE,EAAA,EAAI;AACN,CAAC;AAED,MAAM,QAAA,GAAW,cAAc,iBAAA,CAAkB;AAAA,EAC/C,MAAA,EAAQ;AAAA,IACN,KAAA,EAAO,oBAAA;AAAA,MACL;AAAA,QACE,iBAAA,CAAkB,aAAa,QAAA,EAAS;AAAA,QACxC,sBAAsB,QAAA;AAAS,OACjC;AAAA,MACA;AAAA,QACE,SAAA,EAAW,IAAA;AAAA,QACX,QAAA,EAAU;AAAA;AACZ;AACF,GACF;AAAA,EACA,OAAA,EAAS,CAAC,eAAA,EAAiB,EAAE,QAAO,KAAM;AACxC,IAAA,OAAO,eAAA,CAAgB;AAAA,MACrB,IAAA,EAAM,OAAA;AAAA,MACN,QAAA,EAAU,YAAA;AAAA,MACV,MAAA,EAAQ,MACN,OAAO,2BAAe,CAAA,CAAE,IAAA;AAAA,QAAK,CAAA,CAAA,KAC3B,aAAA;AAAA,0BACE,GAAA;AAAA,YAAC,CAAA,CAAE,uBAAA;AAAA,YAAF;AAAA,cACC,QAAA,EAAU,MAAA,CAAO,KAAA,EAAO,GAAA,CAAI,kBAAkB,YAAY,CAAA;AAAA,cAC1D,KAAA,EAAO,MAAA,CAAO,KAAA,EAAO,GAAA,CAAI,qBAAqB;AAAA;AAAA;AAChD;AACF;AACF,KACH,CAAA;AAAA,EACH;AACF,CAAC,CAAA;AAED,MAAM,2BAAA,GAA8B,wBAAwB,IAAA,CAAK;AAAA,EAC/D,IAAA,EAAM,gBAAA;AAAA,EACN,MAAA,EAAQ;AAAA,IACN,OAAA,sBAAU,aAAA,EAAA,EAAc;AAAA;AAE5B,CAAC,CAAA;AAED,MAAM,SAAA,GAAY,aAAa,IAAA,CAAK;AAAA,EAClC,IAAA,EAAM,QAAA;AAAA,EACN,MAAA,EAAQ,kBACN,YAAA,CAAa;AAAA,IACX,GAAA,EAAK,YAAA;AAAA,IACL,IAAA,EAAM;AAAA,MACJ,UAAA,EAAY,aAAA;AAAA,MACZ,WAAA,EAAa;AAAA,KACf;AAAA,IACA,OAAA,EAAS,CAAC,EAAE,UAAA,EAAY,WAAA,EAAY,KAClC,gBAAA,CAAiB,MAAA,CAAO,EAAE,UAAA,EAAY,WAAA,EAAa;AAAA,GACtD;AACL,CAAC,CAAA;AAKD,YAAe,oBAAA,CAAqB;AAAA,EAClC,QAAA,EAAU,MAAA;AAAA,EACV,MAAM,EAAE,WAAA,EAAa,MAAM,OAAO,uBAAiB,CAAA,EAAE;AAAA,EACrD,UAAA,EAAY,CAAC,QAAA,EAAU,SAAA,EAAW,2BAA2B,CAAA;AAAA,EAC7D,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA;AAEV,CAAC,CAAA;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"VisitsApi.esm.js","sources":["../../src/api/VisitsApi.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { createApiRef } from '@backstage/core-plugin-api';\n\n/**\n * @public\n * The operators that can be used in filter.\n */\nexport type Operators = '<' | '<=' | '==' | '!=' | '>' | '>=' | 'contains';\n\n/**\n * @public\n * Type guard for operators.\n */\nexport const isOperator = (s: string): s is Operators => {\n return ['<', '<=', '==', '!=', '>', '>=', 'contains'].includes(s);\n};\n\n/**\n * @public\n * Model for a visit entity.\n */\nexport type Visit = {\n /**\n * The auto-generated visit identification.\n */\n id: string;\n /**\n * The visited entity, usually an entity id.\n */\n name: string;\n /**\n * The visited url pathname, usually the entity route.\n */\n pathname: string;\n /**\n * An individual view count.\n */\n hits: number;\n /**\n * Last date and time of visit. Format: unix epoch in ms.\n */\n timestamp: number;\n /**\n * Optional entity reference. See stringifyEntityRef from catalog-model.\n */\n entityRef?: string;\n};\n\n/**\n * @public\n * This data structure represents the parameters associated with search queries for visits.\n */\nexport type VisitsApiQueryParams = {\n /**\n * Limits the number of results returned. The default is 8.\n */\n limit?: number;\n /**\n * Allows ordering visits on entity properties.\n * @example\n * Sort ascending by the timestamp field.\n * ```\n * { orderBy: [{ field: 'timestamp', direction: 'asc' }] }\n * ```\n */\n orderBy?: Array<{\n field: keyof Visit;\n direction: 'asc' | 'desc';\n }>;\n /**\n * Allows filtering visits on entity properties.\n * @example\n * Most popular docs on the past 7 days\n * ```\n * {\n * orderBy: [{ field: 'hits', direction: 'desc' }],\n * filterBy: [\n * { field: 'timestamp', operator: '>=', value: <date> },\n * { field: 'entityRef', operator: 'contains', value: 'docs' }\n * ]\n * }\n * ```\n */\n filterBy?: Array<{\n field: keyof Visit;\n operator: Operators;\n value: string | number;\n }>;\n};\n\n/**\n * @public\n * This data structure represents the parameters associated with saving visits.\n */\nexport type VisitsApiSaveParams = {\n visit: Omit<Visit, 'id' | 'hits' | 'timestamp'>;\n};\n\n/**\n * @public\n * Visits API public contract.\n */\nexport interface VisitsApi {\n /**\n * Persist a new visit.\n * @param pageVisit - a new visit data\n */\n save(saveParams: VisitsApiSaveParams): Promise<Visit>;\n /**\n * Get user visits.\n * @param queryParams - optional search query params.\n */\n list(queryParams?: VisitsApiQueryParams): Promise<Visit[]>;\n}\n\n/** @public */\nexport const visitsApiRef = createApiRef<VisitsApi>({\n id: 'homepage.visits',\n});\n"],"names":[],"mappings":";;AA4BO,MAAM,UAAA,GAAa,CAAC,CAAA,KAA8B;AACvD,EAAA,OAAO,CAAC,GAAA,EAAK,IAAA,EAAM,IAAA,EAAM,IAAA,EAAM,KAAK,IAAA,EAAM,UAAU,CAAA,CAAE,QAAA,CAAS,CAAC,CAAA;AAClE;AAqGO,MAAM,eAAe,YAAA,CAAwB;AAAA,EAClD,EAAA,EAAI;AACN,CAAC;;;;"}
1
+ {"version":3,"file":"VisitsApi.esm.js","sources":["../../src/api/VisitsApi.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { createApiRef } from '@backstage/core-plugin-api';\nimport { VisitInput } from './VisitsStorageApi';\n\n/**\n * @public\n * The operators that can be used in filter.\n */\nexport type Operators = '<' | '<=' | '==' | '!=' | '>' | '>=' | 'contains';\n\n/**\n * @public\n * Type guard for operators.\n */\nexport const isOperator = (s: string): s is Operators => {\n return ['<', '<=', '==', '!=', '>', '>=', 'contains'].includes(s);\n};\n\n/**\n * @public\n * Model for a visit entity.\n */\nexport type Visit = {\n /**\n * The auto-generated visit identification.\n */\n id: string;\n /**\n * The visited entity, usually an entity id.\n */\n name: string;\n /**\n * The visited url pathname, usually the entity route.\n */\n pathname: string;\n /**\n * An individual view count.\n */\n hits: number;\n /**\n * Last date and time of visit. Format: unix epoch in ms.\n */\n timestamp: number;\n /**\n * Optional entity reference. See stringifyEntityRef from catalog-model.\n */\n entityRef?: string;\n};\n\n/**\n * @public\n * This data structure represents the parameters associated with search queries for visits.\n */\nexport type VisitsApiQueryParams = {\n /**\n * Limits the number of results returned. The default is 8.\n */\n limit?: number;\n /**\n * Allows ordering visits on entity properties.\n * @example\n * Sort ascending by the timestamp field.\n * ```\n * { orderBy: [{ field: 'timestamp', direction: 'asc' }] }\n * ```\n */\n orderBy?: Array<{\n field: keyof Visit;\n direction: 'asc' | 'desc';\n }>;\n /**\n * Allows filtering visits on entity properties.\n * @example\n * Most popular docs on the past 7 days\n * ```\n * {\n * orderBy: [{ field: 'hits', direction: 'desc' }],\n * filterBy: [\n * { field: 'timestamp', operator: '>=', value: <date> },\n * { field: 'entityRef', operator: 'contains', value: 'docs' }\n * ]\n * }\n * ```\n */\n filterBy?: Array<{\n field: keyof Visit;\n operator: Operators;\n value: string | number;\n }>;\n};\n\n/**\n * @public\n * This data structure represents the parameters associated with saving visits.\n */\nexport type VisitsApiSaveParams = {\n visit: Omit<Visit, 'id' | 'hits' | 'timestamp'>;\n};\n\n/**\n * @public\n * Visits API public contract.\n */\nexport interface VisitsApi {\n /**\n * Persist a new visit.\n * @param pageVisit - a new visit data\n */\n save(saveParams: VisitsApiSaveParams): Promise<Visit>;\n /**\n * Get user visits.\n * @param queryParams - optional search query params.\n */\n list(queryParams?: VisitsApiQueryParams): Promise<Visit[]>;\n /**\n * Transform the pathname before it is considered for any other processing.\n * @param pathname - the original pathname\n */\n transformPathname?(pathname: string): string;\n /**\n * Determine whether a visit should be saved.\n * @param visit - page visit data\n */\n canSave?(visit: VisitInput): boolean | Promise<boolean>;\n /**\n * Add additional data to the visit before saving.\n * @param visit - page visit data\n */\n enrichVisit?(\n visit: VisitInput,\n ): Promise<Record<string, any>> | Record<string, any>;\n}\n\n/** @public */\nexport const visitsApiRef = createApiRef<VisitsApi>({\n id: 'homepage.visits',\n});\n"],"names":[],"mappings":";;AA6BO,MAAM,UAAA,GAAa,CAAC,CAAA,KAA8B;AACvD,EAAA,OAAO,CAAC,GAAA,EAAK,IAAA,EAAM,IAAA,EAAM,IAAA,EAAM,KAAK,IAAA,EAAM,UAAU,CAAA,CAAE,QAAA,CAAS,CAAC,CAAA;AAClE;AAsHO,MAAM,eAAe,YAAA,CAAwB;AAAA,EAClD,EAAA,EAAI;AACN,CAAC;;;;"}
@@ -4,6 +4,9 @@ class VisitsStorageApi {
4
4
  storageApi;
5
5
  storageKeyPrefix = "@backstage/plugin-home:visits";
6
6
  identityApi;
7
+ transformPathnameImpl;
8
+ canSaveImpl;
9
+ enrichVisitImpl;
7
10
  static create(options) {
8
11
  return new VisitsStorageApi(options);
9
12
  }
@@ -11,6 +14,9 @@ class VisitsStorageApi {
11
14
  this.limit = Math.abs(options.limit ?? 100);
12
15
  this.storageApi = options.storageApi;
13
16
  this.identityApi = options.identityApi;
17
+ this.transformPathnameImpl = options.transformPathname;
18
+ this.canSaveImpl = options.canSave;
19
+ this.enrichVisitImpl = options.enrichVisit;
14
20
  }
15
21
  /**
16
22
  * Returns a list of visits through the visitsApi
@@ -40,28 +46,73 @@ class VisitsStorageApi {
40
46
  });
41
47
  return visits.slice(0, queryParams?.limit ?? DEFAULT_LIST_LIMIT);
42
48
  }
49
+ /**
50
+ * Transform the pathname before it is considered for any other processing.
51
+ * @param pathname - the original pathname
52
+ * @returns the transformed pathname
53
+ */
54
+ transformPathname(pathname) {
55
+ return this.transformPathnameImpl?.(pathname) ?? pathname;
56
+ }
57
+ /**
58
+ * Determine whether a visit should be saved.
59
+ * @param visit - page visit data
60
+ */
61
+ async canSave(visit) {
62
+ if (!this.canSaveImpl) {
63
+ return true;
64
+ }
65
+ return Promise.resolve(this.canSaveImpl(visit));
66
+ }
67
+ /**
68
+ * Add additional data to the visit before saving.
69
+ * @param visit - page visit data
70
+ */
71
+ async enrichVisit(visit) {
72
+ if (!this.enrichVisitImpl) {
73
+ return {};
74
+ }
75
+ return Promise.resolve(this.enrichVisitImpl(visit));
76
+ }
43
77
  /**
44
78
  * Saves a visit through the visitsApi
45
79
  */
46
80
  async save(saveParams) {
81
+ let visit = saveParams.visit;
82
+ visit = {
83
+ ...visit,
84
+ pathname: this.transformPathname(visit.pathname)
85
+ };
86
+ if (!await this.canSave(visit)) {
87
+ return {
88
+ ...visit,
89
+ id: "",
90
+ hits: 0,
91
+ timestamp: Date.now()
92
+ };
93
+ }
94
+ const enrichedData = await this.enrichVisit(visit);
95
+ const enrichedVisit = { ...visit, ...enrichedData };
47
96
  const visits = [...await this.retrieveAll()];
48
- const visit = {
49
- ...saveParams.visit,
97
+ const visitToSave = {
98
+ ...enrichedVisit,
50
99
  id: window.crypto.randomUUID(),
51
100
  hits: 1,
52
101
  timestamp: Date.now()
53
102
  };
54
- const visitIndex = visits.findIndex((e) => e.pathname === visit.pathname);
103
+ const visitIndex = visits.findIndex(
104
+ (e) => e.pathname === visitToSave.pathname
105
+ );
55
106
  if (visitIndex >= 0) {
56
- visit.id = visits[visitIndex].id;
57
- visit.hits = visits[visitIndex].hits + 1;
58
- visits[visitIndex] = visit;
107
+ visitToSave.id = visits[visitIndex].id;
108
+ visitToSave.hits = visits[visitIndex].hits + 1;
109
+ visits[visitIndex] = visitToSave;
59
110
  } else {
60
- visits.push(visit);
111
+ visits.push(visitToSave);
61
112
  }
62
113
  visits.sort((a, b) => b.timestamp - a.timestamp);
63
114
  await this.persistAll(visits.splice(0, this.limit));
64
- return visit;
115
+ return visitToSave;
65
116
  }
66
117
  async persistAll(visits) {
67
118
  const storageKey = await this.getStorageKey();
@@ -1 +1 @@
1
- {"version":3,"file":"VisitsStorageApi.esm.js","sources":["../../src/api/VisitsStorageApi.ts"],"sourcesContent":["/*\n * Copyright 2023 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 */\nimport { IdentityApi, StorageApi } from '@backstage/core-plugin-api';\nimport {\n Visit,\n VisitsApi,\n VisitsApiQueryParams,\n VisitsApiSaveParams,\n} from './VisitsApi';\n\n/** @public */\nexport type VisitsStorageApiOptions = {\n limit?: number;\n storageApi: StorageApi;\n identityApi: IdentityApi;\n};\n\ntype ArrayElement<A> = A extends readonly (infer T)[] ? T : never;\n\nconst DEFAULT_LIST_LIMIT = 8;\n\n/**\n * @public\n * This is an implementation of VisitsApi that relies on a StorageApi.\n * Beware that filtering and ordering are done in memory therefore it is\n * prudent to keep limit to a reasonable size.\n */\nexport class VisitsStorageApi implements VisitsApi {\n private readonly limit: number;\n private readonly storageApi: StorageApi;\n private readonly storageKeyPrefix = '@backstage/plugin-home:visits';\n private readonly identityApi: IdentityApi;\n\n static create(options: VisitsStorageApiOptions) {\n return new VisitsStorageApi(options);\n }\n\n private constructor(options: VisitsStorageApiOptions) {\n this.limit = Math.abs(options.limit ?? 100);\n this.storageApi = options.storageApi;\n this.identityApi = options.identityApi;\n }\n\n /**\n * Returns a list of visits through the visitsApi\n */\n async list(queryParams?: VisitsApiQueryParams): Promise<Visit[]> {\n let visits = [...(await this.retrieveAll())];\n\n // reversing order to guarantee orderBy priority\n (queryParams?.orderBy ?? []).reverse().forEach(order => {\n if (order.direction === 'asc') {\n visits.sort((a, b) => this.compare(order, a, b));\n } else {\n visits.sort((a, b) => this.compare(order, b, a));\n }\n });\n\n // reversing order to guarantee filterBy priority\n (queryParams?.filterBy ?? []).reverse().forEach(filter => {\n visits = visits.filter(visit => {\n const field = visit[filter.field] as number | string;\n if (filter.operator === '>') return field > filter.value;\n if (filter.operator === '>=') return field >= filter.value;\n if (filter.operator === '<') return field < filter.value;\n if (filter.operator === '<=') return field <= filter.value;\n if (filter.operator === '==') return field === filter.value;\n if (filter.operator === '!=') return field !== filter.value;\n if (filter.operator === 'contains')\n return `${field}`.includes(`${filter.value}`);\n return false;\n });\n });\n\n return visits.slice(0, queryParams?.limit ?? DEFAULT_LIST_LIMIT);\n }\n\n /**\n * Saves a visit through the visitsApi\n */\n async save(saveParams: VisitsApiSaveParams): Promise<Visit> {\n const visits: Visit[] = [...(await this.retrieveAll())];\n\n const visit: Visit = {\n ...saveParams.visit,\n id: window.crypto.randomUUID(),\n hits: 1,\n timestamp: Date.now(),\n };\n\n // Updates entry if pathname is already registered\n const visitIndex = visits.findIndex(e => e.pathname === visit.pathname);\n if (visitIndex >= 0) {\n visit.id = visits[visitIndex].id;\n visit.hits = visits[visitIndex].hits + 1;\n visits[visitIndex] = visit;\n } else {\n visits.push(visit);\n }\n\n // Sort by time, most recent first\n visits.sort((a, b) => b.timestamp - a.timestamp);\n // Keep the most recent items up to limit\n await this.persistAll(visits.splice(0, this.limit));\n return visit;\n }\n\n private async persistAll(visits: Array<Visit>) {\n const storageKey = await this.getStorageKey();\n return this.storageApi.set<Array<Visit>>(storageKey, visits);\n }\n\n private async retrieveAll(): Promise<Array<Visit>> {\n const storageKey = await this.getStorageKey();\n // Handles for case when snapshot is and is not referenced per storage type used\n const snapshot = this.storageApi.snapshot<Array<Visit>>(storageKey);\n if (snapshot?.presence !== 'unknown') {\n return snapshot?.value ?? [];\n }\n\n return new Promise((resolve, reject) => {\n const subscription = this.storageApi\n .observe$<Visit[]>(storageKey)\n .subscribe({\n next: next => {\n const visits = next.value ?? [];\n subscription.unsubscribe();\n resolve(visits);\n },\n error: err => {\n subscription.unsubscribe();\n reject(err);\n },\n });\n });\n }\n\n private async getStorageKey(): Promise<string> {\n const { userEntityRef } = await this.identityApi.getBackstageIdentity();\n const storageKey = `${this.storageKeyPrefix}:${userEntityRef}`;\n return storageKey;\n }\n\n // This assumes Visit fields are either numbers or strings\n private compare(\n order: ArrayElement<VisitsApiQueryParams['orderBy']>,\n a: Visit,\n b: Visit,\n ): number {\n const isNumber = typeof a[order.field] === 'number';\n return isNumber\n ? (a[order.field] as number) - (b[order.field] as number)\n : `${a[order.field]}`.localeCompare(`${b[order.field]}`);\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"VisitsStorageApi.esm.js","sources":["../../src/api/VisitsStorageApi.ts"],"sourcesContent":["/*\n * Copyright 2023 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 */\nimport { IdentityApi, StorageApi } from '@backstage/core-plugin-api';\nimport {\n Visit,\n VisitsApi,\n VisitsApiQueryParams,\n VisitsApiSaveParams,\n} from './VisitsApi';\n\n/**\n * @public\n * Type definition for visit data before it's saved (without auto-generated fields)\n */\nexport type VisitInput = {\n name: string;\n pathname: string;\n entityRef?: string;\n};\n\n/** @public */\nexport type VisitsStorageApiOptions = {\n limit?: number;\n storageApi: StorageApi;\n identityApi: IdentityApi;\n transformPathname?: (pathname: string) => string;\n canSave?: (visit: VisitInput) => boolean | Promise<boolean>;\n enrichVisit?: (\n visit: VisitInput,\n ) => Promise<Record<string, any>> | Record<string, any>;\n};\n\ntype ArrayElement<A> = A extends readonly (infer T)[] ? T : never;\n\nconst DEFAULT_LIST_LIMIT = 8;\n\n/**\n * @public\n * This is an implementation of VisitsApi that relies on a StorageApi.\n * Beware that filtering and ordering are done in memory therefore it is\n * prudent to keep limit to a reasonable size.\n */\nexport class VisitsStorageApi implements VisitsApi {\n private readonly limit: number;\n private readonly storageApi: StorageApi;\n private readonly storageKeyPrefix = '@backstage/plugin-home:visits';\n private readonly identityApi: IdentityApi;\n private readonly transformPathnameImpl?: (pathname: string) => string;\n private readonly canSaveImpl?: (\n visit: VisitInput,\n ) => boolean | Promise<boolean>;\n private readonly enrichVisitImpl?: (\n visit: VisitInput,\n ) => Promise<Record<string, any>> | Record<string, any>;\n\n static create(options: VisitsStorageApiOptions) {\n return new VisitsStorageApi(options);\n }\n\n private constructor(options: VisitsStorageApiOptions) {\n this.limit = Math.abs(options.limit ?? 100);\n this.storageApi = options.storageApi;\n this.identityApi = options.identityApi;\n this.transformPathnameImpl = options.transformPathname;\n this.canSaveImpl = options.canSave;\n this.enrichVisitImpl = options.enrichVisit;\n }\n\n /**\n * Returns a list of visits through the visitsApi\n */\n async list(queryParams?: VisitsApiQueryParams): Promise<Visit[]> {\n let visits = [...(await this.retrieveAll())];\n\n // reversing order to guarantee orderBy priority\n (queryParams?.orderBy ?? []).reverse().forEach(order => {\n if (order.direction === 'asc') {\n visits.sort((a, b) => this.compare(order, a, b));\n } else {\n visits.sort((a, b) => this.compare(order, b, a));\n }\n });\n\n // reversing order to guarantee filterBy priority\n (queryParams?.filterBy ?? []).reverse().forEach(filter => {\n visits = visits.filter(visit => {\n const field = visit[filter.field] as number | string;\n if (filter.operator === '>') return field > filter.value;\n if (filter.operator === '>=') return field >= filter.value;\n if (filter.operator === '<') return field < filter.value;\n if (filter.operator === '<=') return field <= filter.value;\n if (filter.operator === '==') return field === filter.value;\n if (filter.operator === '!=') return field !== filter.value;\n if (filter.operator === 'contains')\n return `${field}`.includes(`${filter.value}`);\n return false;\n });\n });\n\n return visits.slice(0, queryParams?.limit ?? DEFAULT_LIST_LIMIT);\n }\n\n /**\n * Transform the pathname before it is considered for any other processing.\n * @param pathname - the original pathname\n * @returns the transformed pathname\n */\n transformPathname(pathname: string): string {\n return this.transformPathnameImpl?.(pathname) ?? pathname;\n }\n\n /**\n * Determine whether a visit should be saved.\n * @param visit - page visit data\n */\n async canSave(visit: VisitInput): Promise<boolean> {\n if (!this.canSaveImpl) {\n return true;\n }\n return Promise.resolve(this.canSaveImpl(visit));\n }\n\n /**\n * Add additional data to the visit before saving.\n * @param visit - page visit data\n */\n async enrichVisit(visit: VisitInput): Promise<Record<string, any>> {\n if (!this.enrichVisitImpl) {\n return {};\n }\n return Promise.resolve(this.enrichVisitImpl(visit));\n }\n\n /**\n * Saves a visit through the visitsApi\n */\n async save(saveParams: VisitsApiSaveParams): Promise<Visit> {\n let visit = saveParams.visit;\n\n // Transform pathname if needed\n visit = {\n ...visit,\n pathname: this.transformPathname(visit.pathname),\n };\n\n // Check if visit should be saved\n if (!(await this.canSave(visit))) {\n // Return a minimal visit object without saving\n return {\n ...visit,\n id: '',\n hits: 0,\n timestamp: Date.now(),\n };\n }\n\n // Enrich the visit\n const enrichedData = await this.enrichVisit(visit);\n const enrichedVisit = { ...visit, ...enrichedData };\n\n const visits: Visit[] = [...(await this.retrieveAll())];\n\n const visitToSave: Visit = {\n ...enrichedVisit,\n id: window.crypto.randomUUID(),\n hits: 1,\n timestamp: Date.now(),\n };\n\n // Updates entry if pathname is already registered\n const visitIndex = visits.findIndex(\n e => e.pathname === visitToSave.pathname,\n );\n if (visitIndex >= 0) {\n visitToSave.id = visits[visitIndex].id;\n visitToSave.hits = visits[visitIndex].hits + 1;\n visits[visitIndex] = visitToSave;\n } else {\n visits.push(visitToSave);\n }\n\n // Sort by time, most recent first\n visits.sort((a, b) => b.timestamp - a.timestamp);\n // Keep the most recent items up to limit\n await this.persistAll(visits.splice(0, this.limit));\n return visitToSave;\n }\n\n private async persistAll(visits: Array<Visit>) {\n const storageKey = await this.getStorageKey();\n return this.storageApi.set<Array<Visit>>(storageKey, visits);\n }\n\n private async retrieveAll(): Promise<Array<Visit>> {\n const storageKey = await this.getStorageKey();\n // Handles for case when snapshot is and is not referenced per storage type used\n const snapshot = this.storageApi.snapshot<Array<Visit>>(storageKey);\n if (snapshot?.presence !== 'unknown') {\n return snapshot?.value ?? [];\n }\n\n return new Promise((resolve, reject) => {\n const subscription = this.storageApi\n .observe$<Visit[]>(storageKey)\n .subscribe({\n next: next => {\n const visits = next.value ?? [];\n subscription.unsubscribe();\n resolve(visits);\n },\n error: err => {\n subscription.unsubscribe();\n reject(err);\n },\n });\n });\n }\n\n private async getStorageKey(): Promise<string> {\n const { userEntityRef } = await this.identityApi.getBackstageIdentity();\n const storageKey = `${this.storageKeyPrefix}:${userEntityRef}`;\n return storageKey;\n }\n\n // This assumes Visit fields are either numbers or strings\n private compare(\n order: ArrayElement<VisitsApiQueryParams['orderBy']>,\n a: Visit,\n b: Visit,\n ): number {\n const isNumber = typeof a[order.field] === 'number';\n return isNumber\n ? (a[order.field] as number) - (b[order.field] as number)\n : `${a[order.field]}`.localeCompare(`${b[order.field]}`);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useContext, createContext } from 'react';
3
+ import { parseEntityRef } from '@backstage/catalog-model';
4
+ import { colorVariants } from '@backstage/theme';
5
+
6
+ const getColorByIndex = (index) => {
7
+ const variants = Object.keys(colorVariants);
8
+ const variantIndex = index % variants.length;
9
+ return colorVariants[variants[variantIndex]][0];
10
+ };
11
+ const maybeEntity = (visit) => {
12
+ try {
13
+ return parseEntityRef(visit?.entityRef ?? "");
14
+ } catch (e) {
15
+ return void 0;
16
+ }
17
+ };
18
+ const defaultGetChipColor = (visit) => {
19
+ const defaultColor = getColorByIndex(0);
20
+ const entity = maybeEntity(visit);
21
+ if (!entity) return defaultColor;
22
+ const entityKinds = [
23
+ "component",
24
+ "template",
25
+ "api",
26
+ "group",
27
+ "user",
28
+ "resource",
29
+ "system",
30
+ "domain",
31
+ "location"
32
+ ];
33
+ const foundIndex = entityKinds.indexOf(
34
+ entity.kind.toLocaleLowerCase("en-US")
35
+ );
36
+ return foundIndex === -1 ? defaultColor : getColorByIndex(foundIndex + 1);
37
+ };
38
+ const defaultGetLabel = (visit) => {
39
+ const entity = maybeEntity(visit);
40
+ return (entity?.kind ?? "Other").toLocaleLowerCase("en-US");
41
+ };
42
+ const VisitDisplayContext = createContext({
43
+ getChipColor: defaultGetChipColor,
44
+ getLabel: defaultGetLabel
45
+ });
46
+ const VisitDisplayProvider = ({
47
+ children,
48
+ getChipColor = defaultGetChipColor,
49
+ getLabel = defaultGetLabel
50
+ }) => {
51
+ const value = {
52
+ getChipColor,
53
+ getLabel
54
+ };
55
+ return /* @__PURE__ */ jsx(VisitDisplayContext.Provider, { value, children });
56
+ };
57
+ const useVisitDisplay = () => {
58
+ const context = useContext(VisitDisplayContext);
59
+ if (!context) {
60
+ throw new Error(
61
+ "useVisitDisplay must be used within a VisitDisplayProvider"
62
+ );
63
+ }
64
+ return context;
65
+ };
66
+
67
+ export { VisitDisplayProvider, useVisitDisplay };
68
+ //# sourceMappingURL=Context.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Context.esm.js","sources":["../../../src/components/VisitList/Context.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 { createContext, useContext, ReactNode } from 'react';\nimport { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model';\nimport { colorVariants } from '@backstage/theme';\nimport { Visit } from '../../api/VisitsApi';\n\n/**\n * Type definition for the chip color function\n * @public\n */\nexport type GetChipColorFunction = (visit: Visit) => string;\n\n/**\n * Type definition for the label function\n * @public\n */\nexport type GetLabelFunction = (visit: Visit) => string;\n\n/**\n * Context value interface\n * @public\n */\nexport interface VisitDisplayContextValue {\n getChipColor: GetChipColorFunction;\n getLabel: GetLabelFunction;\n}\n\n/**\n * Props for the VisitDisplayProvider\n * @public\n */\nexport interface VisitDisplayProviderProps {\n children: ReactNode;\n getChipColor?: GetChipColorFunction;\n getLabel?: GetLabelFunction;\n}\n\n// Default implementations\nconst getColorByIndex = (index: number) => {\n const variants = Object.keys(colorVariants);\n const variantIndex = index % variants.length;\n return colorVariants[variants[variantIndex]][0];\n};\n\nconst maybeEntity = (visit: Visit): CompoundEntityRef | undefined => {\n try {\n return parseEntityRef(visit?.entityRef ?? '');\n } catch (e) {\n return undefined;\n }\n};\n\nconst defaultGetChipColor: GetChipColorFunction = (visit: Visit): string => {\n const defaultColor = getColorByIndex(0);\n const entity = maybeEntity(visit);\n if (!entity) return defaultColor;\n\n // IDEA: Use or replicate useAllKinds hook thus supporting all software catalog\n // registered kinds. See:\n // plugins/catalog-react/src/components/EntityKindPicker/kindFilterUtils.ts\n // Provide extension point to register your own color code.\n const entityKinds = [\n 'component',\n 'template',\n 'api',\n 'group',\n 'user',\n 'resource',\n 'system',\n 'domain',\n 'location',\n ];\n const foundIndex = entityKinds.indexOf(\n entity.kind.toLocaleLowerCase('en-US'),\n );\n return foundIndex === -1 ? defaultColor : getColorByIndex(foundIndex + 1);\n};\n\nconst defaultGetLabel: GetLabelFunction = (visit: Visit): string => {\n const entity = maybeEntity(visit);\n return (entity?.kind ?? 'Other').toLocaleLowerCase('en-US');\n};\n\n// Create the context\nconst VisitDisplayContext = createContext<VisitDisplayContextValue>({\n getChipColor: defaultGetChipColor,\n getLabel: defaultGetLabel,\n});\n\n/**\n * Provider component for VisitDisplay customization\n * @public\n */\nexport const VisitDisplayProvider = ({\n children,\n getChipColor = defaultGetChipColor,\n getLabel = defaultGetLabel,\n}: VisitDisplayProviderProps) => {\n const value: VisitDisplayContextValue = {\n getChipColor,\n getLabel,\n };\n\n return (\n <VisitDisplayContext.Provider value={value}>\n {children}\n </VisitDisplayContext.Provider>\n );\n};\n\n/**\n * Hook to use the VisitDisplay context\n * @public\n */\nexport const useVisitDisplay = (): VisitDisplayContextValue => {\n const context = useContext(VisitDisplayContext);\n if (!context) {\n throw new Error(\n 'useVisitDisplay must be used within a VisitDisplayProvider',\n );\n }\n return context;\n};\n"],"names":[],"mappings":";;;;;AAqDA,MAAM,eAAA,GAAkB,CAAC,KAAA,KAAkB;AACzC,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,IAAA,CAAK,aAAa,CAAA;AAC1C,EAAA,MAAM,YAAA,GAAe,QAAQ,QAAA,CAAS,MAAA;AACtC,EAAA,OAAO,aAAA,CAAc,QAAA,CAAS,YAAY,CAAC,EAAE,CAAC,CAAA;AAChD,CAAA;AAEA,MAAM,WAAA,GAAc,CAAC,KAAA,KAAgD;AACnE,EAAA,IAAI;AACF,IAAA,OAAO,cAAA,CAAe,KAAA,EAAO,SAAA,IAAa,EAAE,CAAA;AAAA,EAC9C,SAAS,CAAA,EAAG;AACV,IAAA,OAAO,MAAA;AAAA,EACT;AACF,CAAA;AAEA,MAAM,mBAAA,GAA4C,CAAC,KAAA,KAAyB;AAC1E,EAAA,MAAM,YAAA,GAAe,gBAAgB,CAAC,CAAA;AACtC,EAAA,MAAM,MAAA,GAAS,YAAY,KAAK,CAAA;AAChC,EAAA,IAAI,CAAC,QAAQ,OAAO,YAAA;AAMpB,EAAA,MAAM,WAAA,GAAc;AAAA,IAClB,WAAA;AAAA,IACA,UAAA;AAAA,IACA,KAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GACF;AACA,EAAA,MAAM,aAAa,WAAA,CAAY,OAAA;AAAA,IAC7B,MAAA,CAAO,IAAA,CAAK,iBAAA,CAAkB,OAAO;AAAA,GACvC;AACA,EAAA,OAAO,UAAA,KAAe,EAAA,GAAK,YAAA,GAAe,eAAA,CAAgB,aAAa,CAAC,CAAA;AAC1E,CAAA;AAEA,MAAM,eAAA,GAAoC,CAAC,KAAA,KAAyB;AAClE,EAAA,MAAM,MAAA,GAAS,YAAY,KAAK,CAAA;AAChC,EAAA,OAAA,CAAQ,MAAA,EAAQ,IAAA,IAAQ,OAAA,EAAS,iBAAA,CAAkB,OAAO,CAAA;AAC5D,CAAA;AAGA,MAAM,sBAAsB,aAAA,CAAwC;AAAA,EAClE,YAAA,EAAc,mBAAA;AAAA,EACd,QAAA,EAAU;AACZ,CAAC,CAAA;AAMM,MAAM,uBAAuB,CAAC;AAAA,EACnC,QAAA;AAAA,EACA,YAAA,GAAe,mBAAA;AAAA,EACf,QAAA,GAAW;AACb,CAAA,KAAiC;AAC/B,EAAA,MAAM,KAAA,GAAkC;AAAA,IACtC,YAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,uBACE,GAAA,CAAC,mBAAA,CAAoB,QAAA,EAApB,EAA6B,OAC3B,QAAA,EACH,CAAA;AAEJ;AAMO,MAAM,kBAAkB,MAAgC;AAC7D,EAAA,MAAM,OAAA,GAAU,WAAW,mBAAmB,CAAA;AAC9C,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,OAAA;AACT;;;;"}
@@ -1,8 +1,7 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import Chip from '@material-ui/core/Chip';
3
3
  import { makeStyles } from '@material-ui/core/styles';
4
- import { colorVariants } from '@backstage/theme';
5
- import { parseEntityRef } from '@backstage/catalog-model';
4
+ import { useVisitDisplay } from './Context.esm.js';
6
5
 
7
6
  const useStyles = makeStyles((theme) => ({
8
7
  chip: {
@@ -11,47 +10,16 @@ const useStyles = makeStyles((theme) => ({
11
10
  margin: 0
12
11
  }
13
12
  }));
14
- const maybeEntity = (visit) => {
15
- try {
16
- return parseEntityRef(visit?.entityRef ?? "");
17
- } catch (e) {
18
- return void 0;
19
- }
20
- };
21
- const getColorByIndex = (index) => {
22
- const variants = Object.keys(colorVariants);
23
- const variantIndex = index % variants.length;
24
- return colorVariants[variants[variantIndex]][0];
25
- };
26
- const getChipColor = (entity) => {
27
- const defaultColor = getColorByIndex(0);
28
- if (!entity) return defaultColor;
29
- const entityKinds = [
30
- "component",
31
- "template",
32
- "api",
33
- "group",
34
- "user",
35
- "resource",
36
- "system",
37
- "domain",
38
- "location"
39
- ];
40
- const foundIndex = entityKinds.indexOf(
41
- entity.kind.toLocaleLowerCase("en-US")
42
- );
43
- return foundIndex === -1 ? defaultColor : getColorByIndex(foundIndex + 1);
44
- };
45
13
  const ItemCategory = ({ visit }) => {
46
14
  const classes = useStyles();
47
- const entity = maybeEntity(visit);
15
+ const { getChipColor, getLabel } = useVisitDisplay();
48
16
  return /* @__PURE__ */ jsx(
49
17
  Chip,
50
18
  {
51
19
  size: "small",
52
20
  className: classes.chip,
53
- label: (entity?.kind ?? "Other").toLocaleLowerCase("en-US"),
54
- style: { background: getChipColor(entity) }
21
+ label: getLabel(visit),
22
+ style: { background: getChipColor(visit) }
55
23
  }
56
24
  );
57
25
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ItemCategory.esm.js","sources":["../../../src/components/VisitList/ItemCategory.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 Chip from '@material-ui/core/Chip';\nimport { makeStyles } from '@material-ui/core/styles';\nimport { colorVariants } from '@backstage/theme';\nimport { Visit } from '../../api/VisitsApi';\nimport { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model';\n\nconst useStyles = makeStyles(theme => ({\n chip: {\n color: theme.palette.common.white,\n fontWeight: 'bold',\n margin: 0,\n },\n}));\nconst maybeEntity = (visit: Visit): CompoundEntityRef | undefined => {\n try {\n return parseEntityRef(visit?.entityRef ?? '');\n } catch (e) {\n return undefined;\n }\n};\nconst getColorByIndex = (index: number) => {\n const variants = Object.keys(colorVariants);\n const variantIndex = index % variants.length;\n return colorVariants[variants[variantIndex]][0];\n};\nconst getChipColor = (entity: CompoundEntityRef | undefined): string => {\n const defaultColor = getColorByIndex(0);\n if (!entity) return defaultColor;\n\n // IDEA: Use or replicate useAllKinds hook thus supporting all software catalog\n // registered kinds. See:\n // plugins/catalog-react/src/components/EntityKindPicker/kindFilterUtils.ts\n // Provide extension point to register your own color code.\n const entityKinds = [\n 'component',\n 'template',\n 'api',\n 'group',\n 'user',\n 'resource',\n 'system',\n 'domain',\n 'location',\n ];\n const foundIndex = entityKinds.indexOf(\n entity.kind.toLocaleLowerCase('en-US'),\n );\n return foundIndex === -1 ? defaultColor : getColorByIndex(foundIndex + 1);\n};\n\nexport const ItemCategory = ({ visit }: { visit: Visit }) => {\n const classes = useStyles();\n const entity = maybeEntity(visit);\n\n return (\n <Chip\n size=\"small\"\n className={classes.chip}\n label={(entity?.kind ?? 'Other').toLocaleLowerCase('en-US')}\n style={{ background: getChipColor(entity) }}\n />\n );\n};\n"],"names":[],"mappings":";;;;;;AAsBA,MAAM,SAAA,GAAY,WAAW,CAAA,KAAA,MAAU;AAAA,EACrC,IAAA,EAAM;AAAA,IACJ,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,IAC5B,UAAA,EAAY,MAAA;AAAA,IACZ,MAAA,EAAQ;AAAA;AAEZ,CAAA,CAAE,CAAA;AACF,MAAM,WAAA,GAAc,CAAC,KAAA,KAAgD;AACnE,EAAA,IAAI;AACF,IAAA,OAAO,cAAA,CAAe,KAAA,EAAO,SAAA,IAAa,EAAE,CAAA;AAAA,EAC9C,SAAS,CAAA,EAAG;AACV,IAAA,OAAO,MAAA;AAAA,EACT;AACF,CAAA;AACA,MAAM,eAAA,GAAkB,CAAC,KAAA,KAAkB;AACzC,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,IAAA,CAAK,aAAa,CAAA;AAC1C,EAAA,MAAM,YAAA,GAAe,QAAQ,QAAA,CAAS,MAAA;AACtC,EAAA,OAAO,aAAA,CAAc,QAAA,CAAS,YAAY,CAAC,EAAE,CAAC,CAAA;AAChD,CAAA;AACA,MAAM,YAAA,GAAe,CAAC,MAAA,KAAkD;AACtE,EAAA,MAAM,YAAA,GAAe,gBAAgB,CAAC,CAAA;AACtC,EAAA,IAAI,CAAC,QAAQ,OAAO,YAAA;AAMpB,EAAA,MAAM,WAAA,GAAc;AAAA,IAClB,WAAA;AAAA,IACA,UAAA;AAAA,IACA,KAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GACF;AACA,EAAA,MAAM,aAAa,WAAA,CAAY,OAAA;AAAA,IAC7B,MAAA,CAAO,IAAA,CAAK,iBAAA,CAAkB,OAAO;AAAA,GACvC;AACA,EAAA,OAAO,UAAA,KAAe,EAAA,GAAK,YAAA,GAAe,eAAA,CAAgB,aAAa,CAAC,CAAA;AAC1E,CAAA;AAEO,MAAM,YAAA,GAAe,CAAC,EAAE,KAAA,EAAM,KAAwB;AAC3D,EAAA,MAAM,UAAU,SAAA,EAAU;AAC1B,EAAA,MAAM,MAAA,GAAS,YAAY,KAAK,CAAA;AAEhC,EAAA,uBACE,GAAA;AAAA,IAAC,IAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,OAAA;AAAA,MACL,WAAW,OAAA,CAAQ,IAAA;AAAA,MACnB,KAAA,EAAA,CAAQ,MAAA,EAAQ,IAAA,IAAQ,OAAA,EAAS,kBAAkB,OAAO,CAAA;AAAA,MAC1D,KAAA,EAAO,EAAE,UAAA,EAAY,YAAA,CAAa,MAAM,CAAA;AAAE;AAAA,GAC5C;AAEJ;;;;"}
1
+ {"version":3,"file":"ItemCategory.esm.js","sources":["../../../src/components/VisitList/ItemCategory.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 Chip from '@material-ui/core/Chip';\nimport { makeStyles } from '@material-ui/core/styles';\nimport { Visit } from '../../api/VisitsApi';\nimport { useVisitDisplay } from './Context';\n\nconst useStyles = makeStyles(theme => ({\n chip: {\n color: theme.palette.common.white,\n fontWeight: 'bold',\n margin: 0,\n },\n}));\n\nexport const ItemCategory = ({ visit }: { visit: Visit }) => {\n const classes = useStyles();\n const { getChipColor, getLabel } = useVisitDisplay();\n\n return (\n <Chip\n size=\"small\"\n className={classes.chip}\n label={getLabel(visit)}\n style={{ background: getChipColor(visit) }}\n />\n );\n};\n"],"names":[],"mappings":";;;;;AAqBA,MAAM,SAAA,GAAY,WAAW,CAAA,KAAA,MAAU;AAAA,EACrC,IAAA,EAAM;AAAA,IACJ,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,IAC5B,UAAA,EAAY,MAAA;AAAA,IACZ,MAAA,EAAQ;AAAA;AAEZ,CAAA,CAAE,CAAA;AAEK,MAAM,YAAA,GAAe,CAAC,EAAE,KAAA,EAAM,KAAwB;AAC3D,EAAA,MAAM,UAAU,SAAA,EAAU;AAC1B,EAAA,MAAM,EAAE,YAAA,EAAc,QAAA,EAAS,GAAI,eAAA,EAAgB;AAEnD,EAAA,uBACE,GAAA;AAAA,IAAC,IAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,OAAA;AAAA,MACL,WAAW,OAAA,CAAQ,IAAA;AAAA,MACnB,KAAA,EAAO,SAAS,KAAK,CAAA;AAAA,MACrB,KAAA,EAAO,EAAE,UAAA,EAAY,YAAA,CAAa,KAAK,CAAA;AAAE;AAAA,GAC3C;AAEJ;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"ItemDetail.esm.js","sources":["../../../src/components/VisitList/ItemDetail.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 Typography from '@material-ui/core/Typography';\nimport { Visit } from '../../api/VisitsApi';\nimport { DateTime } from 'luxon';\n\nconst ItemDetailHits = ({ visit }: { visit: Visit }) => (\n <Typography component=\"span\" variant=\"caption\" color=\"textSecondary\">\n {visit.hits} time{visit.hits > 1 ? 's' : ''}\n </Typography>\n);\n\nconst ItemDetailTimeAgo = ({ visit }: { visit: Visit }) => {\n const visitDate = DateTime.fromMillis(visit.timestamp);\n\n return (\n <Typography\n component=\"time\"\n variant=\"caption\"\n color=\"textSecondary\"\n dateTime={visitDate.toISO() ?? undefined}\n >\n {visitDate.toRelative()}\n </Typography>\n );\n};\n\nexport type ItemDetailType = 'time-ago' | 'hits';\n\nexport const ItemDetail = ({\n visit,\n type,\n}: {\n visit: Visit;\n type: ItemDetailType;\n}) =>\n type === 'time-ago' ? (\n <ItemDetailTimeAgo visit={visit} />\n ) : (\n <ItemDetailHits visit={visit} />\n );\n"],"names":[],"mappings":";;;;AAoBA,MAAM,cAAA,GAAiB,CAAC,EAAE,KAAA,EAAM,qBAC9B,IAAA,CAAC,UAAA,EAAA,EAAW,SAAA,EAAU,MAAA,EAAO,OAAA,EAAQ,SAAA,EAAU,KAAA,EAAM,eAAA,EAClD,QAAA,EAAA;AAAA,EAAA,KAAA,CAAM,IAAA;AAAA,EAAK,OAAA;AAAA,EAAM,KAAA,CAAM,IAAA,GAAO,CAAA,GAAI,GAAA,GAAM;AAAA,CAAA,EAC3C,CAAA;AAGF,MAAM,iBAAA,GAAoB,CAAC,EAAE,KAAA,EAAM,KAAwB;AACzD,EAAA,MAAM,SAAA,GAAY,QAAA,CAAS,UAAA,CAAW,KAAA,CAAM,SAAS,CAAA;AAErD,EAAA,uBACE,GAAA;AAAA,IAAC,UAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAU,MAAA;AAAA,MACV,OAAA,EAAQ,SAAA;AAAA,MACR,KAAA,EAAM,eAAA;AAAA,MACN,QAAA,EAAU,SAAA,CAAU,KAAA,EAAM,IAAK,MAAA;AAAA,MAE9B,oBAAU,UAAA;AAAW;AAAA,GACxB;AAEJ,CAAA;AAIO,MAAM,aAAa,CAAC;AAAA,EACzB,KAAA;AAAA,EACA;AACF,CAAA,KAIE,IAAA,KAAS,6BACP,GAAA,CAAC,iBAAA,EAAA,EAAkB,OAAc,CAAA,mBAEjC,GAAA,CAAC,kBAAe,KAAA,EAAc;;;;"}
1
+ {"version":3,"file":"ItemDetail.esm.js","sources":["../../../src/components/VisitList/ItemDetail.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 Typography from '@material-ui/core/Typography';\nimport { Visit } from '../../api/VisitsApi';\nimport { DateTime } from 'luxon';\n\nconst ItemDetailHits = ({ visit }: { visit: Visit }) => (\n <Typography component=\"span\" variant=\"caption\" color=\"textSecondary\">\n {visit.hits} time{visit.hits > 1 ? 's' : ''}\n </Typography>\n);\n\nconst ItemDetailTimeAgo = ({ visit }: { visit: Visit }) => {\n const visitDate = DateTime.fromMillis(visit.timestamp);\n\n return (\n <Typography\n component=\"time\"\n variant=\"caption\"\n color=\"textSecondary\"\n dateTime={visitDate.toISO() ?? undefined}\n >\n {visitDate.toRelative()}\n </Typography>\n );\n};\n\n/**\n * @internal\n */\nexport type ItemDetailType = 'time-ago' | 'hits';\n\nexport const ItemDetail = ({\n visit,\n type,\n}: {\n visit: Visit;\n type: ItemDetailType;\n}) =>\n type === 'time-ago' ? (\n <ItemDetailTimeAgo visit={visit} />\n ) : (\n <ItemDetailHits visit={visit} />\n );\n"],"names":[],"mappings":";;;;AAoBA,MAAM,cAAA,GAAiB,CAAC,EAAE,KAAA,EAAM,qBAC9B,IAAA,CAAC,UAAA,EAAA,EAAW,SAAA,EAAU,MAAA,EAAO,OAAA,EAAQ,SAAA,EAAU,KAAA,EAAM,eAAA,EAClD,QAAA,EAAA;AAAA,EAAA,KAAA,CAAM,IAAA;AAAA,EAAK,OAAA;AAAA,EAAM,KAAA,CAAM,IAAA,GAAO,CAAA,GAAI,GAAA,GAAM;AAAA,CAAA,EAC3C,CAAA;AAGF,MAAM,iBAAA,GAAoB,CAAC,EAAE,KAAA,EAAM,KAAwB;AACzD,EAAA,MAAM,SAAA,GAAY,QAAA,CAAS,UAAA,CAAW,KAAA,CAAM,SAAS,CAAA;AAErD,EAAA,uBACE,GAAA;AAAA,IAAC,UAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAU,MAAA;AAAA,MACV,OAAA,EAAQ,SAAA;AAAA,MACR,KAAA,EAAM,eAAA;AAAA,MACN,QAAA,EAAU,SAAA,CAAU,KAAA,EAAM,IAAK,MAAA;AAAA,MAE9B,oBAAU,UAAA;AAAW;AAAA,GACxB;AAEJ,CAAA;AAOO,MAAM,aAAa,CAAC;AAAA,EACzB,KAAA;AAAA,EACA;AACF,CAAA,KAIE,IAAA,KAAS,6BACP,GAAA,CAAC,iBAAA,EAAA,EAAkB,OAAc,CAAA,mBAEjC,GAAA,CAAC,kBAAe,KAAA,EAAc;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"VisitList.esm.js","sources":["../../../src/components/VisitList/VisitList.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 { ReactElement } from 'react';\nimport Collapse from '@material-ui/core/Collapse';\nimport List from '@material-ui/core/List';\nimport { Visit } from '../../api/VisitsApi';\nimport { VisitListItem } from './VisitListItem';\nimport { ItemDetailType } from './ItemDetail';\nimport { VisitListEmpty } from './VisitListEmpty';\nimport { VisitListFew } from './VisitListFew';\nimport { VisitListSkeleton } from './VisitListSkeleton';\n\nexport const VisitList = ({\n detailType,\n visits = [],\n numVisitsOpen = 3,\n numVisitsTotal = 8,\n collapsed = true,\n loading = false,\n title = '',\n}: {\n detailType: ItemDetailType;\n visits?: Visit[];\n numVisitsOpen?: number;\n numVisitsTotal?: number;\n collapsed?: boolean;\n loading?: boolean;\n title?: string;\n}) => {\n let listBody: ReactElement = <></>;\n if (loading) {\n listBody = (\n <VisitListSkeleton\n numVisitsOpen={numVisitsOpen}\n numVisitsTotal={numVisitsTotal}\n collapsed={collapsed}\n />\n );\n } else if (visits.length === 0) {\n listBody = <VisitListEmpty />;\n } else if (visits.length < numVisitsOpen) {\n listBody = (\n <>\n {visits.map((visit, index) => (\n <VisitListItem visit={visit} key={index} detailType={detailType} />\n ))}\n <VisitListFew />\n </>\n );\n } else {\n listBody = (\n <>\n {visits.slice(0, numVisitsOpen).map((visit, index) => (\n <VisitListItem visit={visit} key={index} detailType={detailType} />\n ))}\n {visits.length > numVisitsOpen && (\n <Collapse in={!collapsed}>\n {visits.slice(numVisitsOpen, numVisitsTotal).map((visit, index) => (\n <VisitListItem\n visit={visit}\n key={index}\n detailType={detailType}\n />\n ))}\n </Collapse>\n )}\n </>\n );\n }\n\n return (\n <>\n {title && <h5>{title}</h5>}\n <List dense disablePadding>\n {listBody}\n </List>\n </>\n );\n};\n"],"names":[],"mappings":";;;;;;;;AA0BO,MAAM,YAAY,CAAC;AAAA,EACxB,UAAA;AAAA,EACA,SAAS,EAAC;AAAA,EACV,aAAA,GAAgB,CAAA;AAAA,EAChB,cAAA,GAAiB,CAAA;AAAA,EACjB,SAAA,GAAY,IAAA;AAAA,EACZ,OAAA,GAAU,KAAA;AAAA,EACV,KAAA,GAAQ;AACV,CAAA,KAQM;AACJ,EAAA,IAAI,2BAAyB,GAAA,CAAA,QAAA,EAAA,EAAE,CAAA;AAC/B,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,QAAA,mBACE,GAAA;AAAA,MAAC,iBAAA;AAAA,MAAA;AAAA,QACC,aAAA;AAAA,QACA,cAAA;AAAA,QACA;AAAA;AAAA,KACF;AAAA,EAEJ,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAC9B,IAAA,QAAA,uBAAY,cAAA,EAAA,EAAe,CAAA;AAAA,EAC7B,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,GAAS,aAAA,EAAe;AACxC,IAAA,QAAA,mBACE,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,MAAA,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,EAAO,KAAA,yBACjB,aAAA,EAAA,EAAc,KAAA,EAA0B,UAAA,EAAA,EAAP,KAA+B,CAClE,CAAA;AAAA,0BACA,YAAA,EAAA,EAAa;AAAA,KAAA,EAChB,CAAA;AAAA,EAEJ,CAAA,MAAO;AACL,IAAA,QAAA,mBACE,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,aAAa,CAAA,CAAE,GAAA,CAAI,CAAC,KAAA,EAAO,KAAA,qBAC1C,GAAA,CAAC,aAAA,EAAA,EAAc,KAAA,EAA0B,UAAA,EAAA,EAAP,KAA+B,CAClE,CAAA;AAAA,MACA,OAAO,MAAA,GAAS,aAAA,oBACf,GAAA,CAAC,QAAA,EAAA,EAAS,IAAI,CAAC,SAAA,EACZ,QAAA,EAAA,MAAA,CAAO,KAAA,CAAM,eAAe,cAAc,CAAA,CAAE,GAAA,CAAI,CAAC,OAAO,KAAA,qBACvD,GAAA;AAAA,QAAC,aAAA;AAAA,QAAA;AAAA,UACC,KAAA;AAAA,UAEA;AAAA,SAAA;AAAA,QADK;AAAA,OAGR,CAAA,EACH;AAAA,KAAA,EAEJ,CAAA;AAAA,EAEJ;AAEA,EAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,IAAA,KAAA,oBAAS,GAAA,CAAC,QAAI,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,wBACpB,IAAA,EAAA,EAAK,KAAA,EAAK,IAAA,EAAC,cAAA,EAAc,MACvB,QAAA,EAAA,QAAA,EACH;AAAA,GAAA,EACF,CAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"VisitList.esm.js","sources":["../../../src/components/VisitList/VisitList.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 { ReactElement } from 'react';\nimport Collapse from '@material-ui/core/Collapse';\nimport List from '@material-ui/core/List';\nimport { Visit } from '../../api/VisitsApi';\nimport { VisitListItem } from './VisitListItem';\nimport { ItemDetailType } from './ItemDetail';\nimport { VisitListEmpty } from './VisitListEmpty';\nimport { VisitListFew } from './VisitListFew';\nimport { VisitListSkeleton } from './VisitListSkeleton';\n\n/**\n * @internal\n */\nexport const VisitList = ({\n detailType,\n visits = [],\n numVisitsOpen = 3,\n numVisitsTotal = 8,\n collapsed = true,\n loading = false,\n title = '',\n}: {\n detailType: ItemDetailType;\n visits?: Visit[];\n numVisitsOpen?: number;\n numVisitsTotal?: number;\n collapsed?: boolean;\n loading?: boolean;\n title?: string;\n}) => {\n let listBody: ReactElement = <></>;\n if (loading) {\n listBody = (\n <VisitListSkeleton\n numVisitsOpen={numVisitsOpen}\n numVisitsTotal={numVisitsTotal}\n collapsed={collapsed}\n />\n );\n } else if (visits.length === 0) {\n listBody = <VisitListEmpty />;\n } else if (visits.length < numVisitsOpen) {\n listBody = (\n <>\n {visits.map((visit, index) => (\n <VisitListItem visit={visit} key={index} detailType={detailType} />\n ))}\n <VisitListFew />\n </>\n );\n } else {\n listBody = (\n <>\n {visits.slice(0, numVisitsOpen).map((visit, index) => (\n <VisitListItem visit={visit} key={index} detailType={detailType} />\n ))}\n {visits.length > numVisitsOpen && (\n <Collapse in={!collapsed}>\n {visits.slice(numVisitsOpen, numVisitsTotal).map((visit, index) => (\n <VisitListItem\n visit={visit}\n key={index}\n detailType={detailType}\n />\n ))}\n </Collapse>\n )}\n </>\n );\n }\n\n return (\n <>\n {title && <h5>{title}</h5>}\n <List dense disablePadding>\n {listBody}\n </List>\n </>\n );\n};\n"],"names":[],"mappings":";;;;;;;;AA6BO,MAAM,YAAY,CAAC;AAAA,EACxB,UAAA;AAAA,EACA,SAAS,EAAC;AAAA,EACV,aAAA,GAAgB,CAAA;AAAA,EAChB,cAAA,GAAiB,CAAA;AAAA,EACjB,SAAA,GAAY,IAAA;AAAA,EACZ,OAAA,GAAU,KAAA;AAAA,EACV,KAAA,GAAQ;AACV,CAAA,KAQM;AACJ,EAAA,IAAI,2BAAyB,GAAA,CAAA,QAAA,EAAA,EAAE,CAAA;AAC/B,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,QAAA,mBACE,GAAA;AAAA,MAAC,iBAAA;AAAA,MAAA;AAAA,QACC,aAAA;AAAA,QACA,cAAA;AAAA,QACA;AAAA;AAAA,KACF;AAAA,EAEJ,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAC9B,IAAA,QAAA,uBAAY,cAAA,EAAA,EAAe,CAAA;AAAA,EAC7B,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,GAAS,aAAA,EAAe;AACxC,IAAA,QAAA,mBACE,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,MAAA,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,EAAO,KAAA,yBACjB,aAAA,EAAA,EAAc,KAAA,EAA0B,UAAA,EAAA,EAAP,KAA+B,CAClE,CAAA;AAAA,0BACA,YAAA,EAAA,EAAa;AAAA,KAAA,EAChB,CAAA;AAAA,EAEJ,CAAA,MAAO;AACL,IAAA,QAAA,mBACE,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,aAAa,CAAA,CAAE,GAAA,CAAI,CAAC,KAAA,EAAO,KAAA,qBAC1C,GAAA,CAAC,aAAA,EAAA,EAAc,KAAA,EAA0B,UAAA,EAAA,EAAP,KAA+B,CAClE,CAAA;AAAA,MACA,OAAO,MAAA,GAAS,aAAA,oBACf,GAAA,CAAC,QAAA,EAAA,EAAS,IAAI,CAAC,SAAA,EACZ,QAAA,EAAA,MAAA,CAAO,KAAA,CAAM,eAAe,cAAc,CAAA,CAAE,GAAA,CAAI,CAAC,OAAO,KAAA,qBACvD,GAAA;AAAA,QAAC,aAAA;AAAA,QAAA;AAAA,UACC,KAAA;AAAA,UAEA;AAAA,SAAA;AAAA,QADK;AAAA,OAGR,CAAA,EACH;AAAA,KAAA,EAEJ,CAAA;AAAA,EAEJ;AAEA,EAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,IAAA,KAAA,oBAAS,GAAA,CAAC,QAAI,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,wBACpB,IAAA,EAAA,EAAK,KAAA,EAAK,IAAA,EAAC,cAAA,EAAc,MACvB,QAAA,EAAA,QAAA,EACH;AAAA,GAAA,EACF,CAAA;AAEJ;;;;"}
@@ -1,4 +1,5 @@
1
1
  export { HomepageCompositionRoot } from './HomepageCompositionRoot.esm.js';
2
2
  export { CustomHomepageGrid } from './CustomHomepage/CustomHomepageGrid.esm.js';
3
3
  export { VisitListener } from './VisitListener.esm.js';
4
+ export { VisitDisplayProvider, useVisitDisplay } from './VisitList/Context.esm.js';
4
5
  //# sourceMappingURL=index.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
@@ -32,9 +32,9 @@ const getFilteredSet = (setContext, contextKey) => (e) => setContext((state) =>
32
32
  [contextKey]: typeof e === "function" ? e(state[contextKey]) : e
33
33
  }));
34
34
  const ContextProvider = ({ children }) => {
35
- const [context, setContext] = useState(
36
- defaultContextValueOnly
37
- );
35
+ const [context, setContext] = useState({
36
+ ...defaultContextValueOnly
37
+ });
38
38
  const {
39
39
  setCollapsed,
40
40
  setNumVisitsOpen,
@@ -1 +1 @@
1
- {"version":3,"file":"Context.esm.js","sources":["../../../src/homePageComponents/VisitedByType/Context.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 {\n Dispatch,\n SetStateAction,\n createContext,\n useMemo,\n useState,\n useContext as useReactContext,\n} from 'react';\nimport { Visit } from '../../api/VisitsApi';\nimport { VisitedByTypeKind } from './Content';\n\nexport type ContextValueOnly = {\n collapsed: boolean;\n numVisitsOpen: number;\n numVisitsTotal: number;\n visits: Array<Visit>;\n loading: boolean;\n kind: VisitedByTypeKind;\n};\n\nexport type ContextValue = ContextValueOnly & {\n setCollapsed: Dispatch<SetStateAction<boolean>>;\n setNumVisitsOpen: Dispatch<SetStateAction<number>>;\n setNumVisitsTotal: Dispatch<SetStateAction<number>>;\n setVisits: Dispatch<SetStateAction<Array<Visit>>>;\n setLoading: Dispatch<SetStateAction<boolean>>;\n setKind: Dispatch<SetStateAction<VisitedByTypeKind>>;\n setContext: Dispatch<SetStateAction<ContextValueOnly>>;\n};\n\nconst defaultContextValueOnly: ContextValueOnly = {\n collapsed: true,\n numVisitsOpen: 3,\n numVisitsTotal: 8,\n visits: [],\n loading: true,\n kind: 'recent',\n};\n\nexport const defaultContextValue: ContextValue = {\n ...defaultContextValueOnly,\n setCollapsed: () => {},\n setNumVisitsOpen: () => {},\n setNumVisitsTotal: () => {},\n setVisits: () => {},\n setLoading: () => {},\n setKind: () => {},\n setContext: () => {},\n};\n\nexport const Context = createContext<ContextValue>(defaultContextValue);\n\nconst getFilteredSet =\n <T,>(\n setContext: Dispatch<SetStateAction<ContextValueOnly>>,\n contextKey: keyof ContextValueOnly,\n ) =>\n (e: SetStateAction<T>) =>\n setContext(state => ({\n ...state,\n [contextKey]:\n typeof e === 'function' ? (e as Function)(state[contextKey]) : e,\n }));\n\nexport const ContextProvider = ({ children }: { children: JSX.Element }) => {\n const [context, setContext] = useState<ContextValueOnly>(\n defaultContextValueOnly,\n );\n const {\n setCollapsed,\n setNumVisitsOpen,\n setNumVisitsTotal,\n setVisits,\n setLoading,\n setKind,\n } = useMemo(\n () => ({\n setCollapsed: getFilteredSet(setContext, 'collapsed'),\n setNumVisitsOpen: getFilteredSet(setContext, 'numVisitsOpen'),\n setNumVisitsTotal: getFilteredSet(setContext, 'numVisitsTotal'),\n setVisits: getFilteredSet(setContext, 'visits'),\n setLoading: getFilteredSet(setContext, 'loading'),\n setKind: getFilteredSet(setContext, 'kind'),\n }),\n [setContext],\n );\n\n const value: ContextValue = {\n ...context,\n setContext,\n setCollapsed,\n setNumVisitsOpen,\n setNumVisitsTotal,\n setVisits,\n setLoading,\n setKind,\n };\n\n return <Context.Provider value={value}>{children}</Context.Provider>;\n};\n\nexport const useContext = () => {\n const value = useReactContext(Context);\n\n if (value === undefined)\n throw new Error(\n 'VisitedByType useContext found undefined ContextValue, <ContextProvider/> could be missing',\n );\n\n return value;\n};\n\nexport default Context;\n"],"names":["useReactContext"],"mappings":";;;AA8CA,MAAM,uBAAA,GAA4C;AAAA,EAChD,SAAA,EAAW,IAAA;AAAA,EACX,aAAA,EAAe,CAAA;AAAA,EACf,cAAA,EAAgB,CAAA;AAAA,EAChB,QAAQ,EAAC;AAAA,EACT,OAAA,EAAS,IAAA;AAAA,EACT,IAAA,EAAM;AACR,CAAA;AAEO,MAAM,mBAAA,GAAoC;AAAA,EAC/C,GAAG,uBAAA;AAAA,EACH,cAAc,MAAM;AAAA,EAAC,CAAA;AAAA,EACrB,kBAAkB,MAAM;AAAA,EAAC,CAAA;AAAA,EACzB,mBAAmB,MAAM;AAAA,EAAC,CAAA;AAAA,EAC1B,WAAW,MAAM;AAAA,EAAC,CAAA;AAAA,EAClB,YAAY,MAAM;AAAA,EAAC,CAAA;AAAA,EACnB,SAAS,MAAM;AAAA,EAAC,CAAA;AAAA,EAChB,YAAY,MAAM;AAAA,EAAC;AACrB;AAEO,MAAM,OAAA,GAAU,cAA4B,mBAAmB;AAEtE,MAAM,iBACJ,CACE,UAAA,EACA,eAEF,CAAC,CAAA,KACC,WAAW,CAAA,KAAA,MAAU;AAAA,EACnB,GAAG,KAAA;AAAA,EACH,CAAC,UAAU,GACT,OAAO,CAAA,KAAM,aAAc,CAAA,CAAe,KAAA,CAAM,UAAU,CAAC,CAAA,GAAI;AACnE,CAAA,CAAE,CAAA;AAEC,MAAM,eAAA,GAAkB,CAAC,EAAE,QAAA,EAAS,KAAiC;AAC1E,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,QAAA;AAAA,IAC5B;AAAA,GACF;AACA,EAAA,MAAM;AAAA,IACJ,YAAA;AAAA,IACA,gBAAA;AAAA,IACA,iBAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF,GAAI,OAAA;AAAA,IACF,OAAO;AAAA,MACL,YAAA,EAAc,cAAA,CAAe,UAAA,EAAY,WAAW,CAAA;AAAA,MACpD,gBAAA,EAAkB,cAAA,CAAe,UAAA,EAAY,eAAe,CAAA;AAAA,MAC5D,iBAAA,EAAmB,cAAA,CAAe,UAAA,EAAY,gBAAgB,CAAA;AAAA,MAC9D,SAAA,EAAW,cAAA,CAAe,UAAA,EAAY,QAAQ,CAAA;AAAA,MAC9C,UAAA,EAAY,cAAA,CAAe,UAAA,EAAY,SAAS,CAAA;AAAA,MAChD,OAAA,EAAS,cAAA,CAAe,UAAA,EAAY,MAAM;AAAA,KAC5C,CAAA;AAAA,IACA,CAAC,UAAU;AAAA,GACb;AAEA,EAAA,MAAM,KAAA,GAAsB;AAAA,IAC1B,GAAG,OAAA;AAAA,IACH,UAAA;AAAA,IACA,YAAA;AAAA,IACA,gBAAA;AAAA,IACA,iBAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,uBAAO,GAAA,CAAC,OAAA,CAAQ,QAAA,EAAR,EAAiB,OAAe,QAAA,EAAS,CAAA;AACnD;AAEO,MAAM,aAAa,MAAM;AAC9B,EAAA,MAAM,KAAA,GAAQA,aAAgB,OAAO,CAAA;AAErC,EAAA,IAAI,KAAA,KAAU,MAAA;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAEF,EAAA,OAAO,KAAA;AACT;;;;"}
1
+ {"version":3,"file":"Context.esm.js","sources":["../../../src/homePageComponents/VisitedByType/Context.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 {\n Dispatch,\n SetStateAction,\n createContext,\n useMemo,\n useState,\n useContext as useReactContext,\n} from 'react';\nimport { Visit } from '../../api/VisitsApi';\nimport { VisitedByTypeKind } from './Content';\n\nexport type ContextValueOnly<T = Visit> = {\n collapsed: boolean;\n numVisitsOpen: number;\n numVisitsTotal: number;\n visits: Array<T>;\n loading: boolean;\n kind: VisitedByTypeKind;\n};\n\nexport type ContextValue<T = Visit> = ContextValueOnly<T> & {\n setCollapsed: Dispatch<SetStateAction<boolean>>;\n setNumVisitsOpen: Dispatch<SetStateAction<number>>;\n setNumVisitsTotal: Dispatch<SetStateAction<number>>;\n setVisits: Dispatch<SetStateAction<Array<T>>>;\n setLoading: Dispatch<SetStateAction<boolean>>;\n setKind: Dispatch<SetStateAction<VisitedByTypeKind>>;\n setContext: Dispatch<SetStateAction<ContextValueOnly<T>>>;\n};\n\nconst defaultContextValueOnly: ContextValueOnly = {\n collapsed: true,\n numVisitsOpen: 3,\n numVisitsTotal: 8,\n visits: [],\n loading: true,\n kind: 'recent',\n};\n\nexport const defaultContextValue: ContextValue = {\n ...defaultContextValueOnly,\n setCollapsed: () => {},\n setNumVisitsOpen: () => {},\n setNumVisitsTotal: () => {},\n setVisits: () => {},\n setLoading: () => {},\n setKind: () => {},\n setContext: () => {},\n};\n\nexport const Context = createContext<ContextValue>(defaultContextValue);\n\nconst getFilteredSet =\n <T,>(\n setContext: Dispatch<SetStateAction<ContextValueOnly>>,\n contextKey: keyof ContextValueOnly,\n ) =>\n (e: SetStateAction<T>) =>\n setContext(state => ({\n ...state,\n [contextKey]:\n typeof e === 'function' ? (e as Function)(state[contextKey]) : e,\n }));\n\nexport const ContextProvider = ({ children }: { children: JSX.Element }) => {\n const [context, setContext] = useState<ContextValueOnly>({\n ...defaultContextValueOnly,\n });\n const {\n setCollapsed,\n setNumVisitsOpen,\n setNumVisitsTotal,\n setVisits,\n setLoading,\n setKind,\n } = useMemo(\n () => ({\n setCollapsed: getFilteredSet(setContext, 'collapsed'),\n setNumVisitsOpen: getFilteredSet(setContext, 'numVisitsOpen'),\n setNumVisitsTotal: getFilteredSet(setContext, 'numVisitsTotal'),\n setVisits: getFilteredSet(setContext, 'visits'),\n setLoading: getFilteredSet(setContext, 'loading'),\n setKind: getFilteredSet(setContext, 'kind'),\n }),\n [setContext],\n );\n\n const value: ContextValue = {\n ...context,\n setContext,\n setCollapsed,\n setNumVisitsOpen,\n setNumVisitsTotal,\n setVisits,\n setLoading,\n setKind,\n };\n\n return <Context.Provider value={value}>{children}</Context.Provider>;\n};\n\nexport const useContext = () => {\n const value = useReactContext(Context);\n\n if (value === undefined)\n throw new Error(\n 'VisitedByType useContext found undefined ContextValue, <ContextProvider/> could be missing',\n );\n\n return value;\n};\n\nexport default Context;\n"],"names":["useReactContext"],"mappings":";;;AA8CA,MAAM,uBAAA,GAA4C;AAAA,EAChD,SAAA,EAAW,IAAA;AAAA,EACX,aAAA,EAAe,CAAA;AAAA,EACf,cAAA,EAAgB,CAAA;AAAA,EAChB,QAAQ,EAAC;AAAA,EACT,OAAA,EAAS,IAAA;AAAA,EACT,IAAA,EAAM;AACR,CAAA;AAEO,MAAM,mBAAA,GAAoC;AAAA,EAC/C,GAAG,uBAAA;AAAA,EACH,cAAc,MAAM;AAAA,EAAC,CAAA;AAAA,EACrB,kBAAkB,MAAM;AAAA,EAAC,CAAA;AAAA,EACzB,mBAAmB,MAAM;AAAA,EAAC,CAAA;AAAA,EAC1B,WAAW,MAAM;AAAA,EAAC,CAAA;AAAA,EAClB,YAAY,MAAM;AAAA,EAAC,CAAA;AAAA,EACnB,SAAS,MAAM;AAAA,EAAC,CAAA;AAAA,EAChB,YAAY,MAAM;AAAA,EAAC;AACrB;AAEO,MAAM,OAAA,GAAU,cAA4B,mBAAmB;AAEtE,MAAM,iBACJ,CACE,UAAA,EACA,eAEF,CAAC,CAAA,KACC,WAAW,CAAA,KAAA,MAAU;AAAA,EACnB,GAAG,KAAA;AAAA,EACH,CAAC,UAAU,GACT,OAAO,CAAA,KAAM,aAAc,CAAA,CAAe,KAAA,CAAM,UAAU,CAAC,CAAA,GAAI;AACnE,CAAA,CAAE,CAAA;AAEC,MAAM,eAAA,GAAkB,CAAC,EAAE,QAAA,EAAS,KAAiC;AAC1E,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,QAAA,CAA2B;AAAA,IACvD,GAAG;AAAA,GACJ,CAAA;AACD,EAAA,MAAM;AAAA,IACJ,YAAA;AAAA,IACA,gBAAA;AAAA,IACA,iBAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF,GAAI,OAAA;AAAA,IACF,OAAO;AAAA,MACL,YAAA,EAAc,cAAA,CAAe,UAAA,EAAY,WAAW,CAAA;AAAA,MACpD,gBAAA,EAAkB,cAAA,CAAe,UAAA,EAAY,eAAe,CAAA;AAAA,MAC5D,iBAAA,EAAmB,cAAA,CAAe,UAAA,EAAY,gBAAgB,CAAA;AAAA,MAC9D,SAAA,EAAW,cAAA,CAAe,UAAA,EAAY,QAAQ,CAAA;AAAA,MAC9C,UAAA,EAAY,cAAA,CAAe,UAAA,EAAY,SAAS,CAAA;AAAA,MAChD,OAAA,EAAS,cAAA,CAAe,UAAA,EAAY,MAAM;AAAA,KAC5C,CAAA;AAAA,IACA,CAAC,UAAU;AAAA,GACb;AAEA,EAAA,MAAM,KAAA,GAAsB;AAAA,IAC1B,GAAG,OAAA;AAAA,IACH,UAAA;AAAA,IACA,YAAA;AAAA,IACA,gBAAA;AAAA,IACA,iBAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,uBAAO,GAAA,CAAC,OAAA,CAAQ,QAAA,EAAR,EAAiB,OAAe,QAAA,EAAS,CAAA;AACnD;AAEO,MAAM,aAAa,MAAM;AAC9B,EAAA,MAAM,KAAA,GAAQA,aAAgB,OAAO,CAAA;AAErC,EAAA,IAAI,KAAA,KAAU,MAAA;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAEF,EAAA,OAAO,KAAA;AACT;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"VisitedByType.esm.js","sources":["../../../src/homePageComponents/VisitedByType/VisitedByType.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 { VisitList } from '../../components/VisitList';\nimport { useContext } from './Context';\n\nexport const VisitedByType = () => {\n const { collapsed, numVisitsOpen, numVisitsTotal, visits, loading, kind } =\n useContext();\n\n return (\n <VisitList\n visits={visits}\n detailType={kind === 'top' ? 'hits' : 'time-ago'}\n collapsed={collapsed}\n numVisitsOpen={numVisitsOpen}\n numVisitsTotal={numVisitsTotal}\n loading={loading}\n />\n );\n};\n"],"names":[],"mappings":";;;;AAmBO,MAAM,gBAAgB,MAAM;AACjC,EAAA,MAAM,EAAE,WAAW,aAAA,EAAe,cAAA,EAAgB,QAAQ,OAAA,EAAS,IAAA,KACjE,UAAA,EAAW;AAEb,EAAA,uBACE,GAAA;AAAA,IAAC,SAAA;AAAA,IAAA;AAAA,MACC,MAAA;AAAA,MACA,UAAA,EAAY,IAAA,KAAS,KAAA,GAAQ,MAAA,GAAS,UAAA;AAAA,MACtC,SAAA;AAAA,MACA,aAAA;AAAA,MACA,cAAA;AAAA,MACA;AAAA;AAAA,GACF;AAEJ;;;;"}
1
+ {"version":3,"file":"VisitedByType.esm.js","sources":["../../../src/homePageComponents/VisitedByType/VisitedByType.tsx"],"sourcesContent":["/*\n * Copyright 2023 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 { VisitList } from '../../components/VisitList/VisitList';\nimport { useContext } from './Context';\n\nexport const VisitedByType = () => {\n const { collapsed, numVisitsOpen, numVisitsTotal, visits, loading, kind } =\n useContext();\n\n return (\n <VisitList\n visits={visits}\n detailType={kind === 'top' ? 'hits' : 'time-ago'}\n collapsed={collapsed}\n numVisitsOpen={numVisitsOpen}\n numVisitsTotal={numVisitsTotal}\n loading={loading}\n />\n );\n};\n"],"names":[],"mappings":";;;;AAmBO,MAAM,gBAAgB,MAAM;AACjC,EAAA,MAAM,EAAE,WAAW,aAAA,EAAe,cAAA,EAAgB,QAAQ,OAAA,EAAS,IAAA,KACjE,UAAA,EAAW;AAEb,EAAA,uBACE,GAAA;AAAA,IAAC,SAAA;AAAA,IAAA;AAAA,MACC,MAAA;AAAA,MACA,UAAA,EAAY,IAAA,KAAS,KAAA,GAAQ,MAAA,GAAS,UAAA;AAAA,MACtC,SAAA;AAAA,MACA,aAAA;AAAA,MACA,cAAA;AAAA,MACA;AAAA;AAAA,GACF;AAEJ;;;;"}
package/dist/index.d.ts CHANGED
@@ -139,15 +139,42 @@ interface VisitsApi {
139
139
  * @param queryParams - optional search query params.
140
140
  */
141
141
  list(queryParams?: VisitsApiQueryParams): Promise<Visit[]>;
142
+ /**
143
+ * Transform the pathname before it is considered for any other processing.
144
+ * @param pathname - the original pathname
145
+ */
146
+ transformPathname?(pathname: string): string;
147
+ /**
148
+ * Determine whether a visit should be saved.
149
+ * @param visit - page visit data
150
+ */
151
+ canSave?(visit: VisitInput): boolean | Promise<boolean>;
152
+ /**
153
+ * Add additional data to the visit before saving.
154
+ * @param visit - page visit data
155
+ */
156
+ enrichVisit?(visit: VisitInput): Promise<Record<string, any>> | Record<string, any>;
142
157
  }
143
158
  /** @public */
144
159
  declare const visitsApiRef: _backstage_core_plugin_api.ApiRef<VisitsApi>;
145
160
 
161
+ /**
162
+ * @public
163
+ * Type definition for visit data before it's saved (without auto-generated fields)
164
+ */
165
+ type VisitInput = {
166
+ name: string;
167
+ pathname: string;
168
+ entityRef?: string;
169
+ };
146
170
  /** @public */
147
171
  type VisitsStorageApiOptions = {
148
172
  limit?: number;
149
173
  storageApi: StorageApi;
150
174
  identityApi: IdentityApi;
175
+ transformPathname?: (pathname: string) => string;
176
+ canSave?: (visit: VisitInput) => boolean | Promise<boolean>;
177
+ enrichVisit?: (visit: VisitInput) => Promise<Record<string, any>> | Record<string, any>;
151
178
  };
152
179
  /**
153
180
  * @public
@@ -160,12 +187,31 @@ declare class VisitsStorageApi implements VisitsApi {
160
187
  private readonly storageApi;
161
188
  private readonly storageKeyPrefix;
162
189
  private readonly identityApi;
190
+ private readonly transformPathnameImpl?;
191
+ private readonly canSaveImpl?;
192
+ private readonly enrichVisitImpl?;
163
193
  static create(options: VisitsStorageApiOptions): VisitsStorageApi;
164
194
  private constructor();
165
195
  /**
166
196
  * Returns a list of visits through the visitsApi
167
197
  */
168
198
  list(queryParams?: VisitsApiQueryParams): Promise<Visit[]>;
199
+ /**
200
+ * Transform the pathname before it is considered for any other processing.
201
+ * @param pathname - the original pathname
202
+ * @returns the transformed pathname
203
+ */
204
+ transformPathname(pathname: string): string;
205
+ /**
206
+ * Determine whether a visit should be saved.
207
+ * @param visit - page visit data
208
+ */
209
+ canSave(visit: VisitInput): Promise<boolean>;
210
+ /**
211
+ * Add additional data to the visit before saving.
212
+ * @param visit - page visit data
213
+ */
214
+ enrichVisit(visit: VisitInput): Promise<Record<string, any>>;
169
215
  /**
170
216
  * Saves a visit through the visitsApi
171
217
  */
@@ -466,6 +512,44 @@ declare const VisitListener: ({ children, toEntityRef, visitName, }: {
466
512
  }) => string;
467
513
  }) => JSX.Element;
468
514
 
515
+ /**
516
+ * Type definition for the chip color function
517
+ * @public
518
+ */
519
+ type GetChipColorFunction = (visit: Visit) => string;
520
+ /**
521
+ * Type definition for the label function
522
+ * @public
523
+ */
524
+ type GetLabelFunction = (visit: Visit) => string;
525
+ /**
526
+ * Context value interface
527
+ * @public
528
+ */
529
+ interface VisitDisplayContextValue {
530
+ getChipColor: GetChipColorFunction;
531
+ getLabel: GetLabelFunction;
532
+ }
533
+ /**
534
+ * Props for the VisitDisplayProvider
535
+ * @public
536
+ */
537
+ interface VisitDisplayProviderProps {
538
+ children: ReactNode;
539
+ getChipColor?: GetChipColorFunction;
540
+ getLabel?: GetLabelFunction;
541
+ }
542
+ /**
543
+ * Provider component for VisitDisplay customization
544
+ * @public
545
+ */
546
+ declare const VisitDisplayProvider: ({ children, getChipColor, getLabel, }: VisitDisplayProviderProps) => react_jsx_runtime.JSX.Element;
547
+ /**
548
+ * Hook to use the VisitDisplay context
549
+ * @public
550
+ */
551
+ declare const useVisitDisplay: () => VisitDisplayContextValue;
552
+
469
553
  /** @public */
470
554
  declare const TemplateBackstageLogo: (props: {
471
555
  classes: {
@@ -528,4 +612,4 @@ declare const SettingsModal: (props: {
528
612
  children: JSX.Element;
529
613
  }) => react_jsx_runtime.JSX.Element;
530
614
 
531
- export { type Breakpoint, type CardConfig, type CardExtensionProps, type CardLayout, type CardSettings, type ClockConfig, ComponentAccordion, type ComponentParts, type ComponentRenderer, ComponentTab, ComponentTabs, CustomHomepageGrid, type CustomHomepageGridProps, FeaturedDocsCard, type FeaturedDocsCardProps, HeaderWorldClock, HomePageCompanyLogo, HomePageRandomJoke, HomePageRecentlyVisited, HomePageStarredEntities, HomePageToolkit, HomePageTopVisited, HomepageCompositionRoot, type LayoutConfiguration, type Operators, QuickStartCard, type QuickStartCardProps, type RendererProps, SettingsModal, type StarredEntitiesProps, TemplateBackstageLogo, TemplateBackstageLogoIcon, type Tool, type ToolkitContentProps, type Visit, VisitListener, type VisitedByTypeKind, type VisitedByTypeProps, type VisitsApi, type VisitsApiQueryParams, type VisitsApiSaveParams, VisitsStorageApi, type VisitsStorageApiOptions, VisitsWebStorageApi, type VisitsWebStorageApiOptions, WelcomeTitle, type WelcomeTitleLanguageProps, createCardExtension, homePlugin, isOperator, visitsApiRef };
615
+ export { type Breakpoint, type CardConfig, type CardExtensionProps, type CardLayout, type CardSettings, type ClockConfig, ComponentAccordion, type ComponentParts, type ComponentRenderer, ComponentTab, ComponentTabs, CustomHomepageGrid, type CustomHomepageGridProps, FeaturedDocsCard, type FeaturedDocsCardProps, type GetChipColorFunction, type GetLabelFunction, HeaderWorldClock, HomePageCompanyLogo, HomePageRandomJoke, HomePageRecentlyVisited, HomePageStarredEntities, HomePageToolkit, HomePageTopVisited, HomepageCompositionRoot, type LayoutConfiguration, type Operators, QuickStartCard, type QuickStartCardProps, type RendererProps, SettingsModal, type StarredEntitiesProps, TemplateBackstageLogo, TemplateBackstageLogoIcon, type Tool, type ToolkitContentProps, type Visit, type VisitDisplayContextValue, VisitDisplayProvider, type VisitDisplayProviderProps, type VisitInput, VisitListener, type VisitedByTypeKind, type VisitedByTypeProps, type VisitsApi, type VisitsApiQueryParams, type VisitsApiSaveParams, VisitsStorageApi, type VisitsStorageApiOptions, VisitsWebStorageApi, type VisitsWebStorageApiOptions, WelcomeTitle, type WelcomeTitleLanguageProps, createCardExtension, homePlugin, isOperator, useVisitDisplay, visitsApiRef };
package/dist/index.esm.js CHANGED
@@ -3,6 +3,7 @@ import 'react/jsx-runtime';
3
3
  import 'react-router-dom';
4
4
  export { CustomHomepageGrid } from './components/CustomHomepage/CustomHomepageGrid.esm.js';
5
5
  export { VisitListener } from './components/VisitListener.esm.js';
6
+ export { VisitDisplayProvider, useVisitDisplay } from './components/VisitList/Context.esm.js';
6
7
  export { TemplateBackstageLogo } from './assets/TemplateBackstageLogo.esm.js';
7
8
  export { TemplateBackstageLogoIcon } from './assets/TemplateBackstageLogoIcon.esm.js';
8
9
  export { SettingsModal, createCardExtension } from './deprecated.esm.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"}
@@ -1,5 +1,5 @@
1
1
  var name = "@backstage/plugin-home";
2
- var version = "0.8.14-next.0";
2
+ var version = "0.8.14-next.1";
3
3
  var description = "A Backstage plugin that helps you build a home page";
4
4
  var backstage = {
5
5
  role: "frontend-plugin",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-home",
3
- "version": "0.8.14-next.0",
3
+ "version": "0.8.14-next.1",
4
4
  "description": "A Backstage plugin that helps you build a home page",
5
5
  "backstage": {
6
6
  "role": "frontend-plugin",
@@ -71,12 +71,12 @@
71
71
  "@backstage/catalog-client": "1.12.1-next.0",
72
72
  "@backstage/catalog-model": "1.7.6-next.0",
73
73
  "@backstage/config": "1.3.6-next.0",
74
- "@backstage/core-app-api": "1.19.2-next.0",
74
+ "@backstage/core-app-api": "1.19.2-next.1",
75
75
  "@backstage/core-compat-api": "0.5.4-next.0",
76
- "@backstage/core-components": "0.18.3-next.0",
77
- "@backstage/core-plugin-api": "1.11.2-next.0",
78
- "@backstage/frontend-plugin-api": "0.12.2-next.0",
79
- "@backstage/plugin-catalog-react": "1.21.3-next.0",
76
+ "@backstage/core-components": "0.18.3-next.2",
77
+ "@backstage/core-plugin-api": "1.11.2-next.1",
78
+ "@backstage/frontend-plugin-api": "0.12.2-next.2",
79
+ "@backstage/plugin-catalog-react": "1.21.3-next.2",
80
80
  "@backstage/plugin-home-react": "0.1.32-next.0",
81
81
  "@backstage/theme": "0.7.0",
82
82
  "@material-ui/core": "^4.12.2",
@@ -94,8 +94,8 @@
94
94
  "zod": "^3.22.4"
95
95
  },
96
96
  "devDependencies": {
97
- "@backstage/cli": "0.34.5-next.0",
98
- "@backstage/dev-utils": "1.1.17-next.0",
97
+ "@backstage/cli": "0.34.5-next.1",
98
+ "@backstage/dev-utils": "1.1.17-next.1",
99
99
  "@backstage/test-utils": "1.7.13-next.0",
100
100
  "@testing-library/dom": "^10.0.0",
101
101
  "@testing-library/jest-dom": "^6.0.0",