feat(playlists): support custom index page
Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
---
|
||||
'@backstage/plugin-playlist': minor
|
||||
---
|
||||
|
||||
Support being able to define custom composable Playlist index pages
|
||||
|
||||
**BREAKING** The individual `PlaylistPage` route must now be manually hooked up by making the following change to your setup:
|
||||
|
||||
```diff
|
||||
-import { PlaylistIndexPage } from '@backstage/plugin-playlist';
|
||||
+import { PlaylistIndexPage, PlaylistPage } from '@backstage/plugin-playlist';
|
||||
|
||||
// ...
|
||||
|
||||
<Route path="/playlist" element={<PlaylistIndexPage />} />
|
||||
+<Route path="/playlist/:playlistId" element={<PlaylistPage />} />
|
||||
```
|
||||
@@ -109,7 +109,7 @@ import { techDocsPage } from './components/techdocs/TechDocsPage';
|
||||
import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow';
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
|
||||
import { PlaylistIndexPage } from '@backstage/plugin-playlist';
|
||||
import { PlaylistIndexPage, PlaylistPage } from '@backstage/plugin-playlist';
|
||||
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
|
||||
import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
|
||||
import { StackstormPage } from '@backstage/plugin-stackstorm';
|
||||
@@ -316,6 +316,7 @@ const routes = (
|
||||
<Route path="/azure-pull-requests" element={<AzurePullRequestsPage />} />
|
||||
<Route path="/apache-airflow" element={<ApacheAirflowPage />} />
|
||||
<Route path="/playlist" element={<PlaylistIndexPage />} />
|
||||
<Route path="/playlist/:playlistId" element={<PlaylistPage />} />
|
||||
<Route path="/score-board" element={<ScoreBoardPage />} />
|
||||
<Route path="/stackstorm" element={<StackstormPage />} />
|
||||
<Route path="/puppetdb" element={<PuppetDbPage />} />
|
||||
|
||||
@@ -23,12 +23,12 @@ yarn --cwd packages/app add @backstage/plugin-playlist
|
||||
|
||||
### Add the plugin to your `packages/app`
|
||||
|
||||
Add the root page that the playlist plugin provides to your app. You can
|
||||
choose any path for the route, but we recommend the following:
|
||||
Add the pages that the playlist plugin provides to your app. You can
|
||||
choose any base path for the route, but we recommend the following:
|
||||
|
||||
```diff
|
||||
// packages/app/src/App.tsx
|
||||
+import { PlaylistIndexPage } from '@backstage/plugin-playlist';
|
||||
+import { PlaylistIndexPage, PlaylistPage } from '@backstage/plugin-playlist';
|
||||
|
||||
|
||||
<FlatRoutes>
|
||||
@@ -37,6 +37,7 @@ choose any path for the route, but we recommend the following:
|
||||
{entityPage}
|
||||
</Route>
|
||||
+ <Route path="/playlist" element={<PlaylistIndexPage />} />
|
||||
+ <Route path="/playlist/:playlistId" element={<PlaylistPage />} />
|
||||
...
|
||||
</FlatRoutes>
|
||||
```
|
||||
@@ -140,6 +141,18 @@ playlist:
|
||||
title: Collection
|
||||
```
|
||||
|
||||
## Custom Index Page
|
||||
|
||||
You can customize your playlist index page by composing your own implementation. See the [`DefaultPlaylistIndexPage`](./src/components/PlaylistIndexPage/DefaultPlaylistIndexPage.tsx) for a reference of what components are available from the default setup.
|
||||
|
||||
```ts
|
||||
- <Route path="/playlist" element={<PlaylistIndexPage />} />
|
||||
+ <Route path="/playlist" element={<PlaylistIndexPage />}>
|
||||
+ <CustomPlaylistIndexPage />
|
||||
+ </Route>
|
||||
<Route path="/playlist/:playlistId" element={<PlaylistPage />} />
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### View All Playlists
|
||||
|
||||
@@ -13,8 +13,24 @@ import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { Playlist } from '@backstage/plugin-playlist-common';
|
||||
import { PlaylistMetadata } from '@backstage/plugin-playlist-common';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export const CreatePlaylistButton: () => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type DefaultPlaylistFilters = {
|
||||
noop?: NoopFilter;
|
||||
owners?: PlaylistOwnerFilter;
|
||||
personal?: PersonalListFilter;
|
||||
text?: PlaylistTextFilter;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const DefaultPlaylistIndexPage: () => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityPlaylistDialog: (
|
||||
props: EntityPlaylistDialogProps,
|
||||
@@ -35,6 +51,43 @@ export interface GetAllPlaylistsRequest {
|
||||
| Record<string, string | string[] | null>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class NoopFilter implements PlaylistFilter {
|
||||
// (undocumented)
|
||||
getBackendFilters(): {
|
||||
'': null;
|
||||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class PersonalListFilter implements PlaylistFilter {
|
||||
constructor(
|
||||
value: PersonalListFilterValue,
|
||||
isOwnedPlaylist: (playlist: Playlist) => boolean,
|
||||
);
|
||||
// (undocumented)
|
||||
filterPlaylist(playlist: Playlist): boolean;
|
||||
// (undocumented)
|
||||
readonly isOwnedPlaylist: (playlist: Playlist) => boolean;
|
||||
// (undocumented)
|
||||
toQueryValue(): string;
|
||||
// (undocumented)
|
||||
readonly value: PersonalListFilterValue;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const enum PersonalListFilterValue {
|
||||
// (undocumented)
|
||||
all = 'all',
|
||||
// (undocumented)
|
||||
following = 'following',
|
||||
// (undocumented)
|
||||
owned = 'owned',
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const PersonalListPicker: () => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface PlaylistApi {
|
||||
// (undocumented)
|
||||
@@ -93,9 +146,43 @@ export class PlaylistClient implements PlaylistApi {
|
||||
updatePlaylist(playlist: PlaylistMetadata): Promise<void>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type PlaylistFilter = {
|
||||
getBackendFilters?: () => Record<string, string | string[] | null>;
|
||||
filterPlaylist?: (playlist: Playlist) => boolean;
|
||||
toQueryValue?: () => string | string[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const PlaylistIndexPage: () => JSX_2.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const PlaylistList: () => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const PlaylistListProvider: <
|
||||
PlaylistFilters extends DefaultPlaylistFilters,
|
||||
>({
|
||||
children,
|
||||
}: PropsWithChildren<{}>) => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export class PlaylistOwnerFilter implements PlaylistFilter {
|
||||
constructor(values: string[]);
|
||||
// (undocumented)
|
||||
filterPlaylist(playlist: Playlist): boolean;
|
||||
// (undocumented)
|
||||
toQueryValue(): string[];
|
||||
// (undocumented)
|
||||
readonly values: string[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const PlaylistOwnerPicker: () => React_2.JSX.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export const PlaylistPage: () => JSX_2.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const playlistPlugin: BackstagePlugin<
|
||||
{
|
||||
@@ -103,4 +190,19 @@ export const playlistPlugin: BackstagePlugin<
|
||||
},
|
||||
{}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const PlaylistSearchBar: () => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const PlaylistSortPicker: () => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export class PlaylistTextFilter implements PlaylistFilter {
|
||||
constructor(value: string);
|
||||
// (undocumented)
|
||||
filterPlaylist(playlist: Playlist): boolean;
|
||||
// (undocumented)
|
||||
readonly value: string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { playlistPlugin, PlaylistIndexPage } from '../src/plugin';
|
||||
import { PlaylistIndexPage, PlaylistPage, playlistPlugin } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(playlistPlugin)
|
||||
@@ -24,4 +24,9 @@ createDevApp()
|
||||
title: 'Root Page',
|
||||
path: '/playlist',
|
||||
})
|
||||
.addPage({
|
||||
element: <PlaylistPage />,
|
||||
title: 'Root Page',
|
||||
path: '/playlist/:playlistId',
|
||||
})
|
||||
.render();
|
||||
|
||||
@@ -26,7 +26,7 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { SWRConfig } from 'swr';
|
||||
import { PlaylistApi, playlistApiRef } from '../../api';
|
||||
import { rootRouteRef } from '../../routes';
|
||||
import { playlistRouteRef, rootRouteRef } from '../../routes';
|
||||
import { CreatePlaylistButton } from './CreatePlaylistButton';
|
||||
|
||||
jest.mock('../PlaylistEditDialog', () => ({
|
||||
@@ -73,7 +73,12 @@ describe('<CreatePlaylistButton/>', () => {
|
||||
<CreatePlaylistButton />
|
||||
</TestApiProvider>
|
||||
</SWRConfig>,
|
||||
{ mountedRoutes: { '/playlists': rootRouteRef } },
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/playlists': rootRouteRef,
|
||||
'/playlists/:playlistId': playlistRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ import { playlistRouteRef } from '../../routes';
|
||||
import { PlaylistEditDialog } from '../PlaylistEditDialog';
|
||||
import { useTitle } from '../../hooks';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const CreatePlaylistButton = () => {
|
||||
const navigate = useNavigate();
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
@@ -28,7 +28,7 @@ import { fireEvent, getByRole, waitFor, act } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { SWRConfig } from 'swr';
|
||||
import { PlaylistApi, playlistApiRef } from '../../api';
|
||||
import { rootRouteRef } from '../../routes';
|
||||
import { playlistRouteRef, rootRouteRef } from '../../routes';
|
||||
import { EntityPlaylistDialog } from './EntityPlaylistDialog';
|
||||
|
||||
const navigateMock = jest.fn();
|
||||
@@ -103,7 +103,12 @@ describe('EntityPlaylistDialog', () => {
|
||||
</TestApiProvider>
|
||||
,
|
||||
</SWRConfig>,
|
||||
{ mountedRoutes: { '/playlists': rootRouteRef } },
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/playlists': rootRouteRef,
|
||||
'/playlists/:playlistId': playlistRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -41,12 +41,18 @@ import useAsync from 'react-use/lib/useAsync';
|
||||
import { usePlaylistList } from '../../hooks';
|
||||
import { PlaylistFilter } from '../../types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const enum PersonalListFilterValue {
|
||||
owned = 'owned',
|
||||
following = 'following',
|
||||
all = 'all',
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class PersonalListFilter implements PlaylistFilter {
|
||||
constructor(
|
||||
readonly value: PersonalListFilterValue,
|
||||
@@ -131,6 +137,9 @@ function getFilterGroups(orgName: string | undefined): ButtonGroup[] {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PersonalListPicker = () => {
|
||||
const classes = useStyles();
|
||||
const configApi = useApi(configApiRef);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
|
||||
import { rootRouteRef } from '../../routes';
|
||||
import { playlistRouteRef, rootRouteRef } from '../../routes';
|
||||
import { PlaylistCard } from './PlaylistCard';
|
||||
|
||||
describe('<PlaylistCard/>', () => {
|
||||
@@ -39,6 +39,7 @@ describe('<PlaylistCard/>', () => {
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/playlists': rootRouteRef,
|
||||
'/playlists/:playlistId': playlistRouteRef,
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
PermissionApi,
|
||||
permissionApiRef,
|
||||
} from '@backstage/plugin-permission-react';
|
||||
import React from 'react';
|
||||
|
||||
import { PlaylistApi, playlistApiRef } from '../../api';
|
||||
import { playlistRouteRef, rootRouteRef } from '../../routes';
|
||||
import { DefaultPlaylistIndexPage } from './DefaultPlaylistIndexPage';
|
||||
|
||||
const playlistApi: Partial<PlaylistApi> = {
|
||||
getAllPlaylists: async () => [],
|
||||
};
|
||||
|
||||
const permissionApi: Partial<PermissionApi> = {
|
||||
authorize: async () => ({ result: AuthorizeResult.ALLOW }),
|
||||
};
|
||||
|
||||
describe('DefaultPlaylistIndexPage', () => {
|
||||
it('should render', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[permissionApiRef, permissionApi],
|
||||
[playlistApiRef, playlistApi],
|
||||
]}
|
||||
>
|
||||
<DefaultPlaylistIndexPage />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/playlists': rootRouteRef,
|
||||
'/playlists/:playlistId': playlistRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(rendered.getByText('Playlists')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 {
|
||||
PageWithHeader,
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { CatalogFilterLayout } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { CreatePlaylistButton } from '../CreatePlaylistButton';
|
||||
import { PersonalListPicker } from '../PersonalListPicker';
|
||||
import { PlaylistList } from '../PlaylistList';
|
||||
import { PlaylistOwnerPicker } from '../PlaylistOwnerPicker';
|
||||
import { PlaylistSearchBar } from '../PlaylistSearchBar';
|
||||
import { PlaylistSortPicker } from '../PlaylistSortPicker';
|
||||
import { PlaylistListProvider } from '../../hooks/PlaylistListProvider';
|
||||
import { useTitle } from '../../hooks/useTitle';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const DefaultPlaylistIndexPage = () => {
|
||||
const pluralTitle = useTitle({
|
||||
pluralize: true,
|
||||
lowerCase: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<PageWithHeader themeId="home" title={pluralTitle}>
|
||||
<PlaylistListProvider>
|
||||
<Content>
|
||||
<ContentHeader title="">
|
||||
<PlaylistSortPicker />
|
||||
<CreatePlaylistButton />
|
||||
<SupportButton />
|
||||
</ContentHeader>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<PlaylistSearchBar />
|
||||
<PersonalListPicker />
|
||||
<PlaylistOwnerPicker />
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<PlaylistList />
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</Content>
|
||||
</PlaylistListProvider>
|
||||
</PageWithHeader>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,39 +14,33 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
PermissionApi,
|
||||
permissionApiRef,
|
||||
} from '@backstage/plugin-permission-react';
|
||||
import React from 'react';
|
||||
|
||||
import { PlaylistApi, playlistApiRef } from '../../api';
|
||||
import { rootRouteRef } from '../../routes';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { useOutlet } from 'react-router-dom';
|
||||
import { PlaylistIndexPage } from './PlaylistIndexPage';
|
||||
|
||||
const playlistApi: Partial<PlaylistApi> = {
|
||||
getAllPlaylists: async () => [],
|
||||
};
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useOutlet: jest.fn().mockReturnValue('Route Children'),
|
||||
}));
|
||||
|
||||
const permissionApi: Partial<PermissionApi> = {
|
||||
authorize: async () => ({ result: AuthorizeResult.ALLOW }),
|
||||
};
|
||||
jest.mock('./DefaultPlaylistIndexPage', () => ({
|
||||
DefaultPlaylistIndexPage: jest
|
||||
.fn()
|
||||
.mockReturnValue('DefaultPlaylistIndexPage'),
|
||||
}));
|
||||
|
||||
describe('PlaylistIndexPage', () => {
|
||||
it('should render', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[permissionApiRef, permissionApi],
|
||||
[playlistApiRef, playlistApi],
|
||||
]}
|
||||
>
|
||||
<PlaylistIndexPage />
|
||||
</TestApiProvider>,
|
||||
{ mountedRoutes: { '/playlists': rootRouteRef } },
|
||||
);
|
||||
expect(rendered.getByText('Playlists')).toBeInTheDocument();
|
||||
it('renders provided router element', async () => {
|
||||
const { getByText } = await renderInTestApp(<PlaylistIndexPage />);
|
||||
|
||||
expect(getByText('Route Children')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders DefaultPlaylistIndexPage when no router children are provided', async () => {
|
||||
(useOutlet as jest.Mock).mockReturnValueOnce(null);
|
||||
const { getByText } = await renderInTestApp(<PlaylistIndexPage />);
|
||||
|
||||
expect(getByText('DefaultPlaylistIndexPage')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
* 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.
|
||||
@@ -15,50 +15,11 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
PageWithHeader,
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { CatalogFilterLayout } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { CreatePlaylistButton } from '../CreatePlaylistButton';
|
||||
import { PersonalListPicker } from '../PersonalListPicker';
|
||||
import { PlaylistList } from '../PlaylistList';
|
||||
import { PlaylistOwnerPicker } from '../PlaylistOwnerPicker';
|
||||
import { PlaylistSearchBar } from '../PlaylistSearchBar';
|
||||
import { PlaylistSortPicker } from '../PlaylistSortPicker';
|
||||
import { PlaylistListProvider } from '../../hooks/PlaylistListProvider';
|
||||
import { useTitle } from '../../hooks/useTitle';
|
||||
import { useOutlet } from 'react-router-dom';
|
||||
import { DefaultPlaylistIndexPage } from './DefaultPlaylistIndexPage';
|
||||
|
||||
export const PlaylistIndexPage = () => {
|
||||
const pluralTitle = useTitle({
|
||||
pluralize: true,
|
||||
lowerCase: false,
|
||||
});
|
||||
const outlet = useOutlet();
|
||||
|
||||
return (
|
||||
<PageWithHeader themeId="home" title={pluralTitle}>
|
||||
<PlaylistListProvider>
|
||||
<Content>
|
||||
<ContentHeader title="">
|
||||
<PlaylistSortPicker />
|
||||
<CreatePlaylistButton />
|
||||
<SupportButton />
|
||||
</ContentHeader>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<PlaylistSearchBar />
|
||||
<PersonalListPicker />
|
||||
<PlaylistOwnerPicker />
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<PlaylistList />
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</Content>
|
||||
</PlaylistListProvider>
|
||||
</PageWithHeader>
|
||||
);
|
||||
return outlet || <DefaultPlaylistIndexPage />;
|
||||
};
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { DefaultPlaylistIndexPage } from './DefaultPlaylistIndexPage';
|
||||
export { PlaylistIndexPage } from './PlaylistIndexPage';
|
||||
|
||||
@@ -26,6 +26,9 @@ import { Typography } from '@material-ui/core';
|
||||
import { useTitle, usePlaylistList } from '../../hooks';
|
||||
import { PlaylistCard } from '../PlaylistCard';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PlaylistList = () => {
|
||||
const { loading, error, playlists } = usePlaylistList();
|
||||
const pluralTitleLowerCase = useTitle({
|
||||
|
||||
@@ -34,6 +34,9 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { usePlaylistList } from '../../hooks';
|
||||
import { PlaylistFilter } from '../../types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class PlaylistOwnerFilter implements PlaylistFilter {
|
||||
constructor(readonly values: string[]) {}
|
||||
|
||||
@@ -49,6 +52,9 @@ export class PlaylistOwnerFilter implements PlaylistFilter {
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PlaylistOwnerPicker = () => {
|
||||
const {
|
||||
updateFilters,
|
||||
|
||||
@@ -31,6 +31,9 @@ import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { usePlaylistList } from '../../hooks';
|
||||
import { PlaylistFilter } from '../../types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class PlaylistTextFilter implements PlaylistFilter {
|
||||
constructor(readonly value: string) {}
|
||||
|
||||
@@ -52,6 +55,9 @@ const useStyles = makeStyles(_theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PlaylistSearchBar = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PlaylistSortPicker = () => {
|
||||
const classes = useStyles();
|
||||
const { updateSort } = usePlaylistList();
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 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 { PlaylistIndexPage } from '../PlaylistIndexPage';
|
||||
import { PlaylistPage } from '../PlaylistPage';
|
||||
import { Router } from './Router';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
jest.mock('../PlaylistIndexPage', () => ({
|
||||
PlaylistIndexPage: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
jest.mock('../PlaylistPage', () => ({
|
||||
PlaylistPage: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
describe('Router', () => {
|
||||
beforeEach(() => {
|
||||
(PlaylistPage as jest.Mock).mockClear();
|
||||
(PlaylistIndexPage as jest.Mock).mockClear();
|
||||
});
|
||||
describe('/', () => {
|
||||
it('should render the PlaylistIndexPage', async () => {
|
||||
await renderInTestApp(<Router />);
|
||||
expect(PlaylistIndexPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('/:playlistId', () => {
|
||||
it('should render the PlaylistPage page', async () => {
|
||||
await renderInTestApp(<Router />, {
|
||||
routeEntries: ['/my-playlist'],
|
||||
});
|
||||
expect(PlaylistPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Routes, Route } from 'react-router-dom';
|
||||
|
||||
import { playlistRouteRef } from '../../routes';
|
||||
import { PlaylistIndexPage } from '../PlaylistIndexPage';
|
||||
import { PlaylistPage } from '../PlaylistPage';
|
||||
|
||||
export const Router = () => (
|
||||
<Routes>
|
||||
<Route path="/" element={<PlaylistIndexPage />} />
|
||||
<Route path={playlistRouteRef.path} element={<PlaylistPage />} />
|
||||
</Routes>
|
||||
);
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 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 './Router';
|
||||
@@ -58,6 +58,9 @@ type OutputState<PlaylistFilters extends DefaultPlaylistFilters> = {
|
||||
backendPlaylists: Playlist[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PlaylistListProvider = <
|
||||
PlaylistFilters extends DefaultPlaylistFilters,
|
||||
>({
|
||||
|
||||
@@ -21,12 +21,18 @@ import { PlaylistOwnerFilter } from '../components/PlaylistOwnerPicker';
|
||||
import { PlaylistTextFilter } from '../components/PlaylistSearchBar';
|
||||
import { PlaylistFilter, PlaylistSortCompareFunction } from '../types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class NoopFilter implements PlaylistFilter {
|
||||
getBackendFilters() {
|
||||
return { '': null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DefaultPlaylistFilters = {
|
||||
noop?: NoopFilter;
|
||||
owners?: PlaylistOwnerFilter;
|
||||
|
||||
@@ -19,10 +19,28 @@
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export * from './api';
|
||||
export {
|
||||
CreatePlaylistButton,
|
||||
DefaultPlaylistIndexPage,
|
||||
PersonalListFilter,
|
||||
PersonalListFilterValue,
|
||||
PersonalListPicker,
|
||||
PlaylistList,
|
||||
PlaylistOwnerFilter,
|
||||
PlaylistOwnerPicker,
|
||||
PlaylistSearchBar,
|
||||
PlaylistSortPicker,
|
||||
PlaylistTextFilter,
|
||||
} from './components';
|
||||
export type { EntityPlaylistDialogProps } from './components';
|
||||
export { NoopFilter } from './hooks';
|
||||
export type { DefaultPlaylistFilters } from './hooks';
|
||||
export { PlaylistListProvider } from './hooks/PlaylistListProvider';
|
||||
export {
|
||||
EntityPlaylistDialog,
|
||||
PlaylistPage,
|
||||
playlistPlugin,
|
||||
PlaylistIndexPage,
|
||||
} from './plugin';
|
||||
export * from './api';
|
||||
export type { PlaylistFilter } from './types';
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { playlistApiRef, PlaylistClient } from './api';
|
||||
import { rootRouteRef } from './routes';
|
||||
import { playlistRouteRef, rootRouteRef } from './routes';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -53,11 +53,24 @@ export const playlistPlugin = createPlugin({
|
||||
export const PlaylistIndexPage = playlistPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'PlaylistIndexPage',
|
||||
component: () => import('./components/Router').then(m => m.Router),
|
||||
component: () =>
|
||||
import('./components/PlaylistIndexPage').then(m => m.PlaylistIndexPage),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PlaylistPage = playlistPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'PlaylistPage',
|
||||
component: () =>
|
||||
import('./components/PlaylistPage').then(m => m.PlaylistPage),
|
||||
mountPoint: playlistRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -13,14 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api';
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'playlist:index-page',
|
||||
});
|
||||
|
||||
export const playlistRouteRef = createSubRouteRef({
|
||||
export const playlistRouteRef = createRouteRef({
|
||||
id: 'playlist:playlist-page',
|
||||
parent: rootRouteRef,
|
||||
path: '/:playlistId',
|
||||
params: ['playlistId'],
|
||||
});
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
import { Playlist } from '@backstage/plugin-playlist-common';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type PlaylistFilter = {
|
||||
getBackendFilters?: () => Record<string, string | string[] | null>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user