diff --git a/public/app/features/playlist/PlaylistPage.test.tsx b/public/app/features/playlist/PlaylistPage.test.tsx
new file mode 100644
index 00000000000..3937c8580c1
--- /dev/null
+++ b/public/app/features/playlist/PlaylistPage.test.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { render, waitFor } from '@testing-library/react';
+import { PlaylistPage, PlaylistPageProps } from './PlaylistPage';
+import { locationService } from '../../../../packages/grafana-runtime/src';
+
+const fnMock = jest.fn();
+
+jest.mock('@grafana/runtime', () => ({
+ ...((jest.requireActual('@grafana/runtime') as unknown) as object),
+ getBackendSrv: () => ({
+ get: fnMock,
+ }),
+}));
+
+jest.mock('app/core/services/context_srv', () => ({
+ contextSrv: {
+ isEditor: true,
+ },
+}));
+
+function getTestContext(propOverrides?: object) {
+ const props: PlaylistPageProps = {
+ navModel: {
+ main: {
+ text: 'Playlist',
+ },
+ node: {
+ text: 'playlist',
+ },
+ },
+ route: {
+ path: '/playlists',
+ component: jest.fn(),
+ },
+ queryParams: { state: 'ok' },
+ match: { params: { name: 'playlist', sourceName: 'test playlist' }, isExact: false, url: 'asdf', path: '' },
+ history: locationService.getHistory(),
+ location: { pathname: '', hash: '', search: '', state: '' },
+ };
+
+ Object.assign(props, propOverrides);
+
+ return render();
+}
+
+describe('PlaylistPage', () => {
+ describe('when mounted without a playlist', () => {
+ it('page should load', () => {
+ fnMock.mockResolvedValue([]);
+ const { getByText } = getTestContext();
+ expect(getByText(/loading/i)).toBeInTheDocument();
+ });
+ it('then show empty list', async () => {
+ const { getByText } = getTestContext();
+ await waitFor(() => getByText('There are no playlists created yet'));
+ });
+ });
+ describe('when mounted with a playlist', () => {
+ it('page should load', () => {
+ fnMock.mockResolvedValue([
+ {
+ id: 0,
+ 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' },
+ ],
+ },
+ ]);
+ const { getByText } = getTestContext();
+ expect(getByText(/loading/i)).toBeInTheDocument();
+ });
+ it('then playlist title and buttons should appear on the page', async () => {
+ const { getByRole, getByText } = getTestContext();
+ await waitFor(() => getByText('A test playlist'));
+ expect(getByRole('button', { name: /Start playlist/i })).toBeInTheDocument();
+ expect(getByRole('link', { name: /Edit playlist/i })).toBeInTheDocument();
+ expect(getByRole('button', { name: /Delete playlist/i })).toBeInTheDocument();
+ });
+ });
+});
diff --git a/public/app/features/playlist/PlaylistPage.tsx b/public/app/features/playlist/PlaylistPage.tsx
index ccaae7c9cd8..69a11f8321f 100644
--- a/public/app/features/playlist/PlaylistPage.tsx
+++ b/public/app/features/playlist/PlaylistPage.tsx
@@ -1,32 +1,45 @@
import React, { FC, useState } from 'react';
import { connect, MapStateToProps } from 'react-redux';
-import { NavModel, SelectableValue, urlUtil } from '@grafana/data';
+import { NavModel } from '@grafana/data';
import Page from 'app/core/components/Page/Page';
import { StoreState } from 'app/types';
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { useAsync } from 'react-use';
-import { getBackendSrv, locationService } from '@grafana/runtime';
import { PlaylistDTO } from './types';
-import { Button, Card, Checkbox, Field, LinkButton, Modal, RadioButtonGroup, VerticalGroup } from '@grafana/ui';
-import { contextSrv } from 'app/core/core';
+import { Button, Card, ConfirmModal, LinkButton } from '@grafana/ui';
+import { contextSrv } from 'app/core/services/context_srv';
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
+import { deletePlaylist, getAllPlaylist } from './api';
+import { StartModal } from './StartModal';
interface ConnectedProps {
navModel: NavModel;
}
-interface Props extends ConnectedProps, GrafanaRouteComponentProps {}
+export interface PlaylistPageProps extends ConnectedProps, GrafanaRouteComponentProps {}
-export const PlaylistPage: FC = ({ navModel }) => {
+export const PlaylistPage: FC = ({ navModel }) => {
const [searchQuery, setSearchQuery] = useState('');
const [startPlaylist, setStartPlaylist] = useState();
+ const [playlistToDelete, setPlaylistToDelete] = useState();
+ const [forcePlaylistsFetch, setForcePlaylistsFetch] = useState(0);
const { value: playlists, loading } = useAsync(async () => {
- return getBackendSrv().get('/api/playlists', { query: searchQuery }) as Promise;
- });
+ return getAllPlaylist(searchQuery) as Promise;
+ }, [forcePlaylistsFetch]);
const hasPlaylists = playlists && playlists.length > 0;
+ const onDismissDelete = () => setPlaylistToDelete(undefined);
+ const onDeletePlaylist = () => {
+ if (!playlistToDelete) {
+ return;
+ }
+ deletePlaylist(playlistToDelete.id).finally(() => {
+ setForcePlaylistsFetch(forcePlaylistsFetch + 1);
+ setPlaylistToDelete(undefined);
+ });
+ };
let content = (
= ({ navModel }) => {
Start playlist
{contextSrv.isEditor && (
-
- Edit playlist
-
+ <>
+
+ Edit playlist
+
+
+ >
)}
@@ -61,7 +84,6 @@ export const PlaylistPage: FC = ({ navModel }) => {
>
);
}
-
return (
@@ -73,6 +95,16 @@ export const PlaylistPage: FC = ({ navModel }) => {
/>
)}
{content}
+ {playlistToDelete && (
+
+ )}
{startPlaylist && setStartPlaylist(undefined)} />}
@@ -84,52 +116,3 @@ const mapStateToProps: MapStateToProps = (state:
});
export default connect(mapStateToProps)(PlaylistPage);
-
-export interface StartModalProps {
- playlist: PlaylistDTO;
- onDismiss: () => void;
-}
-
-export const StartModal: FC = ({ playlist, onDismiss }) => {
- const [mode, setMode] = useState(false);
- const [autoFit, setAutofit] = useState(false);
-
- const modes: Array> = [
- { label: 'Normal', value: false },
- { label: 'TV', value: 'tv' },
- { label: 'Kiosk', value: true },
- ];
-
- const onStart = () => {
- const params: any = {};
- if (mode) {
- params.kiosk = mode;
- }
- if (autoFit) {
- params.autofitpanels = true;
- }
- locationService.push(urlUtil.renderUrl(`/playlists/play/${playlist.id}`, params));
- };
-
- return (
-
-
-
-
-
- setAutofit(e.currentTarget.checked)}
- />
-
-
-
-
-
- );
-};
diff --git a/public/app/features/playlist/StartModal.tsx b/public/app/features/playlist/StartModal.tsx
new file mode 100644
index 00000000000..371eecb8bec
--- /dev/null
+++ b/public/app/features/playlist/StartModal.tsx
@@ -0,0 +1,54 @@
+import React, { FC, useState } from 'react';
+import { SelectableValue, urlUtil } from '@grafana/data';
+import { locationService } from '@grafana/runtime';
+import { PlaylistDTO } from './types';
+import { Button, Checkbox, Field, Modal, RadioButtonGroup, VerticalGroup } from '@grafana/ui';
+
+export interface StartModalProps {
+ playlist: PlaylistDTO;
+ onDismiss: () => void;
+}
+
+export const StartModal: FC = ({ playlist, onDismiss }) => {
+ const [mode, setMode] = useState(false);
+ const [autoFit, setAutofit] = useState(false);
+
+ const modes: Array> = [
+ { label: 'Normal', value: false },
+ { label: 'TV', value: 'tv' },
+ { label: 'Kiosk', value: true },
+ ];
+
+ const onStart = () => {
+ const params: any = {};
+ if (mode) {
+ params.kiosk = mode;
+ }
+ if (autoFit) {
+ params.autofitpanels = true;
+ }
+ locationService.push(urlUtil.renderUrl(`/playlists/play/${playlist.id}`, params));
+ };
+
+ return (
+
+
+
+
+
+ setAutofit(e.currentTarget.checked)}
+ />
+
+
+
+
+
+ );
+};
diff --git a/public/app/features/playlist/api.ts b/public/app/features/playlist/api.ts
index 2c588c10b4f..d7b6f2739a0 100644
--- a/public/app/features/playlist/api.ts
+++ b/public/app/features/playlist/api.ts
@@ -1,16 +1,20 @@
import { getBackendSrv } from '@grafana/runtime';
-import { Playlist } from './types';
+import { Playlist, PlaylistDTO } from './types';
import { dispatch } from '../../store/store';
import { notifyApp } from '../../core/actions';
import { createErrorNotification, createSuccessNotification } from '../../core/copy/appNotification';
export async function createPlaylist(playlist: Playlist) {
- await withErrorHandling(async () => await getBackendSrv().post('/api/playlists', playlist));
+ await withErrorHandling(() => getBackendSrv().post('/api/playlists', playlist));
}
export async function updatePlaylist(id: number, playlist: Playlist) {
- await withErrorHandling(async () => await getBackendSrv().put(`/api/playlists/${id}`, playlist));
+ await withErrorHandling(() => getBackendSrv().put(`/api/playlists/${id}`, playlist));
+}
+
+export async function deletePlaylist(id: number) {
+ await withErrorHandling(() => getBackendSrv().delete(`/api/playlists/${id}`), 'Playlist deleted');
}
export async function getPlaylist(id: number): Promise {
@@ -18,10 +22,15 @@ export async function getPlaylist(id: number): Promise {
return result;
}
-async function withErrorHandling(apiCall: () => Promise) {
+export async function getAllPlaylist(query: string): Promise {
+ const result: PlaylistDTO[] = await getBackendSrv().get('/api/playlists/', { query });
+ return result;
+}
+
+async function withErrorHandling(apiCall: () => Promise, message = 'Playlist saved') {
try {
await apiCall();
- dispatch(notifyApp(createSuccessNotification('Playlist saved')));
+ dispatch(notifyApp(createSuccessNotification(message)));
} catch (e) {
dispatch(notifyApp(createErrorNotification('Unable to save playlist', e)));
}