feature(home-plugin): Recently Visited presentation use cases
This patch adds all the presentation use cases for Recently Visited. Handling loading, few items, custom list sizes etc. Signed-off-by: Renan Mendes Carvalho <aitherios@gmail.com>
This commit is contained in:
committed by
Camila Belo
parent
ec7fce2d41
commit
46aa692057
+81
-15
@@ -5,6 +5,7 @@
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { CardConfig as CardConfig_2 } from '@backstage/plugin-home-react';
|
||||
import { CardExtensionProps as CardExtensionProps_2 } from '@backstage/plugin-home-react';
|
||||
@@ -13,7 +14,7 @@ import { CardSettings as CardSettings_2 } from '@backstage/plugin-home-react';
|
||||
import { ComponentParts as ComponentParts_2 } from '@backstage/plugin-home-react';
|
||||
import { ComponentRenderer as ComponentRenderer_2 } from '@backstage/plugin-home-react';
|
||||
import { createCardExtension as createCardExtension_2 } from '@backstage/plugin-home-react';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactElement } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
@@ -49,7 +50,7 @@ export const ComponentAccordion: (props: {
|
||||
Actions?: (() => JSX.Element) | undefined;
|
||||
Settings?: (() => JSX.Element) | undefined;
|
||||
ContextProvider?: ((props: any) => JSX.Element) | undefined;
|
||||
}) => JSX_2.Element;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type ComponentParts = ComponentParts_2;
|
||||
@@ -62,7 +63,7 @@ export const ComponentTab: (props: {
|
||||
title: string;
|
||||
Content: () => JSX.Element;
|
||||
ContextProvider?: ((props: any) => JSX.Element) | undefined;
|
||||
}) => JSX_2.Element;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const ComponentTabs: (props: {
|
||||
@@ -71,7 +72,7 @@ export const ComponentTabs: (props: {
|
||||
label: string;
|
||||
Component: () => JSX.Element;
|
||||
}[];
|
||||
}) => JSX_2.Element;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const createCardExtension: typeof createCardExtension_2;
|
||||
@@ -79,7 +80,7 @@ export const createCardExtension: typeof createCardExtension_2;
|
||||
// @public
|
||||
export const CustomHomepageGrid: (
|
||||
props: CustomHomepageGridProps,
|
||||
) => React_2.JSX.Element;
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type CustomHomepageGridProps = {
|
||||
@@ -101,36 +102,41 @@ export type CustomHomepageGridProps = {
|
||||
export const HeaderWorldClock: (props: {
|
||||
clockConfigs: ClockConfig[];
|
||||
customTimeFormat?: Intl.DateTimeFormatOptions | undefined;
|
||||
}) => JSX_2.Element | null;
|
||||
}) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
export const HomePageCompanyLogo: (props: {
|
||||
logo?: ReactNode;
|
||||
className?: string | undefined;
|
||||
}) => JSX_2.Element;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const HomepageCompositionRoot: (props: {
|
||||
title?: string | undefined;
|
||||
children?: ReactNode;
|
||||
}) => JSX_2.Element;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const HomePageRandomJoke: (
|
||||
props: CardExtensionProps_2<{
|
||||
defaultCategory?: 'any' | 'programming' | undefined;
|
||||
}>,
|
||||
) => JSX_2.Element;
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const HomePageStarredEntities: (
|
||||
props: CardExtensionProps_2<unknown>,
|
||||
) => JSX_2.Element;
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const HomePageToolkit: (
|
||||
props: CardExtensionProps_2<ToolkitContentProps>,
|
||||
) => JSX_2.Element;
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const HomePageVisitedByType: (
|
||||
props: CardExtensionProps_2<VisitedByTypeProps>,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const homePlugin: BackstagePlugin<
|
||||
@@ -161,7 +167,7 @@ export const SettingsModal: (props: {
|
||||
close: Function;
|
||||
componentName?: string | undefined;
|
||||
children: JSX.Element;
|
||||
}) => JSX_2.Element;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TemplateBackstageLogo: (props: {
|
||||
@@ -169,10 +175,10 @@ export const TemplateBackstageLogo: (props: {
|
||||
svg: string;
|
||||
path: string;
|
||||
};
|
||||
}) => React_2.JSX.Element;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TemplateBackstageLogoIcon: () => React_2.JSX.Element;
|
||||
export const TemplateBackstageLogoIcon: () => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type Tool = {
|
||||
@@ -186,13 +192,73 @@ export type ToolkitContentProps = {
|
||||
tools: Tool[];
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "Visit" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type Visit = {
|
||||
id: string;
|
||||
name: string;
|
||||
pathname: string;
|
||||
hits: number;
|
||||
timestamp: number;
|
||||
entityRef?: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "VisitedByTypeProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type VisitedByTypeProps = {
|
||||
visits?: Array<Visit>;
|
||||
numVisitsOpen?: number;
|
||||
numVisitsTotal?: number;
|
||||
loading?: boolean;
|
||||
kind: VisitedByTypeKind;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "VisitFilter" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type VisitFilter = {
|
||||
field: string;
|
||||
operator: '<' | '<=' | '==' | '>' | '>=' | 'contains';
|
||||
value: JsonValue;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "VisitsApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export interface VisitsApi {
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
listUserVisits(queryParams: VisitsApiQueryParams): Promise<Visit[]>;
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
saveVisit(pageVisit: Omit<Visit, 'id' | 'hits' | 'timestamp'>): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "VisitsApiQueryParams" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type VisitsApiQueryParams = {
|
||||
limit?: number;
|
||||
orderBy?: Record<string, 'asc' | 'desc'>;
|
||||
filterBy?: VisitFilter[];
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "visitsApiRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const visitsApiRef: ApiRef<VisitsApi>;
|
||||
|
||||
// @public
|
||||
export const WelcomeTitle: ({
|
||||
language,
|
||||
}: WelcomeTitleLanguageProps) => JSX_2.Element;
|
||||
}: WelcomeTitleLanguageProps) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type WelcomeTitleLanguageProps = {
|
||||
language?: string[];
|
||||
};
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/homePageComponents/VisitedByType/Content.d.ts:9:5 - (ae-forgotten-export) The symbol "VisitedByTypeKind" needs to be exported by the entry point index.d.ts
|
||||
```
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-home-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
@@ -49,6 +50,7 @@
|
||||
"@rjsf/utils": "5.13.0",
|
||||
"@rjsf/validator-ajv8": "5.13.0",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-resizable": "^3.0.4",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
|
||||
/**
|
||||
@experimental
|
||||
* Model for a visit entity.
|
||||
*/
|
||||
export type Visit = {
|
||||
/**
|
||||
* The auto-generated visit identification.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The visited entity, usually an entity id.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The visited url pathname, usually the entity route.
|
||||
*/
|
||||
pathname: string;
|
||||
/**
|
||||
* An individual view count.
|
||||
*/
|
||||
hits: number;
|
||||
/**
|
||||
* Last date and time of visit. Format: unix epoch in ms.
|
||||
*/
|
||||
timestamp: number;
|
||||
/**
|
||||
* Optional entity reference. See stringifyEntityRef from catalog-model.
|
||||
*/
|
||||
entityRef?: string;
|
||||
};
|
||||
|
||||
export type VisitFilter = {
|
||||
field: string;
|
||||
operator: '<' | '<=' | '==' | '>' | '>=' | 'contains';
|
||||
value: JsonValue;
|
||||
};
|
||||
|
||||
/**
|
||||
@experimental
|
||||
* This data structure represents the parameters associated with search queries for visits.
|
||||
*/
|
||||
export type VisitsApiQueryParams = {
|
||||
/**
|
||||
* Limits the number of results returned. The default is 8.
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* A record for which the key is a field name to sort on, and the value is the sort direction.
|
||||
* For a multi-field sorting query, add multi entries to the record.
|
||||
* @example
|
||||
* Sort ascending by the timestamp field.
|
||||
* ```
|
||||
* { orderBy: { timestamp: 'asc' } }
|
||||
* ```
|
||||
*/
|
||||
orderBy?: Record<string, 'asc' | 'desc'>;
|
||||
/**
|
||||
* Allows filtering visits on number of hits, timestamp and/or entityRef attributes.
|
||||
* @example
|
||||
* Most popular docs on the past 7 days
|
||||
* ```
|
||||
* { orderBy: { hits: 'desc' }, filterBy: [{ field: 'timestamp', operator: '>=', value: <date> }, { field: 'entityRef', operator: 'contains', value: 'docs' }] }
|
||||
* ```
|
||||
*/
|
||||
filterBy?: VisitFilter[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Visits API public contract.
|
||||
*/
|
||||
export interface VisitsApi {
|
||||
/**
|
||||
* Persist a new visit.
|
||||
* @param pageVisit | a new visit data.
|
||||
*/
|
||||
saveVisit(pageVisit: Omit<Visit, 'id' | 'hits' | 'timestamp'>): Promise<void>;
|
||||
/**
|
||||
* Get the logged user visits.
|
||||
* @param queryParams | optional search query params.
|
||||
*/
|
||||
listUserVisits(queryParams: VisitsApiQueryParams): Promise<Visit[]>;
|
||||
}
|
||||
|
||||
export const visitsApiRef = createApiRef<VisitsApi>({
|
||||
id: 'homepage.visits',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './VisitsApi';
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Chip, makeStyles } from '@material-ui/core';
|
||||
import { colorVariants } from '@backstage/theme';
|
||||
import { Visit } from '../../api/VisitsApi';
|
||||
import { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
chip: {
|
||||
color: theme.palette.common.white,
|
||||
fontWeight: 'bold',
|
||||
margin: 0,
|
||||
},
|
||||
}));
|
||||
const maybeEntity = (visit: Visit): CompoundEntityRef | undefined => {
|
||||
try {
|
||||
return parseEntityRef(visit?.entityRef ?? '');
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const getColorByIndex = (index: number) => {
|
||||
const variants = Object.keys(colorVariants);
|
||||
const variantIndex = index % variants.length;
|
||||
return colorVariants[variants[variantIndex]][0];
|
||||
};
|
||||
const getChipColor = (entity: CompoundEntityRef | undefined): string => {
|
||||
const defaultColor = getColorByIndex(0);
|
||||
if (!entity) return defaultColor;
|
||||
|
||||
// IDEA: Use or replicate useAllKinds hook thus supporting all software catalog
|
||||
// registered kinds. See:
|
||||
// plugins/catalog-react/src/components/EntityKindPicker/kindFilterUtils.ts
|
||||
// Provide extension point to register your own color code.
|
||||
const entityKinds = [
|
||||
'component',
|
||||
'template',
|
||||
'api',
|
||||
'group',
|
||||
'user',
|
||||
'resource',
|
||||
'system',
|
||||
'domain',
|
||||
'location',
|
||||
];
|
||||
const foundIndex = entityKinds.indexOf(
|
||||
entity.kind.toLocaleLowerCase('en-US'),
|
||||
);
|
||||
return foundIndex === -1 ? defaultColor : getColorByIndex(foundIndex + 1);
|
||||
};
|
||||
|
||||
export const ItemCategory = ({ visit }: { visit: Visit }) => {
|
||||
const classes = useStyles();
|
||||
const entity = maybeEntity(visit);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
size="small"
|
||||
className={classes.chip}
|
||||
label={(entity?.kind ?? 'Other').toLocaleLowerCase('en-US')}
|
||||
style={{ background: getChipColor(entity) }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { Visit } from '../../api/VisitsApi';
|
||||
import { format, formatDistance, formatISO, isToday } from 'date-fns';
|
||||
|
||||
const ItemDetailHits = ({ visit }: { visit: Visit }) => (
|
||||
<Typography component="span" variant="caption" color="textSecondary">
|
||||
{visit.hits} time{visit.hits > 1 ? 's' : ''}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const ItemDetailTimeAgo = ({ visit }: { visit: Visit }) => (
|
||||
<Typography
|
||||
component="time"
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
dateTime={formatISO(visit.timestamp)}
|
||||
>
|
||||
{isToday(visit.timestamp)
|
||||
? format(visit.timestamp, 'HH:mm')
|
||||
: formatDistance(visit.timestamp, Date.now(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
export type ItemDetailType = 'time-ago' | 'hits';
|
||||
|
||||
export const ItemDetail = ({
|
||||
visit,
|
||||
type,
|
||||
}: {
|
||||
visit: Visit;
|
||||
type: ItemDetailType;
|
||||
}) =>
|
||||
type === 'time-ago' ? (
|
||||
<ItemDetailTimeAgo visit={visit} />
|
||||
) : (
|
||||
<ItemDetailHits visit={visit} />
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Typography, makeStyles } from '@material-ui/core';
|
||||
import { Visit } from '../../api/VisitsApi';
|
||||
import { Link } from '@backstage/core-components';
|
||||
|
||||
const useStyles = makeStyles(_theme => ({
|
||||
name: {
|
||||
marginLeft: '0.8rem',
|
||||
marginRight: '0.8rem',
|
||||
},
|
||||
}));
|
||||
export const ItemName = ({ visit }: { visit: Visit }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Typography
|
||||
component={Link}
|
||||
to={visit.pathname}
|
||||
noWrap
|
||||
className={classes.name}
|
||||
>
|
||||
{visit.name}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Collapse, List, Typography, makeStyles } from '@material-ui/core';
|
||||
import { Visit } from '../../api/VisitsApi';
|
||||
import { VisitListItem } from './VisitListItem';
|
||||
import { ItemDetailType } from './ItemDetail';
|
||||
import { VisitListEmpty } from './VisitListEmpty';
|
||||
import { VisitListFew } from './VisitListFew';
|
||||
import { VisitListSkeleton } from './VisitListSkeleton';
|
||||
|
||||
const useStyles = makeStyles(_theme => ({
|
||||
title: {
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
}));
|
||||
|
||||
export const VisitList = ({
|
||||
visits,
|
||||
title,
|
||||
detailType,
|
||||
numVisitsOpen = 3,
|
||||
numVisitsTotal = 8,
|
||||
collapsed = true,
|
||||
loading = false,
|
||||
}: {
|
||||
visits: Array<Visit>;
|
||||
title: string;
|
||||
detailType: ItemDetailType;
|
||||
numVisitsOpen?: number;
|
||||
numVisitsTotal?: number;
|
||||
collapsed: boolean;
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
let listBody: React.ReactElement = <></>;
|
||||
if (loading) {
|
||||
listBody = (
|
||||
<VisitListSkeleton
|
||||
numVisitsOpen={numVisitsOpen}
|
||||
numVisitsTotal={numVisitsTotal}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
);
|
||||
} else if (visits.length === 0) {
|
||||
listBody = <VisitListEmpty />;
|
||||
} else if (visits.length < numVisitsOpen) {
|
||||
listBody = (
|
||||
<>
|
||||
{visits.map((visit, index) => (
|
||||
<VisitListItem visit={visit} key={index} detailType={detailType} />
|
||||
))}
|
||||
<VisitListFew />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
listBody = (
|
||||
<>
|
||||
{visits.slice(0, numVisitsOpen).map((visit, index) => (
|
||||
<VisitListItem visit={visit} key={index} detailType={detailType} />
|
||||
))}
|
||||
{visits.length > numVisitsOpen && (
|
||||
<Collapse in={!collapsed}>
|
||||
{visits.slice(numVisitsOpen, numVisitsTotal).map((visit, index) => (
|
||||
<VisitListItem
|
||||
visit={visit}
|
||||
key={index}
|
||||
detailType={detailType}
|
||||
/>
|
||||
))}
|
||||
</Collapse>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h5" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
<List dense disablePadding>
|
||||
{listBody}
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Typography } from '@material-ui/core';
|
||||
|
||||
export const VisitListEmpty = () => (
|
||||
<>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
There are no visits to show yet.
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Once you start using Backstage, your visits will appear here as a quick
|
||||
link to carry on where you left off.
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Typography } from '@material-ui/core';
|
||||
|
||||
export const VisitListFew = () => (
|
||||
<>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
The more pages you visit, the more pages will appear here.
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { Visit } from '../../api/VisitsApi';
|
||||
import { ItemName } from './ItemName';
|
||||
import { ItemDetail, ItemDetailType } from './ItemDetail';
|
||||
import { ItemCategory } from './ItemCategory';
|
||||
|
||||
const useStyles = makeStyles(_theme => ({
|
||||
avatar: {
|
||||
minWidth: 0,
|
||||
},
|
||||
}));
|
||||
export const VisitListItem = ({
|
||||
visit,
|
||||
detailType,
|
||||
}: {
|
||||
visit: Visit;
|
||||
detailType: ItemDetailType;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<ListItem disableGutters>
|
||||
<ListItemAvatar className={classes.avatar}>
|
||||
<ItemCategory visit={visit} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={<ItemName visit={visit} />}
|
||||
secondary={<ItemDetail visit={visit} type={detailType} />}
|
||||
disableTypography
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Collapse,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
|
||||
const useStyles = makeStyles(_theme => ({
|
||||
skeleton: {
|
||||
borderRadius: 30,
|
||||
},
|
||||
}));
|
||||
|
||||
const VisitListItemSkeleton = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<ListItem disableGutters>
|
||||
<ListItemAvatar>
|
||||
<Skeleton
|
||||
className={classes.skeleton}
|
||||
variant="rect"
|
||||
width={50}
|
||||
height={24}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={<Skeleton variant="text" width="100%" height={28} />}
|
||||
disableTypography
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const VisitListSkeleton = ({
|
||||
numVisitsOpen,
|
||||
numVisitsTotal,
|
||||
collapsed,
|
||||
}: {
|
||||
numVisitsOpen: number;
|
||||
numVisitsTotal: number;
|
||||
collapsed: boolean;
|
||||
}) => (
|
||||
<>
|
||||
{Array(numVisitsOpen)
|
||||
.fill(null)
|
||||
.map((_e, index) => (
|
||||
<VisitListItemSkeleton key={index} />
|
||||
))}
|
||||
{numVisitsTotal > numVisitsOpen && (
|
||||
<Collapse in={!collapsed}>
|
||||
{Array(numVisitsTotal - numVisitsOpen)
|
||||
.fill(null)
|
||||
.map((_e, index) => (
|
||||
<VisitListItemSkeleton key={index} />
|
||||
))}
|
||||
</Collapse>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { VisitList } from './VisitList';
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { useContext } from './Context';
|
||||
|
||||
export const Actions = () => {
|
||||
const { collapsed, setCollapsed, visits, numVisitsOpen, loading } =
|
||||
useContext();
|
||||
const onClick = useCallback(
|
||||
() => setCollapsed(prevCollapsed => !prevCollapsed),
|
||||
[setCollapsed],
|
||||
);
|
||||
const label = collapsed ? 'View More' : 'View Less';
|
||||
|
||||
if (!loading && visits.length <= numVisitsOpen) return <></>;
|
||||
|
||||
return (
|
||||
<Button variant="text" onClick={onClick}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -14,11 +14,69 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { RecentlyVisited } from './RecentlyVisited';
|
||||
import { Visit, visitsApiRef } from '../../api/VisitsApi';
|
||||
import { useContext } from './Context';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
export type RecentlyVisitedProps = {
|
||||
visits?: Array<Visit>;
|
||||
numVisitsOpen?: number;
|
||||
numVisitsTotal?: number;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Display recently visited pages for the homepage
|
||||
* @public
|
||||
*/
|
||||
export const Content = () => <RecentlyVisited />;
|
||||
export const Content = ({
|
||||
visits,
|
||||
numVisitsOpen,
|
||||
numVisitsTotal,
|
||||
loading,
|
||||
}: RecentlyVisitedProps) => {
|
||||
const { setVisits, setNumVisitsOpen, setNumVisitsTotal, setLoading } =
|
||||
useContext();
|
||||
// Allows behavior override from properties
|
||||
useEffect(() => {
|
||||
if (visits) {
|
||||
setVisits(visits);
|
||||
setLoading(false);
|
||||
} else if (loading) {
|
||||
setLoading(loading);
|
||||
}
|
||||
if (numVisitsOpen) setNumVisitsOpen(numVisitsOpen);
|
||||
if (numVisitsTotal) setNumVisitsTotal(numVisitsTotal);
|
||||
}, [
|
||||
visits,
|
||||
numVisitsOpen,
|
||||
numVisitsTotal,
|
||||
loading,
|
||||
setVisits,
|
||||
setNumVisitsOpen,
|
||||
setNumVisitsTotal,
|
||||
setLoading,
|
||||
]);
|
||||
// Fetches data from visitsApi in case visits and loading are not provided
|
||||
const visitsApi = useApi(visitsApiRef);
|
||||
const { loading: reqLoading } = useAsync(async () => {
|
||||
if (!visits && !loading) {
|
||||
await visitsApi
|
||||
.listUserVisits({
|
||||
limit: numVisitsTotal ?? 8,
|
||||
orderBy: { timestamp: 'desc' },
|
||||
})
|
||||
.then(setVisits);
|
||||
}
|
||||
}, [visitsApi, visits, loading, setVisits]);
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
setLoading(reqLoading);
|
||||
}
|
||||
}, [loading, setLoading, reqLoading]);
|
||||
|
||||
return <RecentlyVisited />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createContext } from 'react';
|
||||
import { Visit } from '../../api/VisitsApi';
|
||||
|
||||
export type ContextValue = {
|
||||
collapsed: boolean;
|
||||
setCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
numVisitsOpen: number;
|
||||
setNumVisitsOpen: React.Dispatch<React.SetStateAction<number>>;
|
||||
numVisitsTotal: number;
|
||||
setNumVisitsTotal: React.Dispatch<React.SetStateAction<number>>;
|
||||
visits: Array<Visit>;
|
||||
setVisits: React.Dispatch<React.SetStateAction<Array<Visit>>>;
|
||||
loading: boolean;
|
||||
setLoading: React.Dispatch<boolean>;
|
||||
};
|
||||
|
||||
const defaultContextValue = {
|
||||
collapsed: true,
|
||||
setCollapsed: () => {},
|
||||
numVisitsOpen: 3,
|
||||
setNumVisitsOpen: () => {},
|
||||
numVisitsTotal: 8,
|
||||
setNumVisitsTotal: () => {},
|
||||
visits: [],
|
||||
setVisits: () => {},
|
||||
loading: true,
|
||||
setLoading: () => {},
|
||||
};
|
||||
|
||||
const Context = createContext<ContextValue>(defaultContextValue);
|
||||
|
||||
export const ContextProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const [collapsed, setCollapsed] = React.useState(
|
||||
defaultContextValue.collapsed,
|
||||
);
|
||||
const [numVisitsOpen, setNumVisitsOpen] = React.useState(
|
||||
defaultContextValue.numVisitsOpen,
|
||||
);
|
||||
const [numVisitsTotal, setNumVisitsTotal] = React.useState(
|
||||
defaultContextValue.numVisitsTotal,
|
||||
);
|
||||
const [visits, setVisits] = React.useState<Array<Visit>>(
|
||||
defaultContextValue.visits,
|
||||
);
|
||||
const [loading, setLoading] = React.useState<boolean>(
|
||||
defaultContextValue.loading,
|
||||
);
|
||||
|
||||
const value: ContextValue = {
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
numVisitsOpen,
|
||||
setNumVisitsOpen,
|
||||
numVisitsTotal,
|
||||
setNumVisitsTotal,
|
||||
visits,
|
||||
setVisits,
|
||||
loading,
|
||||
setLoading,
|
||||
};
|
||||
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
export const useContext = () => {
|
||||
const value = React.useContext(Context);
|
||||
|
||||
if (value === undefined)
|
||||
throw new Error(
|
||||
'RecentlyVisited useContext found undefined ContextValue, <ContextProvider/> could be missing',
|
||||
);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export default Context;
|
||||
@@ -15,15 +15,91 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { ComponentType, PropsWithChildren } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { HomePageRecentlyVisited } from '../../plugin';
|
||||
import { Visit, visitsApiRef } from '../../api/VisitsApi';
|
||||
|
||||
const visits: Array<Visit> = [
|
||||
{
|
||||
id: 'explore',
|
||||
name: 'Explore Backstage',
|
||||
pathname: '/explore',
|
||||
hits: 35,
|
||||
timestamp: Date.now() - 86400_000 * 1,
|
||||
},
|
||||
{
|
||||
id: 'user-1',
|
||||
name: 'Guest',
|
||||
pathname: '/catalog/default/user/guest',
|
||||
hits: 30,
|
||||
timestamp: Date.now() - 86400_000 * 2,
|
||||
entityRef: 'User:default/guest',
|
||||
},
|
||||
{
|
||||
id: 'audio-playback',
|
||||
name: 'Audio Playback',
|
||||
pathname: '/catalog/default/system/audio-playback',
|
||||
hits: 25,
|
||||
timestamp: Date.now() - 86400_000 * 3,
|
||||
entityRef: 'System:default/audio-playback',
|
||||
},
|
||||
{
|
||||
id: 'team-a',
|
||||
name: 'Team A',
|
||||
pathname: '/catalog/default/group/team-a',
|
||||
hits: 20,
|
||||
timestamp: Date.now() - 86400_000 * 4,
|
||||
entityRef: 'Group:default/team-a',
|
||||
},
|
||||
{
|
||||
id: 'playback-order',
|
||||
name: 'Playback Order',
|
||||
pathname: '/catalog/default/component/playback-order',
|
||||
hits: 15,
|
||||
timestamp: Date.now() - 86400_000 * 5,
|
||||
entityRef: 'Component:default/playback-order',
|
||||
},
|
||||
{
|
||||
id: 'playback',
|
||||
name: 'Playback',
|
||||
pathname: '/catalog/default/domain/playback',
|
||||
hits: 10,
|
||||
timestamp: Date.now() - 86400_000 * 6,
|
||||
entityRef: 'Domain:default/playback',
|
||||
},
|
||||
{
|
||||
id: 'hello-world',
|
||||
name: 'Hello World gRPC',
|
||||
pathname: '/catalog/default/api/hello-world',
|
||||
hits: 5,
|
||||
timestamp: Date.now() - 86400_000 * 7,
|
||||
entityRef: 'API:default/hello-world',
|
||||
},
|
||||
{
|
||||
id: 'tech-radar',
|
||||
name: 'Tech Radar',
|
||||
pathname: '/tech-radar',
|
||||
hits: 1,
|
||||
timestamp: Date.now() - 360_000,
|
||||
},
|
||||
];
|
||||
|
||||
const mockVisitsApi = {
|
||||
saveVisit: async () => {},
|
||||
listUserVisits: async () => visits,
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/RecentlyVisited',
|
||||
decorators: [
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) => wrapInTestApp(<Story />),
|
||||
(Story: ComponentType<PropsWithChildren<{}>>) =>
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[visitsApiRef, mockVisitsApi]]}>
|
||||
<Story />
|
||||
</TestApiProvider>,
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -34,3 +110,35 @@ export const Default = () => {
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export const Empty = () => {
|
||||
return (
|
||||
<Grid item xs={12} md={6}>
|
||||
<HomePageRecentlyVisited visits={[]} />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export const FewItems = () => {
|
||||
return (
|
||||
<Grid item xs={12} md={6}>
|
||||
<HomePageRecentlyVisited visits={visits.slice(0, 1)} />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoreItems = () => {
|
||||
return (
|
||||
<Grid item xs={12} md={6}>
|
||||
<HomePageRecentlyVisited numVisitsOpen={5} numVisitsTotal={6} />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<Grid item xs={12} md={6}>
|
||||
<HomePageRecentlyVisited numVisitsOpen={5} numVisitsTotal={6} loading />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,10 +15,25 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
import { VisitList } from '../../components/VisitList';
|
||||
import { useContext } from './Context';
|
||||
/**
|
||||
* Display recently visited pages for the homepage
|
||||
* @public
|
||||
*/
|
||||
export const RecentlyVisited = () => <Typography>RecentlyVisited</Typography>;
|
||||
export const RecentlyVisited = () => {
|
||||
const { collapsed, numVisitsOpen, numVisitsTotal, visits, loading } =
|
||||
useContext();
|
||||
|
||||
return (
|
||||
<VisitList
|
||||
visits={visits}
|
||||
title="Recently visited"
|
||||
detailType="time-ago"
|
||||
collapsed={collapsed}
|
||||
numVisitsOpen={numVisitsOpen}
|
||||
numVisitsTotal={numVisitsTotal}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,3 +15,6 @@
|
||||
*/
|
||||
|
||||
export { Content } from './Content';
|
||||
export { Actions } from './Actions';
|
||||
export { ContextProvider } from './Context';
|
||||
export type { RecentlyVisitedProps } from './Content';
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
export type { ToolkitContentProps, Tool } from './Toolkit';
|
||||
export type { ClockConfig } from './HeaderWorldClock';
|
||||
export type { WelcomeTitleLanguageProps } from './WelcomeTitle';
|
||||
export type { RecentlyVisitedProps } from './RecentlyVisited';
|
||||
|
||||
@@ -37,3 +37,4 @@ export * from './components';
|
||||
export * from './assets';
|
||||
export * from './homePageComponents';
|
||||
export * from './deprecated';
|
||||
export * from './api';
|
||||
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createCardExtension } from '@backstage/plugin-home-react';
|
||||
import { ToolkitContentProps } from './homePageComponents';
|
||||
import {
|
||||
ToolkitContentProps,
|
||||
RecentlyVisitedProps,
|
||||
} from './homePageComponents';
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
/** @public */
|
||||
@@ -178,7 +181,7 @@ export const HeaderWorldClock = homePlugin.provide(
|
||||
* @public
|
||||
*/
|
||||
export const HomePageRecentlyVisited = homePlugin.provide(
|
||||
createCardExtension({
|
||||
createCardExtension<RecentlyVisitedProps>({
|
||||
name: 'HomePageRecentlyVisited',
|
||||
components: () => import('./homePageComponents/RecentlyVisited'),
|
||||
}),
|
||||
|
||||
@@ -7395,6 +7395,7 @@ __metadata:
|
||||
"@backstage/plugin-home-react": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@material-ui/core": ^4.12.2
|
||||
"@material-ui/icons": ^4.9.1
|
||||
"@material-ui/lab": 4.0.0-alpha.61
|
||||
@@ -7408,6 +7409,7 @@ __metadata:
|
||||
"@testing-library/user-event": ^14.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0
|
||||
"@types/react-grid-layout": ^1.3.2
|
||||
date-fns: ^2.30.0
|
||||
lodash: ^4.17.21
|
||||
msw: ^1.0.0
|
||||
react-grid-layout: ^1.3.4
|
||||
|
||||
Reference in New Issue
Block a user