Api clients: Update provisioning, playlist clients and fix type errors (#109850)

* Update provisioning types

* Update playlist types

* Revert

* import

* lint
This commit is contained in:
Alex Khomenko 2025-08-19 16:01:38 +03:00 committed by GitHub
parent 525f444407
commit 2a617abd77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 75 additions and 69 deletions

View File

@ -261,43 +261,43 @@ export type ObjectMeta = {
Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */
uid?: string;
};
export type PlaylistItem = {
/** type of the item. */
type: string;
/** 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;
};
export type PlaylistSpec = {
interval: string;
items: PlaylistItem[];
items: {
/** type of the item. */
type: 'dashboard_by_tag' | 'dashboard_by_uid' | '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;
}[];
title: string;
};
export type PlayliststatusOperatorState = {
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
descriptiveState?: string;
/** details contains any extra information that is operator-specific */
details?: {
[key: string]: object;
};
/** lastEvaluation is the ResourceVersion last evaluated */
lastEvaluation: string;
/** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */
state: string;
};
export type PlaylistStatus = {
/** additionalFields is reserved for future use */
additionalFields?: {
[key: string]: object;
[key: string]: any;
};
/** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
/** operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. */
operatorStates?: {
[key: string]: PlayliststatusOperatorState;
[key: string]: {
/** descriptiveState is an optional more descriptive state field which has no requirements on format */
descriptiveState?: string;
/** details contains any extra information that is operator-specific */
details?: {
[key: string]: any;
};
/** lastEvaluation is the ResourceVersion last evaluated */
lastEvaluation: string;
/** state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation. */
state: 'success' | 'in_progress' | 'failed';
};
};
};
export type Playlist = {
@ -305,10 +305,9 @@ export type Playlist = {
apiVersion?: string;
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
kind?: string;
metadata: ObjectMeta;
/** Spec is the spec of the Playlist */
spec: PlaylistSpec;
status: PlaylistStatus;
metadata?: ObjectMeta;
spec?: PlaylistSpec;
status?: PlaylistStatus;
};
export type ListMeta = {
/** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */

View File

@ -17,17 +17,20 @@ export const playlistAPIv0alpha1 = generatedAPI.enhanceEndpoints({
},
createPlaylist: (endpointDefinition) => {
const originalQuery = endpointDefinition.query;
if (originalQuery) {
endpointDefinition.query = (requestOptions) => {
if (!requestOptions.playlist.metadata.name && !requestOptions.playlist.metadata.generateName) {
const login = contextSrv.user.login;
// GenerateName lets the apiserver create a new uid for the name
// The passed in value is the suggested prefix
requestOptions.playlist.metadata.generateName = login ? login.slice(0, 2) : 'g';
}
return originalQuery(requestOptions);
};
if (!originalQuery) {
return;
}
endpointDefinition.query = (requestOptions) => {
const metadata = requestOptions.playlist.metadata;
if (metadata && !metadata.name && !metadata.generateName) {
// GenerateName lets the apiserver create a new uid for the name
// The passed in value is the suggested prefix
metadata.generateName = contextSrv.user.login?.slice(0, 2) || 'g';
}
return originalQuery(requestOptions);
};
endpointDefinition.onQueryStarted = async (_, { queryFulfilled, dispatch }) => {
try {
await queryFulfilled;
@ -61,7 +64,7 @@ export const playlistAPIv0alpha1 = generatedAPI.enhanceEndpoints({
});
/** @deprecated -- this migrates playlists saved with internal ids to uid */
async function migrateInternalIDs(playlist: PlaylistSpec) {
async function migrateInternalIDs(playlist?: PlaylistSpec) {
if (playlist?.items) {
for (const item of playlist.items) {
if (item.type === 'dashboard_by_id') {
@ -84,4 +87,4 @@ export const {
} = playlistAPIv0alpha1;
// eslint-disable-next-line no-barrel-files/no-barrel-files
export type { Playlist } from './endpoints.gen';
export type { Playlist, PlaylistSpec } from './endpoints.gen';

View File

@ -1013,7 +1013,7 @@ export type RepositorySpec = {
- `"local"` */
type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local';
/** UI driven Workflow that allow changes to the contends of the repository. The order is relevant for defining the precedence of the workflows. When empty, the repository does not support any edits (eg, readonly) */
workflows: RepoWorkflows;
workflows: ('branch' | 'write')[];
};
export type HealthStatus = {
/** When the health was checked last time */
@ -1258,7 +1258,6 @@ export type WebhookResponse = {
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
kind?: string;
};
export type RepoWorkflows = ('branch' | 'write')[]
export type RepositoryView = {
/** For git, this is the target branch */
branch?: string;
@ -1282,7 +1281,7 @@ export type RepositoryView = {
- `"local"` */
type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local';
/** The supported workflows */
workflows: RepoWorkflows;
workflows: ('branch' | 'write')[];
};
export type RepositoryViewList = {
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */

View File

@ -22,7 +22,7 @@ const PlaylistCardComponent = ({ playlist, setStartPlaylist, setPlaylistToDelete
return (
<Card>
<Card.Heading>
{playlist.spec.title}
{playlist.spec?.title}
<ModalsController key="button-share">
{({ showModal, hideModal }) => (
<DashNavButton
@ -31,7 +31,7 @@ const PlaylistCardComponent = ({ playlist, setStartPlaylist, setPlaylistToDelete
iconSize="lg"
onClick={() => {
showModal(ShareModal, {
playlistUid: playlist.metadata.name ?? '',
playlistUid: playlist.metadata?.name ?? '',
onDismiss: hideModal,
});
}}
@ -45,7 +45,7 @@ const PlaylistCardComponent = ({ playlist, setStartPlaylist, setPlaylistToDelete
</Button>
{contextSrv.isEditor && (
<>
<LinkButton key="edit" variant="secondary" href={`/playlists/edit/${playlist.metadata.name}`} icon="cog">
<LinkButton key="edit" variant="secondary" href={`/playlists/edit/${playlist.metadata?.name}`} icon="cog">
<Trans i18nKey="playlist-page.card.edit">Edit playlist</Trans>
</LinkButton>
<Button

View File

@ -20,7 +20,7 @@ export const PlaylistEditPage = () => {
const onSubmit = async (playlist: Playlist) => {
replacePlaylist({
name: playlist.metadata.name ?? '',
name: playlist.metadata?.name ?? '',
playlist,
});
locationService.push('/playlists');

View File

@ -8,7 +8,7 @@ import { Form } from 'app/core/components/Form/Form';
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
import { Playlist } from '../../api/clients/playlist/v0alpha1';
import { Playlist, PlaylistSpec } from '../../api/clients/playlist/v0alpha1';
import { getGrafanaSearcher } from '../search/service/searcher';
import { PlaylistTable } from './PlaylistTable';
@ -21,7 +21,7 @@ interface Props {
export const PlaylistForm = ({ onSubmit, playlist }: Props) => {
const [saving, setSaving] = useState(false);
const { title: name, interval, items: propItems } = playlist.spec;
const { title: name, interval, items: propItems } = playlist.spec || {};
const tagOptions = useMemo(() => {
return () => getGrafanaSearcher().tags({ kind: ['dashboard'] });
}, []);
@ -34,13 +34,15 @@ export const PlaylistForm = ({ onSubmit, playlist }: Props) => {
...playlist,
spec: {
...specUpdates,
interval: specUpdates?.interval ?? '5m',
title: specUpdates?.title ?? '',
items,
},
});
};
return (
<Form onSubmit={doSubmit} validateOn={'onBlur'}>
<Form<PlaylistSpec> onSubmit={doSubmit} validateOn={'onBlur'}>
{({ register, errors }) => {
const isDisabled = items.length === 0 || Object.keys(errors).length > 0;
return (

View File

@ -29,7 +29,7 @@ export const PlaylistPage = () => {
return;
}
deletePlaylist({
name: playlistToDelete.metadata.name ?? '',
name: playlistToDelete.metadata?.name ?? '',
}).finally(() => {
setPlaylistToDelete(undefined);
});
@ -84,10 +84,10 @@ export const PlaylistPage = () => {
)}
{playlistToDelete && (
<ConfirmModal
title={playlistToDelete.spec.title}
title={playlistToDelete.spec?.title ?? ''}
confirmText={t('playlist-page.delete-modal.confirm-text', 'Delete')}
body={t('playlist-page.delete-modal.body', 'Are you sure you want to delete {{name}} playlist?', {
name: playlistToDelete.spec.title,
name: playlistToDelete.spec?.title,
})}
onConfirm={onDeletePlaylist}
isOpen={Boolean(playlistToDelete)}

View File

@ -19,7 +19,7 @@ const PlaylistPageListComponent = ({ playlists, setStartPlaylist, setPlaylistToD
return (
<ul className={styles.list}>
{playlists.map((playlist) => (
<li className={styles.listItem} key={playlist.metadata.name}>
<li className={styles.listItem} key={playlist.metadata?.name}>
<PlaylistCard
playlist={playlist}
setStartPlaylist={setStartPlaylist}

View File

@ -103,14 +103,14 @@ export class PlaylistSrv extends StateManagerBase<PlaylistSrvState> {
this.locationListenerUnsub = locationService.getHistory().listen(this.locationUpdated);
const urls: string[] = [];
if (!playlist.spec.items?.length) {
if (!playlist.spec?.items?.length) {
// alert
return;
}
this.interval = rangeUtil.intervalToMs(playlist.spec.interval);
this.interval = rangeUtil.intervalToMs(playlist.spec?.interval);
const items = await loadDashboards(playlist.spec.items);
const items = await loadDashboards(playlist.spec?.items);
for (const item of items) {
if (item.dashboards) {
for (const dash of item.dashboards) {

View File

@ -45,7 +45,7 @@ export const StartModal = ({ playlist, onDismiss }: Props) => {
params['_dash.hideLinks'] = true;
}
locationService.push(urlUtil.renderUrl(`/playlists/play/${playlist.metadata.name}`, params));
locationService.push(urlUtil.renderUrl(`/playlists/play/${playlist.metadata?.name}`, params));
reportInteraction('grafana_kiosk_mode', {
action: 'start_playlist',
mode: mode,
@ -110,7 +110,7 @@ export const StartModal = ({ playlist, onDismiss }: Props) => {
</FieldSet>
<Modal.ButtonRow>
<Button variant="primary" onClick={onStart}>
<Trans i18nKey="playlist.start-modal.button-start" values={{ title: playlist.spec.title }}>
<Trans i18nKey="playlist.start-modal.button-start" values={{ title: playlist.spec?.title }}>
Start {'{{title}}'}
</Trans>
</Button>

View File

@ -1,8 +1,8 @@
import { Playlist } from '../../api/clients/playlist/v0alpha1';
import { PlaylistSpec } from '../../api/clients/playlist/v0alpha1';
import { DashboardQueryResult } from '../search/service/types';
export type PlaylistMode = boolean;
type PlaylistItem = Playlist['spec']['items'][number];
type PlaylistItem = PlaylistSpec['items'][number];
export interface PlaylistItemUI extends PlaylistItem {
/**

View File

@ -88,5 +88,5 @@ export function searchPlaylists(playlists: Playlist[], query?: string): Playlist
return playlists;
}
query = query.toLowerCase();
return playlists.filter((v) => v.spec.title.toLowerCase().includes(query!));
return playlists.filter((v) => v.spec?.title.toLowerCase().includes(query!));
}

View File

@ -14,6 +14,7 @@ import {
// Repository type definition - extracted from API client
export type RepositoryType = RepositorySpec['type'];
export type RepoWorkflows = RepositorySpec['workflows'];
// Field configuration interface
export interface RepositoryFieldData {

View File

@ -1,8 +1,10 @@
import { t } from '@grafana/i18n';
import { RepositoryView, RepoWorkflows } from 'app/api/clients/provisioning/v0alpha1';
import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1';
import { RepoWorkflows } from '../types';
export function getIsReadOnlyWorkflows(workflows?: RepoWorkflows): boolean {
// Repository is consider read-only if it has no workflows defined (workflows are required for write operations)
// Repository is considered read-only if it has no workflows defined (workflows are required for write operations)
return workflows?.length === 0;
}
@ -14,7 +16,7 @@ export function getIsReadOnlyRepo(repository: RepositoryView | undefined): boole
return getIsReadOnlyWorkflows(repository.workflows);
}
// Right now we only support local file provisioning message and git provisioned. This can be extend in the future as needed.
// Right now we only support local file provisioning message and git provisioned. This can be extended in the future as needed.
export const getReadOnlyTooltipText = ({ isLocal = false }) => {
return isLocal
? t(