catalog-react: decouple UserListPicker from backendEntities
Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
@@ -142,7 +142,7 @@ export const columnFactories: Readonly<{
|
||||
export type DefaultEntityFilters = {
|
||||
kind?: EntityKindFilter;
|
||||
type?: EntityTypeFilter;
|
||||
user?: UserListFilter;
|
||||
user?: UserListFilter | UserOwnersFilter;
|
||||
owners?: EntityOwnerFilter;
|
||||
lifecycles?: EntityLifecycleFilter;
|
||||
tags?: EntityTagFilter;
|
||||
@@ -224,6 +224,8 @@ export class EntityLifecycleFilter implements EntityFilter {
|
||||
// (undocumented)
|
||||
filterEntity(entity: Entity): boolean;
|
||||
// (undocumented)
|
||||
getCatalogFilters(): Record<string, string | string[]>;
|
||||
// (undocumented)
|
||||
toQueryValue(): string[];
|
||||
// (undocumented)
|
||||
readonly values: string[];
|
||||
@@ -275,6 +277,8 @@ export class EntityNamespaceFilter implements EntityFilter {
|
||||
// (undocumented)
|
||||
filterEntity(entity: Entity): boolean;
|
||||
// (undocumented)
|
||||
getCatalogFilters(): Record<string, string | string[]>;
|
||||
// (undocumented)
|
||||
toQueryValue(): string[];
|
||||
// (undocumented)
|
||||
readonly values: string[];
|
||||
@@ -289,6 +293,8 @@ export class EntityOrphanFilter implements EntityFilter {
|
||||
// (undocumented)
|
||||
filterEntity(entity: Entity): boolean;
|
||||
// (undocumented)
|
||||
getCatalogFilters(): Record<string, string | string[]>;
|
||||
// (undocumented)
|
||||
readonly value: boolean;
|
||||
}
|
||||
|
||||
@@ -297,6 +303,8 @@ export class EntityOwnerFilter implements EntityFilter {
|
||||
constructor(values: string[]);
|
||||
// (undocumented)
|
||||
filterEntity(entity: Entity): boolean;
|
||||
// (undocumented)
|
||||
getCatalogFilters(): Record<string, string | string[]>;
|
||||
toQueryValue(): string[];
|
||||
// (undocumented)
|
||||
readonly values: string[];
|
||||
@@ -447,6 +455,8 @@ export class EntityTagFilter implements EntityFilter {
|
||||
// (undocumented)
|
||||
filterEntity(entity: Entity): boolean;
|
||||
// (undocumented)
|
||||
getCatalogFilters(): Record<string, string | string[]>;
|
||||
// (undocumented)
|
||||
toQueryValue(): string[];
|
||||
// (undocumented)
|
||||
readonly values: string[];
|
||||
@@ -620,7 +630,7 @@ export function useRelatedEntities(
|
||||
error: Error | undefined;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export class UserListFilter implements EntityFilter {
|
||||
constructor(
|
||||
value: UserListFilterKind,
|
||||
@@ -651,8 +661,29 @@ export const UserListPicker: (
|
||||
export type UserListPickerProps = {
|
||||
initialFilter?: UserListFilterKind;
|
||||
availableFilters?: UserListFilterKind[];
|
||||
useServerSideFilters?: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export class UserOwnersFilter implements EntityFilter {
|
||||
// (undocumented)
|
||||
static all(): UserOwnersFilter;
|
||||
// (undocumented)
|
||||
filterEntity(entity: Entity): boolean;
|
||||
// (undocumented)
|
||||
getCatalogFilters(): Record<string, string[]>;
|
||||
// (undocumented)
|
||||
static owned(ownershipEntityRefs: string[]): UserOwnersFilter;
|
||||
// (undocumented)
|
||||
readonly refs?: string[] | undefined;
|
||||
// (undocumented)
|
||||
static starred(starredEntityRefs: string[]): UserOwnersFilter;
|
||||
// (undocumented)
|
||||
toQueryValue(): string;
|
||||
// (undocumented)
|
||||
readonly value: UserListFilterKind;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export function useStarredEntities(): {
|
||||
starredEntities: Set<string>;
|
||||
|
||||
@@ -16,15 +16,18 @@
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
|
||||
import {
|
||||
Entity,
|
||||
RELATION_OWNED_BY,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { UserListPicker } from './UserListPicker';
|
||||
import { Entity, UserEntity } from '@backstage/catalog-model';
|
||||
import { UserListPicker, UserListPickerProps } from './UserListPicker';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { EntityTagFilter, UserListFilter } from '../../filters';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import {
|
||||
EntityTagFilter,
|
||||
UserListFilter,
|
||||
UserOwnersFilter,
|
||||
} from '../../filters';
|
||||
import {
|
||||
CatalogApi,
|
||||
QueryEntitiesInitialRequest,
|
||||
} from '@backstage/catalog-client';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
@@ -35,7 +38,7 @@ import {
|
||||
identityApiRef,
|
||||
storageApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { useEntityOwnership } from '../../hooks';
|
||||
import { MockStarredEntitiesApi, starredEntitiesApiRef } from '../../apis';
|
||||
|
||||
const mockUser: UserEntity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
@@ -54,19 +57,22 @@ const mockConfigApi = {
|
||||
} as Partial<ConfigApi>;
|
||||
|
||||
const mockCatalogApi = {
|
||||
getEntityByRef: () => Promise.resolve(mockUser),
|
||||
} as Partial<CatalogApi>;
|
||||
getEntityByRef: jest.fn(),
|
||||
queryEntities: jest.fn(),
|
||||
} as Partial<jest.Mocked<CatalogApi>>;
|
||||
|
||||
const mockIdentityApi = {
|
||||
getUserId: () => 'testUser',
|
||||
getIdToken: async () => undefined,
|
||||
} as Partial<IdentityApi>;
|
||||
getBackstageIdentity: jest.fn(),
|
||||
} as Partial<jest.Mocked<IdentityApi>>;
|
||||
|
||||
const mockStarredEntitiesApi = new MockStarredEntitiesApi();
|
||||
|
||||
const apis = TestApiRegistry.from(
|
||||
[configApiRef, mockConfigApi],
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[identityApiRef, mockIdentityApi],
|
||||
[storageApiRef, MockStorageApi.create()],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
);
|
||||
|
||||
const mockIsOwnedEntity = jest.fn(
|
||||
@@ -77,113 +83,117 @@ const mockIsStarredEntity = jest.fn(
|
||||
(entity: Entity) => entity.metadata.name === 'component-3',
|
||||
);
|
||||
|
||||
jest.mock('../../hooks', () => {
|
||||
const actual = jest.requireActual('../../hooks');
|
||||
return {
|
||||
...actual,
|
||||
useEntityOwnership: jest.fn(() => ({
|
||||
isOwnedEntity: mockIsOwnedEntity,
|
||||
})),
|
||||
useStarredEntities: () => ({
|
||||
isStarredEntity: mockIsStarredEntity,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const backendEntities: Entity[] = [
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'namespace-1',
|
||||
name: 'component-1',
|
||||
tags: ['tag1'],
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/testuser',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'namespace-2',
|
||||
name: 'component-2',
|
||||
tags: ['tag1'],
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'namespace-2',
|
||||
name: 'component-3',
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'namespace-2',
|
||||
name: 'component-4',
|
||||
tags: [],
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/testuser',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const ownershipEntityRefs = ['user:default/testuser'];
|
||||
describe('<UserListPicker />', () => {
|
||||
it('renders filter groups', () => {
|
||||
const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] =
|
||||
async request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['relations.ownedBy']
|
||||
) {
|
||||
// owned entities
|
||||
return { items: [], totalItems: 3, pageInfo: {} };
|
||||
}
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['metadata.name']
|
||||
) {
|
||||
// starred entities
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'e-1', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'e-2', namespace: 'default' },
|
||||
},
|
||||
],
|
||||
totalItems: 2,
|
||||
pageInfo: {},
|
||||
};
|
||||
}
|
||||
// all items
|
||||
return { items: [], totalItems: 10, pageInfo: {} };
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
mockStarredEntitiesApi.toggleStarred('component:default/e-1');
|
||||
mockStarredEntitiesApi.toggleStarred('component:default/e-2');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockCatalogApi.getEntityByRef?.mockResolvedValue(mockUser);
|
||||
mockIdentityApi.getBackstageIdentity?.mockResolvedValue({
|
||||
ownershipEntityRefs,
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/testuser',
|
||||
});
|
||||
|
||||
mockCatalogApi.queryEntities?.mockImplementation(
|
||||
mockQueryEntitiesImplementation,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('renders filter groups', async () => {
|
||||
render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider value={{ backendEntities }}>
|
||||
<MockEntityListContextProvider value={{}}>
|
||||
<UserListPicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalled(),
|
||||
);
|
||||
expect(screen.getByText('Personal')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Company')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders filters', () => {
|
||||
it('renders filters', async () => {
|
||||
render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider value={{ backendEntities }}>
|
||||
<MockEntityListContextProvider value={{}}>
|
||||
<UserListPicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getAllByRole('menuitem').map(({ textContent }) => textContent),
|
||||
).toEqual(['Owned 1', 'Starred 1', 'All 4']);
|
||||
});
|
||||
|
||||
it('includes counts alongside each filter', async () => {
|
||||
render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider value={{ backendEntities }}>
|
||||
<UserListPicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
// Material UI renders ListItemSecondaryActions outside the
|
||||
// menuitem itself, so we pick off the next sibling.
|
||||
await waitFor(() => {
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getAllByRole('menuitem').map(({ textContent }) => textContent),
|
||||
).toEqual(['Owned 1', 'Starred 1', 'All 4']);
|
||||
).toEqual(['Owned 3', 'Starred 2', 'All 10']),
|
||||
);
|
||||
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: {},
|
||||
limit: 0,
|
||||
});
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: { 'metadata.name': ['e-1', 'e-2'] },
|
||||
limit: 1000,
|
||||
});
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: { 'relations.ownedBy': ['user:default/testuser'] },
|
||||
limit: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -192,7 +202,6 @@ describe('<UserListPicker />', () => {
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
backendEntities,
|
||||
filters: { tags: new EntityTagFilter(['tag1']) },
|
||||
}}
|
||||
>
|
||||
@@ -204,35 +213,104 @@ describe('<UserListPicker />', () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByRole('menuitem').map(({ textContent }) => textContent),
|
||||
).toEqual(['Owned 1', 'Starred 0', 'All 2']);
|
||||
).toEqual(['Owned 3', 'Starred 2', 'All 10']);
|
||||
});
|
||||
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: { 'metadata.tags': ['tag1'] },
|
||||
limit: 0,
|
||||
});
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: { 'metadata.name': ['e-1', 'e-2'], 'metadata.tags': ['tag1'] },
|
||||
limit: 1000,
|
||||
});
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: {
|
||||
'relations.ownedBy': ['user:default/testuser'],
|
||||
'metadata.tags': ['tag1'],
|
||||
},
|
||||
limit: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('respects the query parameter filter value', () => {
|
||||
it('respects the query parameter filter value, legacy', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
const queryParameters = { user: 'owned' };
|
||||
render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{ backendEntities, updateFilters, queryParameters }}
|
||||
value={{ updateFilters, queryParameters }}
|
||||
>
|
||||
<UserListPicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter('owned', mockIsOwnedEntity, mockIsStarredEntity),
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'owned',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: {},
|
||||
limit: 0,
|
||||
});
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: { 'metadata.name': ['e-1', 'e-2'] },
|
||||
limit: 1000,
|
||||
});
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: {
|
||||
'relations.ownedBy': ['user:default/testuser'],
|
||||
},
|
||||
limit: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates user filter when a menuitem is selected', () => {
|
||||
it('respects the query parameter filter value', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
const queryParameters = { user: 'owned' };
|
||||
render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{ backendEntities, updateFilters }}
|
||||
value={{ updateFilters, queryParameters }}
|
||||
>
|
||||
<UserListPicker useServerSideFilters />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: UserOwnersFilter.owned(ownershipEntityRefs),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: {},
|
||||
limit: 0,
|
||||
});
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: { 'metadata.name': ['e-1', 'e-2'] },
|
||||
limit: 1000,
|
||||
});
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
|
||||
filter: {
|
||||
'relations.ownedBy': ['user:default/testuser'],
|
||||
},
|
||||
limit: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates user filter when a menuitem is selected, legacy', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider value={{ updateFilters }}>
|
||||
<UserListPicker />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
@@ -240,22 +318,45 @@ describe('<UserListPicker />', () => {
|
||||
|
||||
fireEvent.click(screen.getByText('Starred'));
|
||||
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'starred',
|
||||
mockIsOwnedEntity,
|
||||
mockIsStarredEntity,
|
||||
),
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'starred',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('responds to external queryParameters changes', () => {
|
||||
it('updates user filter when a menuitem is selected', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider value={{ updateFilters }}>
|
||||
<UserListPicker useServerSideFilters />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Starred'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: UserOwnersFilter.starred([
|
||||
'component:default/e-1',
|
||||
'component:default/e-2',
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('responds to external queryParameters changes, legacy', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
backendEntities,
|
||||
updateFilters,
|
||||
queryParameters: { user: ['all'] },
|
||||
}}
|
||||
@@ -264,14 +365,21 @@ describe('<UserListPicker />', () => {
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter('all', mockIsOwnedEntity, mockIsStarredEntity),
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'all',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
rendered.rerender(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
backendEntities,
|
||||
updateFilters,
|
||||
queryParameters: { user: ['owned'] },
|
||||
}}
|
||||
@@ -281,23 +389,59 @@ describe('<UserListPicker />', () => {
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter('owned', mockIsOwnedEntity, mockIsStarredEntity),
|
||||
user: new UserListFilter(
|
||||
'owned',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
type | filterFn
|
||||
${'owned'} | ${mockIsOwnedEntity}
|
||||
${'starred'} | ${mockIsStarredEntity}
|
||||
`('filter resetting for $type entities', ({ type, filterFn }) => {
|
||||
let updateFilters: jest.Mock;
|
||||
|
||||
const picker = (props: { loading: boolean }) => (
|
||||
it('responds to external queryParameters changes', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{ backendEntities, updateFilters, loading: props.loading }}
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { user: ['all'] },
|
||||
}}
|
||||
>
|
||||
<UserListPicker initialFilter={type} />
|
||||
<UserListPicker useServerSideFilters />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: UserOwnersFilter.all(),
|
||||
}),
|
||||
);
|
||||
|
||||
rendered.rerender(
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { user: ['owned'] },
|
||||
}}
|
||||
>
|
||||
<UserListPicker useServerSideFilters />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: UserOwnersFilter.owned(ownershipEntityRefs),
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter resetting', () => {
|
||||
let updateFilters: jest.Mock;
|
||||
|
||||
const Picker = (props: UserListPickerProps) => (
|
||||
<ApiProvider apis={apis}>
|
||||
<MockEntityListContextProvider value={{ updateFilters }}>
|
||||
<UserListPicker {...props} />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>
|
||||
);
|
||||
@@ -306,57 +450,213 @@ describe('<UserListPicker />', () => {
|
||||
updateFilters = jest.fn();
|
||||
});
|
||||
|
||||
describe(`when there are no ${type} entities match the filter`, () => {
|
||||
beforeEach(() => {
|
||||
filterFn.mockReturnValue(false);
|
||||
describe(`when there are no owned entities match the filter`, () => {
|
||||
it('does not reset the filter while entities are loading', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(
|
||||
() => new Promise(() => {}),
|
||||
);
|
||||
|
||||
render(<Picker initialFilter="owned" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalled(),
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not reset the filter while entities are loading', () => {
|
||||
render(picker({ loading: true }));
|
||||
it('does not reset the filter while owned entities are loading', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['relations.ownedBy']
|
||||
) {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
return mockQueryEntitiesImplementation(request);
|
||||
});
|
||||
|
||||
render(<Picker initialFilter="owned" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalledWith({
|
||||
user: new UserListFilter(
|
||||
'all',
|
||||
mockIsOwnedEntity,
|
||||
mockIsStarredEntity,
|
||||
),
|
||||
user: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not reset the filter while owned entities are loading', () => {
|
||||
const isOwnedEntity = jest.fn(() => false);
|
||||
(useEntityOwnership as jest.Mock).mockReturnValueOnce({
|
||||
loading: true,
|
||||
isOwnedEntity,
|
||||
it('resets the filter to "all" when entities are loaded, legacy', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(async request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['relations.ownedBy']
|
||||
) {
|
||||
return { items: [], totalItems: 0, pageInfo: {} };
|
||||
}
|
||||
return mockQueryEntitiesImplementation(request);
|
||||
});
|
||||
|
||||
render(picker({ loading: false }));
|
||||
expect(updateFilters).not.toHaveBeenCalledWith({
|
||||
user: new UserListFilter('all', isOwnedEntity, mockIsStarredEntity),
|
||||
});
|
||||
render(<Picker initialFilter="owned" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'all',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('resets the filter to "all" when entities are loaded', () => {
|
||||
render(picker({ loading: false }));
|
||||
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'all',
|
||||
mockIsOwnedEntity,
|
||||
mockIsStarredEntity,
|
||||
),
|
||||
it('resets the filter to "all" when entities are loaded', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(async request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['relations.ownedBy']
|
||||
) {
|
||||
return { items: [], totalItems: 0, pageInfo: {} };
|
||||
}
|
||||
return mockQueryEntitiesImplementation(request);
|
||||
});
|
||||
|
||||
render(<Picker initialFilter="owned" useServerSideFilters />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: UserOwnersFilter.all(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there are some ${type} entities present`, () => {
|
||||
beforeEach(() => {
|
||||
filterFn.mockReturnValue(true);
|
||||
describe(`when there are no starred entities match the filter`, () => {
|
||||
it('does not reset the filter while entities are loading', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(
|
||||
() => new Promise(() => {}),
|
||||
);
|
||||
|
||||
render(<Picker initialFilter="starred" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalled(),
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not reset the filter while entities are loading', () => {
|
||||
render(picker({ loading: true }));
|
||||
it('does not reset the filter while starred entities are loading', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['metadata.name']
|
||||
) {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
return mockQueryEntitiesImplementation(request);
|
||||
});
|
||||
|
||||
render(<Picker initialFilter="starred" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalledWith({
|
||||
user: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('resets the filter to "all" when entities are loaded, legacy', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(async request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['metadata.name']
|
||||
) {
|
||||
return { items: [], totalItems: 0, pageInfo: {} };
|
||||
}
|
||||
return mockQueryEntitiesImplementation(request);
|
||||
});
|
||||
|
||||
render(<Picker initialFilter="starred" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'all',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('resets the filter to "all" when entities are loaded', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(async request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['metadata.name']
|
||||
) {
|
||||
return { items: [], totalItems: 0, pageInfo: {} };
|
||||
}
|
||||
return mockQueryEntitiesImplementation(request);
|
||||
});
|
||||
|
||||
render(<Picker initialFilter="starred" useServerSideFilters />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: UserOwnersFilter.all(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there are some owned entities present`, () => {
|
||||
it('does not reset the filter while entities are loading', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['relations.ownedBy']
|
||||
) {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
return mockQueryEntitiesImplementation(request);
|
||||
});
|
||||
|
||||
render(
|
||||
<Picker initialFilter="owned" />,
|
||||
); /* picker({ loading: true })*/
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalledWith({
|
||||
user: new UserListFilter(
|
||||
'all',
|
||||
@@ -366,17 +666,74 @@ describe('<UserListPicker />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not reset the filter when entities are loaded', () => {
|
||||
render(picker({ loading: false }));
|
||||
it('does not reset the filter when entities are loaded', async () => {
|
||||
render(<Picker initialFilter="owned" />);
|
||||
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'owned',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there are some starred entities present`, () => {
|
||||
it('does not reset the filter while entities are loading', async () => {
|
||||
mockCatalogApi.queryEntities?.mockImplementation(request => {
|
||||
if (
|
||||
(
|
||||
(request as QueryEntitiesInitialRequest).filter as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
)['metadata.name']
|
||||
) {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
return mockQueryEntitiesImplementation(request);
|
||||
});
|
||||
|
||||
render(
|
||||
<Picker initialFilter="starred" />,
|
||||
); /* picker({ loading: true })*/
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalledWith({
|
||||
user: new UserListFilter(
|
||||
type,
|
||||
'all',
|
||||
mockIsOwnedEntity,
|
||||
mockIsStarredEntity,
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not reset the filter when entities are loaded', async () => {
|
||||
render(<Picker initialFilter="starred" />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
user: new UserListFilter(
|
||||
'starred',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,16 +32,14 @@ import {
|
||||
} from '@material-ui/core';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import { compact } from 'lodash';
|
||||
import React, { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { UserListFilter } from '../../filters';
|
||||
import {
|
||||
useEntityList,
|
||||
useStarredEntities,
|
||||
useEntityOwnership,
|
||||
} from '../../hooks';
|
||||
import { UserListFilter, UserOwnersFilter } from '../../filters';
|
||||
import { useEntityList, useStarredEntities } from '../../hooks';
|
||||
import { UserListFilterKind } from '../../types';
|
||||
import { reduceEntityFilters } from '../../utils';
|
||||
import { useOwnedEntitiesCount } from './useOwnedEntitiesCount';
|
||||
import { useAllEntitiesCount } from './useAllEntitiesCount';
|
||||
import { useStarredEntitiesCount } from './useStarredEntitiesCount';
|
||||
import { useIsOwnedEntity } from '../../hooks/useEntityOwnership';
|
||||
|
||||
/** @public */
|
||||
export type CatalogReactUserListPickerClassKey =
|
||||
@@ -122,20 +120,19 @@ function getFilterGroups(orgName: string | undefined): ButtonGroup[] {
|
||||
export type UserListPickerProps = {
|
||||
initialFilter?: UserListFilterKind;
|
||||
availableFilters?: UserListFilterKind[];
|
||||
useServerSideFilters?: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const UserListPicker = (props: UserListPickerProps) => {
|
||||
const { initialFilter, availableFilters } = props;
|
||||
const { initialFilter, availableFilters, useServerSideFilters } = props;
|
||||
const classes = useStyles();
|
||||
const configApi = useApi(configApiRef);
|
||||
const orgName = configApi.getOptionalString('organization.name') ?? 'Company';
|
||||
const {
|
||||
filters,
|
||||
updateFilters,
|
||||
backendEntities,
|
||||
queryParameters: { kind: kindParameter, user: userParameter },
|
||||
loading: loadingBackendEntities,
|
||||
} = useEntityList();
|
||||
|
||||
// Remove group items that aren't in availableFilters and exclude
|
||||
@@ -153,21 +150,18 @@ export const UserListPicker = (props: UserListPickerProps) => {
|
||||
}))
|
||||
.filter(({ items }) => !!items.length);
|
||||
|
||||
const { isStarredEntity } = useStarredEntities();
|
||||
const { isOwnedEntity, loading: loadingEntityOwnership } =
|
||||
useEntityOwnership();
|
||||
|
||||
const loading = loadingBackendEntities || loadingEntityOwnership;
|
||||
|
||||
// Static filters; used for generating counts of potentially unselected kinds
|
||||
const ownedFilter = useMemo(
|
||||
() => new UserListFilter('owned', isOwnedEntity, isStarredEntity),
|
||||
[isOwnedEntity, isStarredEntity],
|
||||
);
|
||||
const starredFilter = useMemo(
|
||||
() => new UserListFilter('starred', isOwnedEntity, isStarredEntity),
|
||||
[isOwnedEntity, isStarredEntity],
|
||||
);
|
||||
const {
|
||||
count: ownedEntitiesCount,
|
||||
loading: loadingOwnedEntities,
|
||||
filter: ownedEntitiesFilter,
|
||||
ownershipEntityRefs,
|
||||
} = useOwnedEntitiesCount();
|
||||
const { count: allCount } = useAllEntitiesCount();
|
||||
const {
|
||||
count: starredEntitiesCount,
|
||||
filter: starredEntitiesFilter,
|
||||
loading: loadingStarredEntities,
|
||||
} = useStarredEntitiesCount();
|
||||
|
||||
const queryParamUserFilter = useMemo(
|
||||
() => [userParameter].flat()[0],
|
||||
@@ -175,33 +169,19 @@ export const UserListPicker = (props: UserListPickerProps) => {
|
||||
);
|
||||
|
||||
const [selectedUserFilter, setSelectedUserFilter] = useState(
|
||||
queryParamUserFilter ?? initialFilter,
|
||||
(queryParamUserFilter as UserListFilterKind) ?? initialFilter,
|
||||
);
|
||||
|
||||
// To show proper counts for each section, apply all other frontend filters _except_ the user
|
||||
// filter that's controlled by this picker.
|
||||
const entitiesWithoutUserFilter = useMemo(
|
||||
() =>
|
||||
backendEntities.filter(
|
||||
reduceEntityFilters(
|
||||
compact(Object.values({ ...filters, user: undefined })),
|
||||
),
|
||||
),
|
||||
[filters, backendEntities],
|
||||
);
|
||||
const filterCounts = useMemo(() => {
|
||||
return {
|
||||
all: allCount,
|
||||
starred: starredEntitiesCount,
|
||||
owned: ownedEntitiesCount,
|
||||
};
|
||||
}, [starredEntitiesCount, ownedEntitiesCount, allCount]);
|
||||
|
||||
const filterCounts = useMemo<Record<string, number>>(
|
||||
() => ({
|
||||
all: entitiesWithoutUserFilter.length,
|
||||
starred: entitiesWithoutUserFilter.filter(entity =>
|
||||
starredFilter.filterEntity(entity),
|
||||
).length,
|
||||
owned: entitiesWithoutUserFilter.filter(entity =>
|
||||
ownedFilter.filterEntity(entity),
|
||||
).length,
|
||||
}),
|
||||
[entitiesWithoutUserFilter, starredFilter, ownedFilter],
|
||||
);
|
||||
const { isStarredEntity } = useStarredEntities();
|
||||
const isOwnedEntity = useIsOwnedEntity(ownershipEntityRefs);
|
||||
|
||||
// Set selected user filter on query parameter updates; this happens at initial page load and from
|
||||
// external updates to the page location.
|
||||
@@ -211,6 +191,8 @@ export const UserListPicker = (props: UserListPickerProps) => {
|
||||
}
|
||||
}, [queryParamUserFilter]);
|
||||
|
||||
const loading = loadingOwnedEntities || loadingStarredEntities;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!loading &&
|
||||
@@ -223,16 +205,46 @@ export const UserListPicker = (props: UserListPickerProps) => {
|
||||
}, [loading, filterCounts, selectedUserFilter, setSelectedUserFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilters({
|
||||
user: selectedUserFilter
|
||||
? new UserListFilter(
|
||||
selectedUserFilter as UserListFilterKind,
|
||||
isOwnedEntity,
|
||||
isStarredEntity,
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
}, [selectedUserFilter, isOwnedEntity, isStarredEntity, updateFilters]);
|
||||
if (!selectedUserFilter) {
|
||||
return;
|
||||
}
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
if (useServerSideFilters) {
|
||||
const getFilter = () => {
|
||||
if (selectedUserFilter === 'owned') {
|
||||
return ownedEntitiesFilter;
|
||||
}
|
||||
if (selectedUserFilter === 'starred') {
|
||||
return starredEntitiesFilter;
|
||||
}
|
||||
return UserOwnersFilter.all();
|
||||
};
|
||||
|
||||
updateFilters({ user: getFilter() });
|
||||
} else {
|
||||
// legacy
|
||||
updateFilters({
|
||||
user: selectedUserFilter
|
||||
? new UserListFilter(
|
||||
selectedUserFilter as UserListFilterKind,
|
||||
isOwnedEntity,
|
||||
isStarredEntity,
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
selectedUserFilter,
|
||||
starredEntitiesFilter,
|
||||
ownedEntitiesFilter,
|
||||
updateFilters,
|
||||
useServerSideFilters,
|
||||
isOwnedEntity,
|
||||
isStarredEntity,
|
||||
loading,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { QueryEntitiesInitialRequest } from '@backstage/catalog-client';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { compact, isEqual } from 'lodash';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { useEntityList } from '../../hooks';
|
||||
import { reduceCatalogFilters } from '../../utils';
|
||||
|
||||
/**
|
||||
* TODO(vinzscam): we need to find a better way
|
||||
* for retrieving this value. One possible way, could be to use
|
||||
* the /entities endpoint: since this method is paginated,
|
||||
* it should also return how many items matching the provided filters
|
||||
* are in the catalog
|
||||
*/
|
||||
export function useAllEntitiesCount() {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { filters } = useEntityList();
|
||||
|
||||
const refRequest = useRef<QueryEntitiesInitialRequest>();
|
||||
useMemo(() => {
|
||||
const { user, ...allFilters } = filters;
|
||||
const compacted = compact(Object.values(allFilters));
|
||||
const filter = reduceCatalogFilters(compacted);
|
||||
const request: QueryEntitiesInitialRequest = {
|
||||
filter,
|
||||
limit: 0,
|
||||
};
|
||||
|
||||
if (isEqual(request, refRequest.current)) {
|
||||
return refRequest.current;
|
||||
}
|
||||
refRequest.current = request;
|
||||
|
||||
return request;
|
||||
}, [filters]);
|
||||
|
||||
const { value: count, loading } = useAsync(async () => {
|
||||
const { totalItems } = await catalogApi.queryEntities(refRequest.current);
|
||||
|
||||
return totalItems;
|
||||
}, [refRequest.current]);
|
||||
|
||||
return { count, loading };
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 { QueryEntitiesInitialRequest } from '@backstage/catalog-client';
|
||||
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { compact, intersection, isEqual } from 'lodash';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { UserOwnersFilter } from '../../filters';
|
||||
import { useEntityList } from '../../hooks';
|
||||
import { reduceCatalogFilters } from '../../utils';
|
||||
|
||||
export function useOwnedEntitiesCount() {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const { filters } = useEntityList();
|
||||
// Trigger load only on mount
|
||||
const { value: ownershipEntityRefs, loading: loadingEntityRefs } = useAsync(
|
||||
async () => (await identityApi.getBackstageIdentity()).ownershipEntityRefs,
|
||||
|
||||
[],
|
||||
);
|
||||
|
||||
const refRequest = useRef<QueryEntitiesInitialRequest>();
|
||||
|
||||
useMemo(async () => {
|
||||
const compacted = compact(Object.values(filters));
|
||||
const allFilter = reduceCatalogFilters(compacted);
|
||||
const { ['metadata.name']: metadata, ...filter } = allFilter;
|
||||
|
||||
const facet = 'relations.ownedBy';
|
||||
|
||||
const ownedByFilter = Array.isArray(filter[facet])
|
||||
? (filter[facet] as string[])
|
||||
: [];
|
||||
|
||||
const commonOwnedBy = intersection(ownedByFilter, ownershipEntityRefs);
|
||||
|
||||
const ownedBy =
|
||||
ownedByFilter.length > 0 ? ownedByFilter : ownershipEntityRefs;
|
||||
if (ownedByFilter.length > 0 && commonOwnedBy.length === 0) {
|
||||
// don't send any request if another filter sets
|
||||
// totally different values for relations.ownedBy filter.
|
||||
// TODO(vinzscam): check conflicts between UserOwnersFilter and EntityOwnerFilter.
|
||||
// both set filters on the same relations.ownedBy key, so the conflicts need
|
||||
// to be addressed properly.
|
||||
refRequest.current = undefined;
|
||||
return null;
|
||||
}
|
||||
const request: QueryEntitiesInitialRequest = {
|
||||
filter: {
|
||||
...filter,
|
||||
'relations.ownedBy': ownedBy ?? [],
|
||||
},
|
||||
limit: 0,
|
||||
};
|
||||
|
||||
if (isEqual(request, refRequest.current)) {
|
||||
return refRequest.current;
|
||||
}
|
||||
|
||||
refRequest.current = request;
|
||||
|
||||
return request;
|
||||
}, [filters, ownershipEntityRefs]);
|
||||
|
||||
const { value: count, loading: loadingEntityOwnership } =
|
||||
useAsync(async () => {
|
||||
if (!ownershipEntityRefs?.length) {
|
||||
return 0;
|
||||
}
|
||||
if (!refRequest.current) {
|
||||
return 0;
|
||||
}
|
||||
const { totalItems } = await catalogApi.queryEntities(refRequest.current);
|
||||
|
||||
return totalItems;
|
||||
}, [refRequest.current]);
|
||||
|
||||
const loading = loadingEntityRefs || loadingEntityOwnership;
|
||||
const filter = useMemo(
|
||||
() => UserOwnersFilter.owned(ownershipEntityRefs ?? []),
|
||||
[ownershipEntityRefs],
|
||||
);
|
||||
|
||||
return {
|
||||
count,
|
||||
loading,
|
||||
filter,
|
||||
ownershipEntityRefs,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 { QueryEntitiesInitialRequest } from '@backstage/catalog-client';
|
||||
import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { compact, isEqual } from 'lodash';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { UserOwnersFilter } from '../../filters';
|
||||
import { useEntityList, useStarredEntities } from '../../hooks';
|
||||
import { reduceCatalogFilters } from '../../utils';
|
||||
|
||||
export function useStarredEntitiesCount() {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { filters } = useEntityList();
|
||||
const { starredEntities } = useStarredEntities();
|
||||
|
||||
const refRequest = useRef<QueryEntitiesInitialRequest>();
|
||||
useMemo(async () => {
|
||||
const { user, ...allFilters } = filters;
|
||||
const compacted = compact(Object.values(allFilters));
|
||||
const filter = reduceCatalogFilters(compacted);
|
||||
|
||||
const facet = 'metadata.name';
|
||||
|
||||
const request: QueryEntitiesInitialRequest = {
|
||||
filter: {
|
||||
...filter,
|
||||
[facet]: Array.from(starredEntities).map(e => parseEntityRef(e).name),
|
||||
},
|
||||
limit: 1000,
|
||||
};
|
||||
if (isEqual(request, refRequest.current)) {
|
||||
return refRequest.current;
|
||||
}
|
||||
refRequest.current = request;
|
||||
|
||||
return request;
|
||||
}, [filters, starredEntities]);
|
||||
|
||||
const { value: count, loading } = useAsync(async () => {
|
||||
if (!starredEntities.size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const response = await catalogApi.queryEntities(refRequest.current);
|
||||
|
||||
return response.items
|
||||
.map(e =>
|
||||
stringifyEntityRef({
|
||||
kind: e.kind,
|
||||
namespace: e.metadata.namespace,
|
||||
name: e.metadata.name,
|
||||
}),
|
||||
)
|
||||
.filter(e => starredEntities.has(e)).length;
|
||||
}, [refRequest.current, starredEntities]);
|
||||
|
||||
const filter = useMemo(
|
||||
() => UserOwnersFilter.starred(Array.from(starredEntities)),
|
||||
[starredEntities],
|
||||
);
|
||||
|
||||
return { count, loading, filter };
|
||||
}
|
||||
Reference in New Issue
Block a user