diff --git a/.betterer.results b/.betterer.results index 57de0404739..4243bd6bc36 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4897,18 +4897,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"] - ], - "public/app/features/playlist/PlaylistSrv.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], - "public/app/features/playlist/usePlaylistItems.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "public/app/features/plugins/__mocks__/pluginMocks.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index ff2aa956c4b..4c97cac64c9 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -218,11 +218,6 @@ export const Pages = { PlaylistForm: { name: 'Playlist name', interval: 'Playlist interval', - itemRow: 'Playlist item row', - itemIdType: 'Playlist item dashboard by ID type', - itemTagType: 'Playlist item dashboard by Tag type', - itemMoveUp: 'Move playlist item order up', - itemMoveDown: 'Move playlist item order down', itemDelete: 'Delete playlist item', }, }; diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 0e447440bff..530a8238deb 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -66,6 +66,7 @@ func (hs *HTTPServer) populateDashboardsByTag(ctx context.Context, orgID int64, return result } +// Deprecated -- the frontend can do this better func (hs *HTTPServer) LoadPlaylistDashboards(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, playlistUID string) (dtos.PlaylistDashboardsSlice, error) { playlistItems, _ := hs.LoadPlaylistItems(ctx, playlistUID, orgID) diff --git a/public/app/core/components/OptionsUI/DashboardPicker.tsx b/public/app/core/components/OptionsUI/DashboardPicker.tsx index 2baaf4d46d4..b360d68de24 100644 --- a/public/app/core/components/OptionsUI/DashboardPicker.tsx +++ b/public/app/core/components/OptionsUI/DashboardPicker.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { SelectableValue, StandardEditorProps } from '@grafana/data'; import { DashboardPicker as BasePicker, DashboardPickerDTO } from 'app/core/components/Select/DashboardPicker'; @@ -8,12 +8,10 @@ export interface DashboardPickerOptions { isClearable?: boolean; } +type Props = StandardEditorProps; + /** This will return the item UID */ -export const DashboardPicker: FC> = ({ - value, - onChange, - item, -}) => { +export const DashboardPicker = ({ value, onChange, item }: Props) => { const { placeholder, isClearable } = item?.settings ?? {}; const onPicked = useCallback( diff --git a/public/app/core/components/Select/DashboardPicker.tsx b/public/app/core/components/Select/DashboardPicker.tsx index 279dab1cb23..41e783fe337 100644 --- a/public/app/core/components/Select/DashboardPicker.tsx +++ b/public/app/core/components/Select/DashboardPicker.tsx @@ -17,7 +17,7 @@ export type DashboardPickerDTO = Pick `${folderTitle}/${dashboardTitle}`; -const getDashboards = debounce((query = ''): Promise>> => { +async function findDashboards(query = '') { return backendSrv.search({ type: 'dash-db', query, limit: 100 }).then((result: DashboardSearchItem[]) => { return result.map((item: DashboardSearchItem) => ({ value: { @@ -30,7 +30,9 @@ const getDashboards = debounce((query = ''): Promise ); diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 39439798fc9..8007cabae88 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -10,6 +10,7 @@ import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoa import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; +import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { toStateKey } from 'app/features/variables/utils'; import { DashboardDTO, DashboardInitPhase, DashboardRoutes, StoreState, ThunkDispatch, ThunkResult } from 'app/types'; @@ -70,7 +71,7 @@ async function fetchDashboard( case DashboardRoutes.Normal: { const dashDTO: DashboardDTO = await dashboardLoaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid); - if (args.fixUrl && dashDTO.meta.url) { + if (args.fixUrl && dashDTO.meta.url && !playlistSrv.isPlaying) { // check if the current url is correct (might be old slug) const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); const currentPath = locationService.getLocation().pathname; diff --git a/public/app/features/org/OrgDetailsPage.test.tsx b/public/app/features/org/OrgDetailsPage.test.tsx index eda242b5f95..e566078ba8f 100644 --- a/public/app/features/org/OrgDetailsPage.test.tsx +++ b/public/app/features/org/OrgDetailsPage.test.tsx @@ -60,6 +60,10 @@ const setup = (propOverrides?: object) => { }; describe('Render', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + it('should render component', () => { expect(() => setup()).not.toThrow(); }); diff --git a/public/app/features/playlist/PlaylistEditPage.test.tsx b/public/app/features/playlist/PlaylistEditPage.test.tsx index 3a859580188..d1894a85839 100644 --- a/public/app/features/playlist/PlaylistEditPage.test.tsx +++ b/public/app/features/playlist/PlaylistEditPage.test.tsx @@ -13,7 +13,7 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); -jest.mock('../../core/components/TagFilter/TagFilter', () => ({ +jest.mock('app/core/components/TagFilter/TagFilter', () => ({ TagFilter: () => { return <>mocked-tag-filter; }, @@ -32,7 +32,7 @@ async function getTestContext({ name, interval, items, uid }: Partial getMock.mockResolvedValue({ name: 'Test Playlist', interval: '5s', - items: [{ title: 'First item', type: 'dashboard_by_id', order: 1, value: '1' }], + items: [{ title: 'First item', type: 'dashboard_by_uid', order: 1, value: '1' }], uid: 'foo', }); const { rerender } = render( @@ -51,7 +51,7 @@ describe('PlaylistEditPage', () => { expect(screen.getByRole('heading', { name: /edit playlist/i })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: /playlist name/i })).toHaveValue('Test Playlist'); expect(screen.getByRole('textbox', { name: /playlist interval/i })).toHaveValue('5s'); - expect(screen.getAllByRole('row', { name: /playlist item row/i })).toHaveLength(1); + expect(screen.getAllByRole('row')).toHaveLength(1); }); }); @@ -69,7 +69,7 @@ describe('PlaylistEditPage', () => { expect(putMock).toHaveBeenCalledWith('/api/playlists/foo', { name: 'A Name', interval: '10s', - items: [{ title: 'First item', type: 'dashboard_by_id', order: 1, value: '1' }], + items: [{ title: 'First item', type: 'dashboard_by_uid', order: 1, value: '1' }], }); expect(locationService.getLocation().pathname).toEqual('/playlists'); }); diff --git a/public/app/features/playlist/PlaylistEditPage.tsx b/public/app/features/playlist/PlaylistEditPage.tsx index c42502d65ab..d72018ec05f 100644 --- a/public/app/features/playlist/PlaylistEditPage.tsx +++ b/public/app/features/playlist/PlaylistEditPage.tsx @@ -1,16 +1,15 @@ -import React, { FC } from 'react'; +import React from 'react'; +import { useAsync } from 'react-use'; import { locationService } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; - -import { GrafanaRouteComponentProps } from '../../core/navigation/types'; +import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { PlaylistForm } from './PlaylistForm'; -import { updatePlaylist } from './api'; +import { getPlaylist, updatePlaylist } from './api'; import { getPlaylistStyles } from './styles'; import { Playlist } from './types'; -import { usePlaylist } from './usePlaylist'; export interface RouteParams { uid: string; @@ -18,9 +17,10 @@ export interface RouteParams { interface Props extends GrafanaRouteComponentProps {} -export const PlaylistEditPage: FC = ({ match }) => { +export const PlaylistEditPage = ({ match }: Props) => { const styles = useStyles2(getPlaylistStyles); - const { playlist, loading } = usePlaylist(match.params.uid); + const playlist = useAsync(() => getPlaylist(match.params.uid), [match.params]); + const onSubmit = async (playlist: Playlist) => { await updatePlaylist(match.params.uid, playlist); locationService.push('/playlists'); @@ -28,15 +28,21 @@ export const PlaylistEditPage: FC = ({ match }) => { return ( - +

Edit playlist

-

- A playlist rotates through a pre-selected list of dashboards. A playlist can be a great way to build - situational awareness, or just show off your metrics to your team or visitors. -

+ {playlist.error &&
Error loading playlist: {JSON.stringify(playlist.error)}
} - + {playlist.value && ( + <> +

+ A playlist rotates through a pre-selected list of dashboards. A playlist can be a great way to build + situational awareness, or just show off your metrics to your team or visitors. +

+ + + + )}
); diff --git a/public/app/features/playlist/PlaylistForm.test.tsx b/public/app/features/playlist/PlaylistForm.test.tsx index 0fcec7b5d3b..b19a05f35cb 100644 --- a/public/app/features/playlist/PlaylistForm.test.tsx +++ b/public/app/features/playlist/PlaylistForm.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { PlaylistForm } from './PlaylistForm'; import { Playlist } from './types'; -jest.mock('../../core/components/TagFilter/TagFilter', () => ({ +jest.mock('app/core/components/TagFilter/TagFilter', () => ({ TagFilter: () => { return <>mocked-tag-filter; }, @@ -24,25 +24,29 @@ const playlist: Playlist = { name: 'A test playlist', interval: '10m', items: [ - { title: 'First item', type: 'dashboard_by_id', order: 1, value: '1' }, - { title: 'Middle item', type: 'dashboard_by_id', order: 2, value: '2' }, - { title: 'Last item', type: 'dashboard_by_tag', order: 2, value: 'Last item' }, + { type: 'dashboard_by_uid', value: 'uid_1' }, + { type: 'dashboard_by_uid', value: 'uid_2' }, + { type: 'dashboard_by_tag', value: 'tag_A' }, ], uid: 'foo', }; function rows() { - return screen.getAllByRole('row', { name: /playlist item row/i }); + return screen.getAllByRole('row'); } describe('PlaylistForm', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + describe('when mounted without playlist', () => { it('then it should contain name and interval fields', () => { getTestContext(); expect(screen.getByRole('textbox', { name: /playlist name/i })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: /playlist interval/i })).toBeInTheDocument(); - expect(screen.queryByRole('row', { name: /playlist item row/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('row')).not.toBeInTheDocument(); }); it('then name field should have empty string as default value', () => { @@ -74,25 +78,15 @@ describe('PlaylistForm', () => { it('then items row count should be correct', () => { getTestContext(playlist); - expect(screen.getAllByRole('row', { name: /playlist item row/i })).toHaveLength(3); + expect(screen.getAllByRole('row')).toHaveLength(3); }); it('then the first item row should be correct', () => { getTestContext(playlist); - expectCorrectRow({ index: 0, type: 'id', title: 'first item', first: true }); - }); - - it('then the middle item row should be correct', () => { - getTestContext(playlist); - - expectCorrectRow({ index: 1, type: 'id', title: 'middle item' }); - }); - - it('then the last item row should be correct', () => { - getTestContext(playlist); - - expectCorrectRow({ index: 2, type: 'tag', title: 'last item', last: true }); + expectCorrectRow({ index: 0, type: 'dashboard_by_uid', value: 'uid_1' }); + expectCorrectRow({ index: 1, type: 'dashboard_by_uid', value: 'uid_2' }); + expectCorrectRow({ index: 2, type: 'dashboard_by_tag', value: 'tag_A' }); }); }); @@ -103,30 +97,8 @@ describe('PlaylistForm', () => { expect(rows()).toHaveLength(3); await userEvent.click(within(rows()[2]).getByRole('button', { name: /delete playlist item/i })); expect(rows()).toHaveLength(2); - expectCorrectRow({ index: 0, type: 'id', title: 'first item', first: true }); - expectCorrectRow({ index: 1, type: 'id', title: 'middle item', last: true }); - }); - }); - - describe('when moving a playlist item up', () => { - it('then the item should be removed and other items should be correct', async () => { - getTestContext(playlist); - - await userEvent.click(within(rows()[2]).getByRole('button', { name: /move playlist item order up/i })); - expectCorrectRow({ index: 0, type: 'id', title: 'first item', first: true }); - expectCorrectRow({ index: 1, type: 'tag', title: 'last item' }); - expectCorrectRow({ index: 2, type: 'id', title: 'middle item', last: true }); - }); - }); - - describe('when moving a playlist item down', () => { - it('then the item should be removed and other items should be correct', async () => { - getTestContext(playlist); - - await userEvent.click(within(rows()[0]).getByRole('button', { name: /move playlist item order down/i })); - expectCorrectRow({ index: 0, type: 'id', title: 'middle item', first: true }); - expectCorrectRow({ index: 1, type: 'id', title: 'first item' }); - expectCorrectRow({ index: 2, type: 'tag', title: 'last item', last: true }); + expectCorrectRow({ index: 0, type: 'dashboard_by_uid', value: 'uid_1' }); + expectCorrectRow({ index: 1, type: 'dashboard_by_uid', value: 'uid_2' }); }); }); @@ -140,9 +112,9 @@ describe('PlaylistForm', () => { name: 'A test playlist', interval: '10m', items: [ - { title: 'First item', type: 'dashboard_by_id', order: 1, value: '1' }, - { title: 'Middle item', type: 'dashboard_by_id', order: 2, value: '2' }, - { title: 'Last item', type: 'dashboard_by_tag', order: 2, value: 'Last item' }, + { type: 'dashboard_by_uid', value: 'uid_1' }, + { type: 'dashboard_by_uid', value: 'uid_2' }, + { type: 'dashboard_by_tag', value: 'tag_A' }, ], }); }); @@ -180,28 +152,13 @@ describe('PlaylistForm', () => { interface ExpectCorrectRowArgs { index: number; - type: 'id' | 'tag'; - title: string; - first?: boolean; - last?: boolean; + type: 'dashboard_by_tag' | 'dashboard_by_uid'; + value: string; } -function expectCorrectRow({ index, type, title, first = false, last = false }: ExpectCorrectRowArgs) { +function expectCorrectRow({ index, type, value }: ExpectCorrectRowArgs) { const row = within(rows()[index]); - const cell = `playlist item dashboard by ${type} type ${title}`; + const cell = `Playlist item, ${type}, ${value}`; const regex = new RegExp(cell, 'i'); expect(row.getByRole('cell', { name: regex })).toBeInTheDocument(); - if (first) { - expect(row.queryByRole('button', { name: /move playlist item order up/i })).not.toBeInTheDocument(); - } else { - expect(row.getByRole('button', { name: /move playlist item order up/i })).toBeInTheDocument(); - } - - if (last) { - expect(row.queryByRole('button', { name: /move playlist item order down/i })).not.toBeInTheDocument(); - } else { - expect(row.getByRole('button', { name: /move playlist item order down/i })).toBeInTheDocument(); - } - - expect(row.getByRole('button', { name: /delete playlist item/i })).toBeInTheDocument(); } diff --git a/public/app/features/playlist/PlaylistForm.tsx b/public/app/features/playlist/PlaylistForm.tsx index 1d70ca5fac9..c930acf4ef1 100644 --- a/public/app/features/playlist/PlaylistForm.tsx +++ b/public/app/features/playlist/PlaylistForm.tsx @@ -1,27 +1,30 @@ -import React, { FC } from 'react'; +import React, { useMemo } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { config } from '@grafana/runtime'; import { Button, Field, Form, HorizontalGroup, Input, LinkButton } from '@grafana/ui'; -import { DashboardPickerByID } from 'app/core/components/OptionsUI/DashboardPickerByID'; +import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; +import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; -import { TagFilter } from '../../core/components/TagFilter/TagFilter'; -import { SearchSrv } from '../../core/services/search_srv'; +import { getGrafanaSearcher } from '../search/service'; import { PlaylistTable } from './PlaylistTable'; import { Playlist } from './types'; import { usePlaylistItems } from './usePlaylistItems'; -interface PlaylistFormProps { +interface Props { onSubmit: (playlist: Playlist) => void; playlist: Playlist; } -const searchSrv = new SearchSrv(); - -export const PlaylistForm: FC = ({ onSubmit, playlist }) => { +export const PlaylistForm = ({ onSubmit, playlist }: Props) => { const { name, interval, items: propItems } = playlist; - const { items, addById, addByTag, deleteItem, moveDown, moveUp } = usePlaylistItems(propItems); + const tagOptions = useMemo(() => { + return () => getGrafanaSearcher().tags({ kind: ['dashboard'] }); + }, []); + + const { items, addById, addByTag, deleteItem, moveItem } = usePlaylistItems(propItems); + return (
onSubmit({ ...list, items })} validateOn={'onBlur'}> @@ -48,13 +51,13 @@ export const PlaylistForm: FC = ({ onSubmit, playlist }) => { /> - +

Add dashboards

- + @@ -62,9 +65,9 @@ export const PlaylistForm: FC = ({ onSubmit, playlist }) => { isClearable tags={[]} hideValues - tagOptions={searchSrv.getDashboardTags} + tagOptions={tagOptions} onChange={addByTag} - placeholder={''} + placeholder="Select a tag" />
diff --git a/public/app/features/playlist/PlaylistNewPage.test.tsx b/public/app/features/playlist/PlaylistNewPage.test.tsx index 9b4285e4277..704dd9841fb 100644 --- a/public/app/features/playlist/PlaylistNewPage.test.tsx +++ b/public/app/features/playlist/PlaylistNewPage.test.tsx @@ -2,26 +2,19 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { selectors } from '@grafana/e2e-selectors'; import { locationService } from '@grafana/runtime'; - -import { backendSrv } from '../../core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { PlaylistNewPage } from './PlaylistNewPage'; import { Playlist } from './types'; -jest.mock('./usePlaylist', () => ({ - // so we don't need to add dashboard items in test - usePlaylist: jest.fn().mockReturnValue({ - playlist: { items: [{ title: 'First item', type: 'dashboard_by_id', order: 1, value: '1' }], loading: false }, - }), -})); - jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, })); -jest.mock('../../core/components/TagFilter/TagFilter', () => ({ +jest.mock('app/core/components/TagFilter/TagFilter', () => ({ TagFilter: () => { return <>mocked-tag-filter; }, @@ -38,6 +31,10 @@ function getTestContext({ name, interval, items }: Partial = {}) { } describe('PlaylistNewPage', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + describe('when mounted', () => { it('then header should be correct', () => { getTestContext(); @@ -51,13 +48,14 @@ describe('PlaylistNewPage', () => { const { backendSrvMock } = getTestContext(); expect(locationService.getLocation().pathname).toEqual('/'); - await userEvent.type(screen.getByRole('textbox', { name: /playlist name/i }), 'A Name'); + + await userEvent.type(screen.getByRole('textbox', { name: selectors.pages.PlaylistForm.name }), 'A new name'); fireEvent.submit(screen.getByRole('button', { name: /save/i })); await waitFor(() => expect(backendSrvMock).toHaveBeenCalledTimes(1)); expect(backendSrvMock).toHaveBeenCalledWith('/api/playlists', { - name: 'A Name', + name: 'A new name', interval: '5m', - items: [{ title: 'First item', type: 'dashboard_by_id', order: 1, value: '1' }], + items: [], }); expect(locationService.getLocation().pathname).toEqual('/playlists'); }); diff --git a/public/app/features/playlist/PlaylistNewPage.tsx b/public/app/features/playlist/PlaylistNewPage.tsx index 579f0d83466..5437fdbba7c 100644 --- a/public/app/features/playlist/PlaylistNewPage.tsx +++ b/public/app/features/playlist/PlaylistNewPage.tsx @@ -1,18 +1,18 @@ -import React from 'react'; +import React, { useState } from 'react'; import { locationService } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { PlaylistForm } from './PlaylistForm'; -import { createPlaylist } from './api'; +import { createPlaylist, getDefaultPlaylist } from './api'; import { getPlaylistStyles } from './styles'; import { Playlist } from './types'; -import { usePlaylist } from './usePlaylist'; export const PlaylistNewPage = () => { const styles = useStyles2(getPlaylistStyles); - const { playlist, loading } = usePlaylist(); + const [playlist] = useState(getDefaultPlaylist()); + const onSubmit = async (playlist: Playlist) => { await createPlaylist(playlist); locationService.push('/playlists'); @@ -20,7 +20,7 @@ export const PlaylistNewPage = () => { return ( - +

New Playlist

diff --git a/public/app/features/playlist/PlaylistPage.test.tsx b/public/app/features/playlist/PlaylistPage.test.tsx index 59aa7e6f60c..39b32744c00 100644 --- a/public/app/features/playlist/PlaylistPage.test.tsx +++ b/public/app/features/playlist/PlaylistPage.test.tsx @@ -25,6 +25,10 @@ function getTestContext() { } describe('PlaylistPage', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + describe('when mounted without a playlist', () => { it('page should load', () => { fnMock.mockResolvedValue([]); @@ -60,9 +64,9 @@ describe('PlaylistPage', () => { name: 'A test playlist', interval: '10m', items: [ - { title: 'First item', type: 'dashboard_by_id', order: 1, value: '1' }, - { title: 'Middle item', type: 'dashboard_by_id', order: 2, value: '2' }, - { title: 'Last item', type: 'dashboard_by_tag', order: 2, value: 'Last item' }, + { title: 'First item', type: 'dashboard_by_uid', value: '1' }, + { title: 'Middle item', type: 'dashboard_by_uid', value: '2' }, + { title: 'Last item', type: 'dashboard_by_tag', value: 'Last item' }, ], uid: 'playlist-0', }, diff --git a/public/app/features/playlist/PlaylistPage.tsx b/public/app/features/playlist/PlaylistPage.tsx index c32bdaec989..5d7273b216a 100644 --- a/public/app/features/playlist/PlaylistPage.tsx +++ b/public/app/features/playlist/PlaylistPage.tsx @@ -1,41 +1,26 @@ -import React, { useState } from 'react'; -import { useDebounce } from 'react-use'; +import React, { useMemo, useState } from 'react'; +import { useAsync } from 'react-use'; import { ConfirmModal } from '@grafana/ui'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { Page } from 'app/core/components/Page/Page'; import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; import { contextSrv } from 'app/core/services/context_srv'; -import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; - import { EmptyQueryListBanner } from './EmptyQueryListBanner'; import { PlaylistPageList } from './PlaylistPageList'; import { StartModal } from './StartModal'; -import { deletePlaylist, getAllPlaylist } from './api'; -import { PlaylistDTO } from './types'; +import { deletePlaylist, getAllPlaylist, searchPlaylists } from './api'; +import { Playlist } from './types'; export const PlaylistPage = () => { - const [searchQuery, setSearchQuery] = useState(''); - const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery); - const [hasFetched, setHasFetched] = useState(false); - const [startPlaylist, setStartPlaylist] = useState(); - const [playlistToDelete, setPlaylistToDelete] = useState(); const [forcePlaylistsFetch, setForcePlaylistsFetch] = useState(0); + const [searchQuery, setSearchQuery] = useState(''); + const allPlaylists = useAsync(() => getAllPlaylist(), [forcePlaylistsFetch]); + const playlists = useMemo(() => searchPlaylists(allPlaylists.value ?? [], searchQuery), [searchQuery, allPlaylists]); - const [playlists, setPlaylists] = useState([]); - - useDebounce( - async () => { - const playlists = await getAllPlaylist(searchQuery); - if (!hasFetched) { - setHasFetched(true); - } - setPlaylists(playlists); - setDebouncedSearchQuery(searchQuery); - }, - 350, - [forcePlaylistsFetch, searchQuery] - ); + const [startPlaylist, setStartPlaylist] = useState(); + const [playlistToDelete, setPlaylistToDelete] = useState(); const hasPlaylists = playlists && playlists.length > 0; const onDismissDelete = () => setPlaylistToDelete(undefined); @@ -63,11 +48,11 @@ export const PlaylistPage = () => { /> ); - const showSearch = playlists.length > 0 || searchQuery.length > 0 || debouncedSearchQuery.length > 0; + const showSearch = playlists.length > 0 || searchQuery.length > 0; return ( - + {showSearch && ( void; - setPlaylistToDelete: (playlistItem: PlaylistDTO) => void; - playlists: PlaylistDTO[] | undefined; + setStartPlaylist: (playlistItem: Playlist) => void; + setPlaylistToDelete: (playlistItem: Playlist) => void; + playlists: Playlist[] | undefined; } export const PlaylistPageList = ({ playlists, setStartPlaylist, setPlaylistToDelete }: Props) => { const styles = useStyles2(getStyles); return (

    - {playlists!.map((playlist: PlaylistDTO) => ( + {playlists!.map((playlist: Playlist) => (
  • @@ -52,7 +51,7 @@ export const PlaylistPageList = ({ playlists, setStartPlaylist, setPlaylistToDel