TechDocs Homepage (#4401)
* add tabs to homepage Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add new overview content component Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add new owned content component Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add types Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add test for owned content component Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add test for overview content Signed-off-by: Emma Indal <emma.indahl@gmail.com> * document name to link and change copy to clipboard link Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add changeset Signed-off-by: Emma Indal <emma.indahl@gmail.com> * rename changeset file Signed-off-by: Emma Indal <emma.indahl@gmail.com> * fix types Signed-off-by: Emma Indal <emma.indahl@gmail.com> * move home components out from reader dir to its own dir Signed-off-by: Emma Indal <emma.indahl@gmail.com> * Update plugins/techdocs/src/reader/components/OverviewContent.tsx Co-authored-by: Himanshu Mishra <himanshu@orkohunter.net> Signed-off-by: Emma Indal <emma.indahl@gmail.com> * enable search in table Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add docs icon to core icons Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add example TechDocs support button config Signed-off-by: Emma Indal <emma.indahl@gmail.com> * use isOwnerOff and useOwnUser to see documents that are owned via group membership Signed-off-by: Emma Indal <emma.indahl@gmail.com> * update tests Signed-off-by: Emma Indal <emma.indahl@gmail.com> * cleanup imports Signed-off-by: Emma Indal <emma.indahl@gmail.com> * leave techdocs support config out of this PR Signed-off-by: Emma Indal <emma.indahl@gmail.com> * prettieeeeeer Signed-off-by: Emma Indal <emma.indahl@gmail.com> * copy paste useOwnUser hook over to techdocs plugin Signed-off-by: Emma Indal <emma.indahl@gmail.com> * add changeset Signed-off-by: Emma Indal <emma.indahl@gmail.com> * delete unused package Signed-off-by: Emma Indal <emma.indahl@gmail.com> Co-authored-by: Himanshu Mishra <himanshu@orkohunter.net>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Extended TechDocs HomePage with owned documents
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-api': patch
|
||||
---
|
||||
|
||||
Added new Docs Icon to Core Icons
|
||||
@@ -24,6 +24,8 @@ import MuiHelpIcon from '@material-ui/icons/Help';
|
||||
import MuiPeopleIcon from '@material-ui/icons/People';
|
||||
import MuiPersonIcon from '@material-ui/icons/Person';
|
||||
import MuiWarningIcon from '@material-ui/icons/Warning';
|
||||
import MuiDocsIcon from '@material-ui/icons/Description';
|
||||
|
||||
import React from 'react';
|
||||
import { useApp } from '../app/AppContext';
|
||||
import { IconComponent, IconComponentMap, SystemIconKey } from './types';
|
||||
@@ -38,6 +40,7 @@ export const defaultSystemIcons: IconComponentMap = {
|
||||
help: MuiHelpIcon,
|
||||
user: MuiPersonIcon,
|
||||
warning: MuiWarningIcon,
|
||||
docs: MuiDocsIcon,
|
||||
};
|
||||
|
||||
const overridableSystemIcon = (key: SystemIconKey): IconComponent => {
|
||||
@@ -58,3 +61,4 @@ export const GroupIcon = overridableSystemIcon('group');
|
||||
export const HelpIcon = overridableSystemIcon('help');
|
||||
export const UserIcon = overridableSystemIcon('user');
|
||||
export const WarningIcon = overridableSystemIcon('warning');
|
||||
export const DocsIcon = overridableSystemIcon('docs');
|
||||
|
||||
@@ -26,7 +26,8 @@ export type SystemIconKey =
|
||||
| 'group'
|
||||
| 'help'
|
||||
| 'user'
|
||||
| 'warning';
|
||||
| 'warning'
|
||||
| 'docs';
|
||||
|
||||
export type IconComponent = ComponentType<SvgIconProps>;
|
||||
export type IconKey = SystemIconKey | string;
|
||||
|
||||
@@ -8,4 +8,4 @@ metadata:
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
owner: documented@example.com
|
||||
owner: user:guest
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
rootDocsRouteRef,
|
||||
rootCatalogDocsRouteRef,
|
||||
} from './plugin';
|
||||
import { TechDocsHome } from './reader/components/TechDocsHome';
|
||||
import { TechDocsHome } from './home/components/TechDocsHome';
|
||||
import { TechDocsPage } from './reader/components/TechDocsPage';
|
||||
import { EntityPageDocs } from './EntityPageDocs';
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { OverviewContent } from './OverviewContent';
|
||||
|
||||
describe('TechDocs Overview Content', () => {
|
||||
it('should render all TechDocs Documents', async () => {
|
||||
const { findByText } = render(
|
||||
wrapInTestApp(
|
||||
<OverviewContent
|
||||
entities={[
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
},
|
||||
spec: {
|
||||
owner: 'techdocs@example.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind2',
|
||||
metadata: {
|
||||
name: 'testName2',
|
||||
},
|
||||
spec: {
|
||||
owner: 'not-owned@example.com',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(await findByText('Overview')).toBeInTheDocument();
|
||||
expect(
|
||||
await findByText(
|
||||
/Explore your internal technical ecosystem through documentation./i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(await findByText('testName')).toBeInTheDocument();
|
||||
expect(await findByText('testName2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { generatePath } from 'react-router-dom';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
ItemCardGrid,
|
||||
ItemCardHeader,
|
||||
} from '@backstage/core';
|
||||
import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core';
|
||||
|
||||
import { rootDocsRouteRef } from '../../plugin';
|
||||
|
||||
export const OverviewContent = ({
|
||||
entities,
|
||||
}: {
|
||||
entities: Entity[] | undefined;
|
||||
}) => {
|
||||
if (!entities) return null;
|
||||
return (
|
||||
<Content>
|
||||
<ContentHeader
|
||||
title="Overview"
|
||||
description="Explore your internal technical ecosystem through documentation."
|
||||
>
|
||||
<SupportButton>Discover documentation in your ecosystem.</SupportButton>
|
||||
</ContentHeader>
|
||||
<ItemCardGrid data-testid="docs-explore">
|
||||
{!entities?.length
|
||||
? null
|
||||
: entities.map((entity, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardMedia>
|
||||
<ItemCardHeader title={entity.metadata.name} />
|
||||
</CardMedia>
|
||||
<CardContent>{entity.metadata.description}</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
to={generatePath(rootDocsRouteRef.path, {
|
||||
namespace: entity.metadata.namespace ?? 'default',
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
})}
|
||||
color="primary"
|
||||
>
|
||||
Read Docs
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
))}
|
||||
</ItemCardGrid>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { OwnedContent } from './OwnedContent';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useOwnUser: () => {
|
||||
return {
|
||||
value: {
|
||||
apiVersion: 'version',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'owned',
|
||||
namespace: 'default',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'TestKind',
|
||||
name: 'testName',
|
||||
},
|
||||
type: 'ownerOf',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('TechDocs Owned Content', () => {
|
||||
it('should render TechDocs Owned Documents', async () => {
|
||||
const { findByText, queryByText } = render(
|
||||
wrapInTestApp(
|
||||
<OwnedContent
|
||||
entities={[
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
},
|
||||
spec: {
|
||||
owner: 'user:owned',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'user',
|
||||
namespace: 'default',
|
||||
name: 'owned',
|
||||
},
|
||||
type: 'ownedBy',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind2',
|
||||
metadata: {
|
||||
name: 'testName2',
|
||||
},
|
||||
spec: {
|
||||
owner: 'not-owned@example.com',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'user',
|
||||
namespace: 'default',
|
||||
name: 'not-owned',
|
||||
},
|
||||
type: 'ownedBy',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(await findByText('Owned documents')).toBeInTheDocument();
|
||||
expect(await findByText(/Access your documentation./i)).toBeInTheDocument();
|
||||
expect(await findByText('testName')).toBeInTheDocument();
|
||||
expect(await queryByText('testName2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty state if no owned documents exist', async () => {
|
||||
const { findByText } = render(
|
||||
wrapInTestApp(<OwnedContent entities={[]} />),
|
||||
);
|
||||
|
||||
expect(await findByText('No documents to show')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { useCopyToClipboard } from 'react-use';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
|
||||
import { IconButton, Tooltip } from '@material-ui/core';
|
||||
import ShareIcon from '@material-ui/icons/Share';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
Table,
|
||||
EmptyState,
|
||||
Button,
|
||||
SubvalueCell,
|
||||
Link,
|
||||
} from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { isOwnerOf } from '@backstage/plugin-catalog-react';
|
||||
import { rootDocsRouteRef } from '../../plugin';
|
||||
import { useOwnUser } from '../hooks';
|
||||
|
||||
export const OwnedContent = ({
|
||||
entities,
|
||||
}: {
|
||||
entities: Entity[] | undefined;
|
||||
}) => {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { value: user } = useOwnUser();
|
||||
|
||||
if (!entities || !user) return null;
|
||||
|
||||
const ownedDocuments = entities
|
||||
.filter((entity: Entity) => isOwnerOf(user, entity))
|
||||
.map(entity => {
|
||||
return {
|
||||
name: entity.metadata.name,
|
||||
description: entity.metadata.description,
|
||||
owner: entity?.spec?.owner,
|
||||
type: entity?.spec?.type,
|
||||
docsUrl: generatePath(rootDocsRouteRef.path, {
|
||||
namespace: entity.metadata.namespace ?? 'default',
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Document',
|
||||
field: 'name',
|
||||
highlight: true,
|
||||
render: (row: any): React.ReactNode => (
|
||||
<SubvalueCell
|
||||
value={<Link to={row.docsUrl}>{row.name}</Link>}
|
||||
subvalue={row.description}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Owner',
|
||||
field: 'owner',
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
field: 'type',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
width: '10%',
|
||||
render: (row: any) => (
|
||||
<Tooltip title="Click to copy documentation link to clipboard">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
copyToClipboard(`${window.location.origin}/${row.docsUrl}`)
|
||||
}
|
||||
>
|
||||
<ShareIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ContentHeader
|
||||
title="Owned documents"
|
||||
description="Access your documentation."
|
||||
>
|
||||
<SupportButton>Discover documentation you own.</SupportButton>
|
||||
</ContentHeader>
|
||||
{ownedDocuments && ownedDocuments.length > 0 ? (
|
||||
<Table
|
||||
options={{ paging: true, pageSize: 20, search: true }}
|
||||
data={ownedDocuments}
|
||||
columns={columns}
|
||||
title={`Owned (${ownedDocuments.length})`}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
missing="data"
|
||||
title="No documents to show"
|
||||
description="Create your own document. Check out our Getting Started Information"
|
||||
action={
|
||||
<Button
|
||||
color="primary"
|
||||
href="#"
|
||||
to="https://backstage.io/docs/features/techdocs/getting-started"
|
||||
variant="contained"
|
||||
>
|
||||
DOCS
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
+27
-37
@@ -14,31 +14,34 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Button,
|
||||
CodeSnippet,
|
||||
Content,
|
||||
Header,
|
||||
ItemCardGrid,
|
||||
ItemCardHeader,
|
||||
HeaderTabs,
|
||||
Page,
|
||||
Progress,
|
||||
useApi,
|
||||
WarningPanel,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { rootDocsRouteRef } from '../../plugin';
|
||||
|
||||
import { OverviewContent } from './OverviewContent';
|
||||
import { OwnedContent } from './OwnedContent';
|
||||
|
||||
export const TechDocsHome = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const catalogApi: CatalogApi = useApi(catalogApiRef);
|
||||
|
||||
const tabs = [{ label: 'Overview' }, { label: 'Owned Documents' }];
|
||||
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
const response = await catalogApi.getEntities();
|
||||
return response.items.filter(entity => {
|
||||
return response.items.filter((entity: Entity) => {
|
||||
return !!entity.metadata.annotations?.['backstage.io/techdocs-ref'];
|
||||
});
|
||||
});
|
||||
@@ -82,32 +85,19 @@ export const TechDocsHome = () => {
|
||||
title="Documentation"
|
||||
subtitle="Documentation available in Backstage"
|
||||
/>
|
||||
<Content>
|
||||
<ItemCardGrid data-testid="docs-explore">
|
||||
{!value?.length
|
||||
? null
|
||||
: value.map((entity, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardMedia>
|
||||
<ItemCardHeader title={entity.metadata.name} />
|
||||
</CardMedia>
|
||||
<CardContent>{entity.metadata.description}</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
to={generatePath(rootDocsRouteRef.path, {
|
||||
namespace: entity.metadata.namespace ?? 'default',
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
})}
|
||||
color="primary"
|
||||
>
|
||||
Read Docs
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
))}
|
||||
</ItemCardGrid>
|
||||
</Content>
|
||||
<HeaderTabs
|
||||
selectedIndex={selectedTab}
|
||||
onChange={index => setSelectedTab(index)}
|
||||
tabs={tabs.map(({ label }, index) => ({
|
||||
id: index.toString(),
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
{selectedTab === 0 ? (
|
||||
<OverviewContent entities={value} />
|
||||
) : (
|
||||
<OwnedContent entities={value} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { useOwnUser } from './useOwnUser';
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { UserEntity } from '@backstage/catalog-model';
|
||||
import { identityApiRef, useApi } from '@backstage/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { AsyncState } from 'react-use/lib/useAsync';
|
||||
|
||||
/**
|
||||
* Get the catalog User entity (if any) that matches the logged-in user.
|
||||
*/
|
||||
export function useOwnUser(): AsyncState<UserEntity | undefined> {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
// TODO: get the full entity (or at least the full entity name) from the
|
||||
// identityApi
|
||||
return useAsync(
|
||||
() =>
|
||||
catalogApi.getEntityByName({
|
||||
kind: 'User',
|
||||
namespace: 'default',
|
||||
name: identityApi.getUserId(),
|
||||
}) as Promise<UserEntity | undefined>,
|
||||
[catalogApi, identityApi],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user