mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Playlist: Add an api wrapper for playlist requests (#76308)
This commit is contained in:
parent
bf7fae4bd3
commit
6983af3a70
@ -76,6 +76,7 @@ describe('PlaylistEditPage', () => {
|
||||
fireEvent.submit(screen.getByRole('button', { name: /save/i }));
|
||||
await waitFor(() => expect(putMock).toHaveBeenCalledTimes(1));
|
||||
expect(putMock).toHaveBeenCalledWith('/api/playlists/foo', {
|
||||
uid: 'foo',
|
||||
name: 'A Name',
|
||||
interval: '10s',
|
||||
items: [{ title: 'First item', type: 'dashboard_by_uid', order: 1, value: '1' }],
|
||||
|
@ -8,11 +8,9 @@ import { t, Trans } from 'app/core/internationalization';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
import { PlaylistForm } from './PlaylistForm';
|
||||
import { playlistAPI, updatePlaylist } from './api';
|
||||
import { getPlaylistAPI } from './api';
|
||||
import { Playlist } from './types';
|
||||
|
||||
const { getPlaylist } = playlistAPI;
|
||||
|
||||
export interface RouteParams {
|
||||
uid: string;
|
||||
}
|
||||
@ -20,10 +18,11 @@ export interface RouteParams {
|
||||
interface Props extends GrafanaRouteComponentProps<RouteParams> {}
|
||||
|
||||
export const PlaylistEditPage = ({ match }: Props) => {
|
||||
const playlist = useAsync(() => getPlaylist(match.params.uid), [match.params]);
|
||||
const api = getPlaylistAPI();
|
||||
const playlist = useAsync(() => api.getPlaylist(match.params.uid), [match.params]);
|
||||
|
||||
const onSubmit = async (playlist: Playlist) => {
|
||||
await updatePlaylist(match.params.uid, playlist);
|
||||
await api.updatePlaylist(playlist);
|
||||
locationService.push('/playlists');
|
||||
};
|
||||
|
||||
|
@ -108,6 +108,7 @@ describe('PlaylistForm', () => {
|
||||
await userEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
expect(onSubmitMock).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmitMock).toHaveBeenCalledWith({
|
||||
uid: 'foo',
|
||||
name: 'A test playlist',
|
||||
interval: '10m',
|
||||
items: [
|
||||
|
@ -29,7 +29,7 @@ export const PlaylistForm = ({ onSubmit, playlist }: Props) => {
|
||||
|
||||
const doSubmit = (list: Playlist) => {
|
||||
setSaving(true);
|
||||
onSubmit({ ...list, items });
|
||||
onSubmit({ ...list, items, uid: playlist.uid });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -60,6 +60,7 @@ describe('PlaylistNewPage', () => {
|
||||
await waitFor(() => expect(backendSrvMock).toHaveBeenCalledTimes(1));
|
||||
expect(backendSrvMock).toHaveBeenCalledWith('/api/playlists', {
|
||||
name: 'A new name',
|
||||
uid: '',
|
||||
interval: '5m',
|
||||
items: [],
|
||||
});
|
||||
|
@ -5,14 +5,14 @@ import { locationService } from '@grafana/runtime';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { PlaylistForm } from './PlaylistForm';
|
||||
import { createPlaylist, getDefaultPlaylist } from './api';
|
||||
import { getPlaylistAPI, getDefaultPlaylist } from './api';
|
||||
import { Playlist } from './types';
|
||||
|
||||
export const PlaylistNewPage = () => {
|
||||
const [playlist] = useState<Playlist>(getDefaultPlaylist());
|
||||
|
||||
const onSubmit = async (playlist: Playlist) => {
|
||||
await createPlaylist(playlist);
|
||||
await getPlaylistAPI().createPlaylist(playlist);
|
||||
locationService.push('/playlists');
|
||||
};
|
||||
|
||||
|
@ -11,15 +11,14 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { EmptyQueryListBanner } from './EmptyQueryListBanner';
|
||||
import { PlaylistPageList } from './PlaylistPageList';
|
||||
import { StartModal } from './StartModal';
|
||||
import { deletePlaylist, searchPlaylists, playlistAPI } from './api';
|
||||
import { getPlaylistAPI, searchPlaylists } from './api';
|
||||
import { Playlist } from './types';
|
||||
|
||||
const { getAllPlaylist } = playlistAPI;
|
||||
|
||||
export const PlaylistPage = () => {
|
||||
const api = getPlaylistAPI();
|
||||
const [forcePlaylistsFetch, setForcePlaylistsFetch] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const allPlaylists = useAsync(() => getAllPlaylist(), [forcePlaylistsFetch]);
|
||||
const allPlaylists = useAsync(() => api.getAllPlaylist(), [forcePlaylistsFetch]);
|
||||
const playlists = useMemo(() => searchPlaylists(allPlaylists.value ?? [], searchQuery), [searchQuery, allPlaylists]);
|
||||
|
||||
const [startPlaylist, setStartPlaylist] = useState<Playlist | undefined>();
|
||||
@ -31,7 +30,7 @@ export const PlaylistPage = () => {
|
||||
if (!playlistToDelete) {
|
||||
return;
|
||||
}
|
||||
deletePlaylist(playlistToDelete.uid).finally(() => {
|
||||
api.deletePlaylist(playlistToDelete.uid).finally(() => {
|
||||
setForcePlaylistsFetch(forcePlaylistsFetch + 1);
|
||||
setPlaylistToDelete(undefined);
|
||||
});
|
||||
|
@ -11,16 +11,17 @@ import { PlaylistSrv } from './PlaylistSrv';
|
||||
import { Playlist, PlaylistItem } from './types';
|
||||
|
||||
jest.mock('./api', () => ({
|
||||
playlistAPI: {
|
||||
getPlaylistAPI: () => ({
|
||||
getPlaylist: jest.fn().mockReturnValue({
|
||||
interval: '1s',
|
||||
uid: 'xyz',
|
||||
name: 'The display',
|
||||
items: [
|
||||
{ type: 'dashboard_by_uid', value: 'aaa' },
|
||||
{ type: 'dashboard_by_uid', value: 'bbb' },
|
||||
],
|
||||
} as Playlist),
|
||||
},
|
||||
}),
|
||||
loadDashboards: (items: PlaylistItem[]) => {
|
||||
return Promise.resolve(
|
||||
items.map((v) => ({
|
||||
|
@ -4,9 +4,8 @@ import { pickBy } from 'lodash';
|
||||
import { locationUtil, urlUtil, rangeUtil } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
|
||||
import { playlistAPI, loadDashboards } from './api';
|
||||
|
||||
const { getPlaylist } = playlistAPI;
|
||||
import { getPlaylistAPI, loadDashboards } from './api';
|
||||
import { PlaylistAPI } from './types';
|
||||
|
||||
export const queryParamsToPreserve: { [key: string]: boolean } = {
|
||||
kiosk: true,
|
||||
@ -23,11 +22,13 @@ export class PlaylistSrv {
|
||||
private numberOfLoops = 0;
|
||||
private declare validPlaylistUrl: string;
|
||||
private locationListenerUnsub?: () => void;
|
||||
private api: PlaylistAPI;
|
||||
|
||||
isPlaying = false;
|
||||
|
||||
constructor() {
|
||||
this.locationUpdated = this.locationUpdated.bind(this);
|
||||
this.api = getPlaylistAPI();
|
||||
}
|
||||
|
||||
next() {
|
||||
@ -81,7 +82,7 @@ export class PlaylistSrv {
|
||||
this.locationListenerUnsub = locationService.getHistory().listen(this.locationUpdated);
|
||||
|
||||
const urls: string[] = [];
|
||||
let playlist = await getPlaylist(playlistUid);
|
||||
let playlist = await this.api.getPlaylist(playlistUid);
|
||||
if (!playlist.items?.length) {
|
||||
// alert
|
||||
return;
|
||||
|
@ -11,71 +11,132 @@ import { dispatch } from 'app/store/store';
|
||||
|
||||
import { DashboardQueryResult, getGrafanaSearcher, SearchQuery } from '../search/service';
|
||||
|
||||
import { Playlist, PlaylistItem, KubernetesPlaylist, KubernetesPlaylistList, PlaylistAPI } from './types';
|
||||
import { Playlist, PlaylistItem, PlaylistAPI } from './types';
|
||||
|
||||
export async function createPlaylist(playlist: Playlist) {
|
||||
class LegacyAPI implements PlaylistAPI {
|
||||
async getAllPlaylist(): Promise<Playlist[]> {
|
||||
return getBackendSrv().get<Playlist[]>('/api/playlists/');
|
||||
}
|
||||
|
||||
async getPlaylist(uid: string): Promise<Playlist> {
|
||||
const p = await getBackendSrv().get<Playlist>(`/api/playlists/${uid}`);
|
||||
await migrateInternalIDs(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
async createPlaylist(playlist: Playlist): Promise<void> {
|
||||
await withErrorHandling(() => getBackendSrv().post('/api/playlists', playlist));
|
||||
}
|
||||
|
||||
export async function updatePlaylist(uid: string, playlist: Playlist) {
|
||||
await withErrorHandling(() => getBackendSrv().put(`/api/playlists/${uid}`, playlist));
|
||||
async updatePlaylist(playlist: Playlist): Promise<void> {
|
||||
await withErrorHandling(() => getBackendSrv().put(`/api/playlists/${playlist.uid}`, playlist));
|
||||
}
|
||||
|
||||
export async function deletePlaylist(uid: string) {
|
||||
async deletePlaylist(uid: string): Promise<void> {
|
||||
await withErrorHandling(() => getBackendSrv().delete(`/api/playlists/${uid}`), 'Playlist deleted');
|
||||
}
|
||||
}
|
||||
|
||||
export const playlistAPI: PlaylistAPI = {
|
||||
getPlaylist: config.featureToggles.kubernetesPlaylists ? k8sGetPlaylist : legacyGetPlaylist,
|
||||
getAllPlaylist: config.featureToggles.kubernetesPlaylists ? k8sGetAllPlaylist : legacyGetAllPlaylist,
|
||||
interface K8sPlaylistList {
|
||||
playlists: K8sPlaylist[];
|
||||
}
|
||||
|
||||
interface K8sPlaylist {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: {
|
||||
name: string;
|
||||
interval: string;
|
||||
items: PlaylistItem[];
|
||||
};
|
||||
}
|
||||
|
||||
/** This returns a playlist where all ids are replaced with UIDs */
|
||||
export async function k8sGetPlaylist(uid: string): Promise<Playlist> {
|
||||
const k8splaylist = await getBackendSrv().get<KubernetesPlaylist>(
|
||||
`/apis/playlist.x.grafana.com/v0alpha1/namespaces/org-${contextSrv.user.orgId}/playlists/${uid}`
|
||||
class K8sAPI implements PlaylistAPI {
|
||||
readonly url = `/apis/playlist.x.grafana.com/v0alpha1/namespaces/org-${contextSrv.user.orgId}/playlists`;
|
||||
readonly legacy = new LegacyAPI(); // set to null for full CRUD
|
||||
|
||||
async getAllPlaylist(): Promise<Playlist[]> {
|
||||
const result = await getBackendSrv().get<K8sPlaylistList>(this.url);
|
||||
console.log('getAllPlaylist', result);
|
||||
const v = result.playlists.map(k8sResourceAsPlaylist);
|
||||
console.log('after', v);
|
||||
return v;
|
||||
}
|
||||
|
||||
async getPlaylist(uid: string): Promise<Playlist> {
|
||||
const r = await getBackendSrv().get<K8sPlaylist>(this.url + '/' + uid);
|
||||
const p = k8sResourceAsPlaylist(r);
|
||||
await migrateInternalIDs(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
async createPlaylist(playlist: Playlist): Promise<void> {
|
||||
if (this.legacy) {
|
||||
return this.legacy.createPlaylist(playlist);
|
||||
}
|
||||
await withErrorHandling(() =>
|
||||
getBackendSrv().post(this.url, {
|
||||
apiVersion: 'playlists.grafana.com/v0alpha1',
|
||||
kind: 'Playlist',
|
||||
metadata: {
|
||||
name: playlist.uid,
|
||||
},
|
||||
spec: playlist,
|
||||
})
|
||||
);
|
||||
const playlist = k8splaylist.spec;
|
||||
if (playlist.items) {
|
||||
}
|
||||
|
||||
async updatePlaylist(playlist: Playlist): Promise<void> {
|
||||
if (this.legacy) {
|
||||
return this.legacy.updatePlaylist(playlist);
|
||||
}
|
||||
await withErrorHandling(() =>
|
||||
getBackendSrv().put(`${this.url}/${playlist.uid}`, {
|
||||
apiVersion: 'playlists.grafana.com/v0alpha1',
|
||||
kind: 'Playlist',
|
||||
metadata: {
|
||||
name: playlist.uid,
|
||||
},
|
||||
spec: {
|
||||
...playlist,
|
||||
title: playlist.name,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async deletePlaylist(uid: string): Promise<void> {
|
||||
if (this.legacy) {
|
||||
return this.legacy.deletePlaylist(uid);
|
||||
}
|
||||
await withErrorHandling(() => getBackendSrv().delete(`${this.url}/${uid}`), 'Playlist deleted');
|
||||
}
|
||||
}
|
||||
|
||||
// This converts a saved k8s resource into a playlist object
|
||||
// the main difference is that k8s uses metdata.name as the uid
|
||||
// to avoid future confusion, the display name is now called "title"
|
||||
function k8sResourceAsPlaylist(r: K8sPlaylist): Playlist {
|
||||
return {
|
||||
...r.spec,
|
||||
uid: r.metadata.name, // replace the uid from the k8s name
|
||||
};
|
||||
}
|
||||
|
||||
/** @deprecated -- this migrates playlists saved with internal ids to uid */
|
||||
async function migrateInternalIDs(playlist: Playlist) {
|
||||
if (playlist?.items) {
|
||||
for (const item of playlist.items) {
|
||||
if (item.type === 'dashboard_by_id') {
|
||||
item.type = 'dashboard_by_uid';
|
||||
const uids = await getBackendSrv().get<string[]>(`/api/dashboards/ids/${item.value}`);
|
||||
if (uids.length) {
|
||||
if (uids?.length) {
|
||||
item.value = uids[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return playlist;
|
||||
}
|
||||
|
||||
export async function k8sGetAllPlaylist(): Promise<Playlist[]> {
|
||||
const k8splaylists = await getBackendSrv().get<KubernetesPlaylistList>(
|
||||
`/apis/playlist.x.grafana.com/v0alpha1/namespaces/org-${contextSrv.user.orgId}/playlists`
|
||||
);
|
||||
return k8splaylists.playlists.map((p) => p.spec);
|
||||
}
|
||||
|
||||
/** This returns a playlist where all ids are replaced with UIDs */
|
||||
export async function legacyGetPlaylist(uid: string): Promise<Playlist> {
|
||||
const playlist = await getBackendSrv().get<Playlist>(`/api/playlists/${uid}`);
|
||||
if (playlist.items) {
|
||||
for (const item of playlist.items) {
|
||||
if (item.type === 'dashboard_by_id') {
|
||||
item.type = 'dashboard_by_uid';
|
||||
const uids = await getBackendSrv().get<string[]>(`/api/dashboards/ids/${item.value}`);
|
||||
if (uids.length) {
|
||||
item.value = uids[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return playlist;
|
||||
}
|
||||
|
||||
export async function legacyGetAllPlaylist(): Promise<Playlist[]> {
|
||||
return getBackendSrv().get<Playlist[]>('/api/playlists/');
|
||||
}
|
||||
|
||||
async function withErrorHandling(apiCall: () => Promise<void>, message = 'Playlist saved') {
|
||||
@ -158,3 +219,7 @@ export function searchPlaylists(playlists: Playlist[], query?: string): Playlist
|
||||
query = query.toLowerCase();
|
||||
return playlists.filter((v) => v.name.toLowerCase().includes(query!));
|
||||
}
|
||||
|
||||
export function getPlaylistAPI() {
|
||||
return config.featureToggles.kubernetesPlaylists ? new K8sAPI() : new LegacyAPI();
|
||||
}
|
||||
|
@ -1,37 +1,65 @@
|
||||
import { PlaylistItem as PlaylistItemFromSchema } from '@grafana/schema';
|
||||
|
||||
import { DashboardQueryResult } from '../search/service';
|
||||
|
||||
export type PlaylistMode = boolean | 'tv';
|
||||
|
||||
export interface PlayListItemDTO {
|
||||
id: number;
|
||||
title: string;
|
||||
playlistid: string;
|
||||
type: 'dashboard' | 'tag';
|
||||
}
|
||||
|
||||
export interface PlaylistAPI {
|
||||
getPlaylist(uid: string): Promise<Playlist>;
|
||||
getAllPlaylist(): Promise<Playlist[]>;
|
||||
}
|
||||
|
||||
export interface KubernetesPlaylistList {
|
||||
playlists: KubernetesPlaylist[];
|
||||
}
|
||||
|
||||
export interface KubernetesPlaylist {
|
||||
spec: Playlist;
|
||||
getPlaylist(uid: string): Promise<Playlist>;
|
||||
createPlaylist(playlist: Playlist): Promise<void>;
|
||||
updatePlaylist(playlist: Playlist): Promise<void>;
|
||||
deletePlaylist(uid: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface Playlist {
|
||||
/**
|
||||
* Unique playlist identifier. Generated on creation, either by the
|
||||
* creator of the playlist of by the application.
|
||||
*/
|
||||
uid: string;
|
||||
|
||||
/**
|
||||
* Name of the playlist.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Interval sets the time between switching views in a playlist.
|
||||
*/
|
||||
interval: string;
|
||||
|
||||
/**
|
||||
* The ordered list of items that the playlist will iterate over.
|
||||
*/
|
||||
items?: PlaylistItem[];
|
||||
}
|
||||
|
||||
export interface PlaylistItem extends PlaylistItemFromSchema {
|
||||
// Loaded in the frontend
|
||||
export interface PlaylistItem {
|
||||
/**
|
||||
* Type of the item.
|
||||
*/
|
||||
type: // Use an explicit dashboard
|
||||
| 'dashboard_by_uid'
|
||||
// find all dashboards with a given tag
|
||||
| 'dashboard_by_tag'
|
||||
// @deprecated use a dashboard with a given internal id
|
||||
| 'dashboard_by_id';
|
||||
|
||||
/**
|
||||
* Value depends on type and describes the playlist item.
|
||||
*
|
||||
* - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
|
||||
* is not portable as the numerical identifier is non-deterministic between different instances.
|
||||
* Will be replaced by dashboard_by_uid in the future. (deprecated)
|
||||
* - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
|
||||
* dashboards behind the tag will be added to the playlist.
|
||||
* - dashboard_by_uid: The value is the dashboard UID
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* Loaded at runtime by the frontend.
|
||||
*
|
||||
* The values are not stored in the backend database.
|
||||
*/
|
||||
dashboards?: DashboardQueryResult[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user