diff --git a/dashboards/src/components/Datasources/EditDatasourcesButton.tsx b/dashboards/src/components/Datasources/EditDatasourcesButton.tsx index 2a986c9..364270b 100644 --- a/dashboards/src/components/Datasources/EditDatasourcesButton.tsx +++ b/dashboards/src/components/Datasources/EditDatasourcesButton.tsx @@ -15,7 +15,6 @@ import { ReactElement, useState } from 'react'; import { Button } from '@mui/material'; import PencilIcon from 'mdi-material-ui/PencilOutline'; import { Drawer, InfoTooltip } from '@perses-dev/components'; -import { DashboardResource, EphemeralDashboardResource } from '@perses-dev/core'; // TODO weird that ephemeral dashboard is required here import { DatasourceSpec } from '@perses-dev/spec'; import { useDatasourceStore } from '@perses-dev/plugin-system'; import { TOOLTIP_TEXT, editButtonStyle } from '../../constants'; @@ -60,23 +59,13 @@ export function EditDatasourcesButton(): ReactElement { {} as Record ); - setDashboard( - dashboard.kind === 'Dashboard' - ? ({ - ...dashboard, - spec: { - ...dashboard.spec, - datasources: datasources, - }, - } as DashboardResource) - : ({ - ...dashboard, - spec: { - ...dashboard.spec, - datasources: datasources, - }, - } as EphemeralDashboardResource) - ); + setDashboard({ + ...dashboard, + spec: { + ...dashboard.spec, + datasources: datasources, + }, + }); setSavedDatasources(newSavedDatasources); setLocalDatasources(datasources); setIsDatasourceEditorOpen(false); diff --git a/dashboards/src/components/DownloadButton/serializeDashboard.ts b/dashboards/src/components/DownloadButton/serializeDashboard.ts index 393f46e..893c489 100644 --- a/dashboards/src/components/DownloadButton/serializeDashboard.ts +++ b/dashboards/src/components/DownloadButton/serializeDashboard.ts @@ -11,18 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DashboardResource, EphemeralDashboardResource } from '@perses-dev/core'; // TODO metadata should not be used, same for ephemeral dashboard import { stringify } from 'yaml'; +import { DashboardResource } from '../../model/DashboardResource'; + +//TODO: Although the previous comment suggests the metadata not should not be used, I keep them. Need to be discussed. +// Check git history to find prev comment type SerializedDashboard = { contentType: string; content: string; }; -function serializeYaml( - dashboard: DashboardResource | EphemeralDashboardResource, - shape?: 'cr-v1alpha1' | 'cr-v1alpha2' -): SerializedDashboard { +function serializeYaml(dashboard: DashboardResource, shape?: 'cr-v1alpha1' | 'cr-v1alpha2'): SerializedDashboard { let content: string; if (shape === 'cr-v1alpha1') { @@ -73,7 +73,7 @@ function serializeYaml( } export function serializeDashboard( - dashboard: DashboardResource | EphemeralDashboardResource, + dashboard: DashboardResource, format: 'json' | 'yaml', shape?: 'cr-v1alpha1' | 'cr-v1alpha2' ): SerializedDashboard { diff --git a/dashboards/src/components/LeaveDialog/LeaveDialog.tsx b/dashboards/src/components/LeaveDialog/LeaveDialog.tsx index 832e8a1..89635e5 100644 --- a/dashboards/src/components/LeaveDialog/LeaveDialog.tsx +++ b/dashboards/src/components/LeaveDialog/LeaveDialog.tsx @@ -11,11 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DashboardResource, EphemeralDashboardResource } from '@perses-dev/core'; // TODO only dashboard spec should be used import { ReactElement, ReactNode, useEffect } from 'react'; import { useBlocker } from 'react-router-dom'; import { DiscardChangesConfirmationDialog } from '@perses-dev/components'; import type { BlockerFunction } from '@remix-run/router'; +import { DashboardResource } from '../../model'; const handleRouteChange = (event: BeforeUnloadEvent): string => { event.preventDefault(); @@ -69,8 +69,8 @@ export function LeaveDialog({ original, current, }: { - original: DashboardResource | EphemeralDashboardResource | undefined; - current: DashboardResource | EphemeralDashboardResource; + original: DashboardResource | undefined; + current: DashboardResource; }): ReactNode { const handleIsBlocked: BlockerFunction = (ctx) => { if (JSON.stringify(original) !== JSON.stringify(current)) { diff --git a/dashboards/src/context/DashboardProvider/DashboardProvider.tsx b/dashboards/src/context/DashboardProvider/DashboardProvider.tsx index 2e8cb27..22fbac5 100644 --- a/dashboards/src/context/DashboardProvider/DashboardProvider.tsx +++ b/dashboards/src/context/DashboardProvider/DashboardProvider.tsx @@ -18,14 +18,10 @@ import { devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { shallow } from 'zustand/shallow'; import { createContext, ReactElement, ReactNode, useCallback, useContext, useEffect, useState } from 'react'; -import { - ProjectMetadata, - DashboardResource, - DEFAULT_REFRESH_INTERVAL, - EphemeralDashboardResource, -} from '@perses-dev/core'; +import { DEFAULT_REFRESH_INTERVAL } from '@perses-dev/core'; import { Display, DurationString, DatasourceSpec } from '@perses-dev/spec'; import { usePlugin, usePluginRegistry } from '@perses-dev/plugin-system'; +import { DashboardMetaData, DashboardKind, DashboardResource } from '../../model/DashboardResource'; import { createPanelGroupEditorSlice, PanelGroupEditorSlice } from './panel-group-editor-slice'; import { convertLayoutsToPanelGroups, createPanelGroupSlice, PanelGroupSlice } from './panel-group-slice'; import { createPanelEditorSlice, PanelEditorSlice } from './panel-editor-slice'; @@ -55,9 +51,10 @@ export interface DashboardStoreState LinksSlice { isEditMode: boolean; setEditMode: (isEditMode: boolean) => void; - setDashboard: (dashboard: DashboardResource | EphemeralDashboardResource) => void; - kind: DashboardResource['kind'] | EphemeralDashboardResource['kind']; - metadata: ProjectMetadata; + setDashboard: (dashboard: DashboardResource) => void; + dashboardName: string; + kind: DashboardKind; + metadata: DashboardMetaData; duration: DurationString; refreshInterval: DurationString; display?: Display; @@ -76,7 +73,7 @@ export function useDashboardStore(selector: (state: DashboardStoreState) => T } export interface DashboardStoreProps { - dashboardResource: DashboardResource | EphemeralDashboardResource; + dashboardResource: DashboardResource; isEditMode?: boolean; viewPanelRef?: VirtualPanelRef; setViewPanelRef?: (viewPanelRef: VirtualPanelRef | undefined) => void; @@ -128,7 +125,7 @@ function initStore(props: DashboardProviderProps): StoreApi const links = dashboardResource.spec.links ?? []; - const ttl = 'ttl' in dashboardResource.spec ? dashboardResource.spec.ttl : undefined; + const ttl = 'ttl' in dashboardResource.spec ? (dashboardResource.spec.ttl as DurationString) : undefined; const store = createStore()( immer( @@ -151,6 +148,7 @@ function initStore(props: DashboardProviderProps): StoreApi ...createDiscardChangesDialogSlice(...args), ...createEditJsonDialogSlice(...args), ...createSaveChangesDialogSlice(...args), + dashboardName: metadata.name, kind, metadata, display, @@ -169,6 +167,7 @@ function initStore(props: DashboardProviderProps): StoreApi }): void => { set((state) => { state.kind = kind; + state.dashboardName = metadata.name; state.metadata = metadata; state.display = display; state.panels = panels; diff --git a/dashboards/src/context/DashboardProvider/common.ts b/dashboards/src/context/DashboardProvider/common.ts index f69c770..496b995 100644 --- a/dashboards/src/context/DashboardProvider/common.ts +++ b/dashboards/src/context/DashboardProvider/common.ts @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DashboardResource, EphemeralDashboardResource } from '@perses-dev/core'; // TODO import { PanelDefinition, UnknownSpec } from '@perses-dev/spec'; +import { DashboardResource } from '../../model'; -export type OnSaveDashboard = (dashboard: DashboardResource | EphemeralDashboardResource) => Promise; +export type OnSaveDashboard = (dashboard: DashboardResource) => Promise; /** * The middleware applied to the DashboardStore (can be used as generic argument in StateCreator). diff --git a/dashboards/src/context/DashboardProvider/dashboard-provider-api.ts b/dashboards/src/context/DashboardProvider/dashboard-provider-api.ts index cf7baf3..a27d8a0 100644 --- a/dashboards/src/context/DashboardProvider/dashboard-provider-api.ts +++ b/dashboards/src/context/DashboardProvider/dashboard-provider-api.ts @@ -12,14 +12,9 @@ // limitations under the License. import { useCallback, useMemo } from 'react'; -import { - DashboardResource, - EphemeralDashboardResource, - PanelGroupItemLayout, - PanelGroupDefinition, - PanelGroupItemId, -} from '@perses-dev/core'; // TODO +import { PanelGroupItemLayout, PanelGroupDefinition, PanelGroupItemId } from '@perses-dev/core'; // TODO import { DurationString, Link, PanelDefinition, PanelGroupId } from '@perses-dev/spec'; +import { DashboardResource } from '../../model'; import { DashboardStoreState, useDashboardStore } from './DashboardProvider'; import { DeletePanelGroupDialogState } from './delete-panel-group-slice'; import { PanelGroupEditor } from './panel-group-editor-slice'; @@ -41,7 +36,7 @@ export function useEditMode(): { setEditMode: (isEditMode: boolean) => void; isE const selectDashboardActions: ({ setDashboard, openAddPanelGroup, openAddPanel }: DashboardStoreState) => { openAddPanelGroup: () => void; openAddPanel: (panelGroupId?: PanelGroupId) => void; - setDashboard: (dashboard: DashboardResource | EphemeralDashboardResource) => void; + setDashboard: (dashboard: DashboardResource) => void; } = ({ setDashboard, openAddPanelGroup, openAddPanel }: DashboardStoreState) => ({ setDashboard, openAddPanelGroup, @@ -53,7 +48,7 @@ const selectDashboardActions: ({ setDashboard, openAddPanelGroup, openAddPanel } export function useDashboardActions(): { openAddPanelGroup: () => void; openAddPanel: () => void; - setDashboard: (dashboard: DashboardResource | EphemeralDashboardResource) => void; + setDashboard: (dashboard: DashboardResource) => void; } { const { setDashboard, openAddPanelGroup, openAddPanel } = useDashboardStore(selectDashboardActions); return { diff --git a/dashboards/src/context/DatasourceStoreProvider.test.tsx b/dashboards/src/context/DatasourceStoreProvider.test.tsx index 4200065..79e4405 100644 --- a/dashboards/src/context/DatasourceStoreProvider.test.tsx +++ b/dashboards/src/context/DatasourceStoreProvider.test.tsx @@ -23,8 +23,9 @@ import { import { DatasourceStoreProvider } from '@perses-dev/dashboards'; import { PropsWithChildren, ReactElement } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { DashboardResource, Datasource, GlobalDatasourceResource, DatasourceResource } from '@perses-dev/core'; // TODO Datasource should not be used like that, only the spec should be considered -import { DashboardSpec, DatasourceSpec } from '@perses-dev/spec'; +import { Datasource, GlobalDatasourceResource, DatasourceResource } from '@perses-dev/core'; +import { DashboardSpec, DatasourceSpec, UnknownSpec } from '@perses-dev/spec'; +import { DashboardResource } from '../model/DashboardResource'; const PROJECT = 'perses'; const FAKE_PLUGIN_NAME = 'FakeDatasourcePlugin'; @@ -494,9 +495,13 @@ describe('DatasourceStoreProvider::useListDatasourceSelectItems', () => { listGlobalDatasources: jest.fn().mockReturnValue(Promise.resolve(data.input.datasources.global)), }; const queryClient = new QueryClient(); - const dashboard = { - spec: { datasources: data.input.datasources.local } as Partial, - } as DashboardResource; + const dashboard: DashboardResource = { + spec: { + datasources: data.input.datasources.local as Record>, + } as DashboardSpec, + kind: 'Dashboard', + metadata: { name: 'test_dashboard', project: 'test_project' }, + }; const wrapper = ({ children }: PropsWithChildren): ReactElement => { return ( diff --git a/dashboards/src/context/DatasourceStoreProvider.tsx b/dashboards/src/context/DatasourceStoreProvider.tsx index 38309da..33304b1 100644 --- a/dashboards/src/context/DatasourceStoreProvider.tsx +++ b/dashboards/src/context/DatasourceStoreProvider.tsx @@ -12,13 +12,7 @@ // limitations under the License. import { ReactElement, ReactNode, useCallback, useMemo, useState } from 'react'; -import { - DashboardResource, - EphemeralDashboardResource, - DatasourceDefinition, - BuildDatasourceProxyUrlParams, - DatasourceApi, -} from '@perses-dev/core'; // TODO +import { DatasourceDefinition, BuildDatasourceProxyUrlParams, DatasourceApi } from '@perses-dev/core'; // TODO import { DashboardSpec, DatasourceSelector, DatasourceSpec } from '@perses-dev/spec'; import { DatasourceStoreContext, @@ -29,9 +23,10 @@ import { DatasourceClient, DatasourceSelectItem, } from '@perses-dev/plugin-system'; +import { DashboardResource } from '../model/DashboardResource'; export interface DatasourceStoreProviderProps { - dashboardResource?: DashboardResource | EphemeralDashboardResource; + dashboardResource?: DashboardResource; projectName?: string; datasourceApi: DatasourceApi; children?: ReactNode; @@ -71,7 +66,7 @@ export function DatasourceStoreProvider(props: DatasourceStoreProviderProps): Re if (project) { // Try to find it at the project level as a Datasource resource - const datasource = await datasourceApi.getDatasource(project, selector); + const datasource = await datasourceApi.getDatasource(String(project), selector); if (datasource !== undefined) { return { spec: datasource.spec, @@ -126,7 +121,7 @@ export function DatasourceStoreProvider(props: DatasourceStoreProviderProps): Re async (datasourcePluginName: string): Promise => { const [pluginMetadata, datasources, globalDatasources] = await Promise.all([ listPluginMetadata(['Datasource']), - project ? datasourceApi.listDatasources(project, datasourcePluginName) : [], + project ? datasourceApi.listDatasources(String(project), datasourcePluginName) : [], datasourceApi.listGlobalDatasources(datasourcePluginName), ]); @@ -182,23 +177,13 @@ export function DatasourceStoreProvider(props: DatasourceStoreProviderProps): Re const setLocalDatasources = useCallback( (datasources: Record) => { if (dashboardResource) { - setDashboardResource( - dashboardResource.kind === 'Dashboard' - ? ({ - ...dashboardResource, - spec: { - ...dashboardResource.spec, - datasources: datasources, - }, - } as DashboardResource) - : ({ - ...dashboardResource, - spec: { - ...dashboardResource.spec, - datasources: datasources, - }, - } as EphemeralDashboardResource) - ); + setDashboardResource({ + ...dashboardResource, + spec: { + ...dashboardResource.spec, + datasources: datasources, + }, + }); } }, [dashboardResource] diff --git a/dashboards/src/context/useDashboard.tsx b/dashboards/src/context/useDashboard.tsx index 00235df..3043b46 100644 --- a/dashboards/src/context/useDashboard.tsx +++ b/dashboards/src/context/useDashboard.tsx @@ -11,14 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DashboardResource, EphemeralDashboardResource, PanelGroupDefinition } from '@perses-dev/core'; // TODO -import { createPanelRef, GridDefinition, PanelGroupId } from '@perses-dev/spec'; +import { DurationString, PanelGroupDefinition } from '@perses-dev/core'; +import { createPanelRef, DashboardSpec, GridDefinition, PanelGroupId } from '@perses-dev/spec'; +import { DashboardResource } from '../model'; import { useDashboardStore } from './DashboardProvider'; import { useVariableDefinitionActions, useVariableDefinitions } from './VariableProvider'; +type DashboardType = Omit & { spec: DashboardSpec & { ttl?: DurationString } }; export function useDashboard(): { - dashboard: DashboardResource | EphemeralDashboardResource; - setDashboard: (dashboardResource: DashboardResource | EphemeralDashboardResource) => void; + dashboard: DashboardType; + setDashboard: (dashboardResource: DashboardResource) => void; } { const { panels, @@ -66,9 +68,9 @@ export function useDashboard(): { const variables = useVariableDefinitions(); const layouts = convertPanelGroupsToLayouts(panelGroups, panelGroupOrder); - const dashboard = + const dashboard: DashboardType = kind === 'Dashboard' - ? ({ + ? { kind, metadata, spec: { @@ -81,8 +83,8 @@ export function useDashboard(): { datasources, links, }, - } as DashboardResource) - : ({ + } + : { kind, metadata, spec: { @@ -96,9 +98,9 @@ export function useDashboard(): { links, ttl, }, - } as EphemeralDashboardResource); + }; - const setDashboard = (dashboardResource: DashboardResource | EphemeralDashboardResource): void => { + const setDashboard = (dashboardResource: DashboardResource): void => { setVariableDefinitions(dashboardResource.spec.variables); setDashboardResource(dashboardResource); }; diff --git a/dashboards/src/index.ts b/dashboards/src/index.ts index 8bb201f..5127ab9 100644 --- a/dashboards/src/index.ts +++ b/dashboards/src/index.ts @@ -14,3 +14,4 @@ export * from './components'; export * from './context'; export * from './views'; +export * from './model'; diff --git a/dashboards/src/model/DashboardResource.ts b/dashboards/src/model/DashboardResource.ts new file mode 100644 index 0000000..3375af0 --- /dev/null +++ b/dashboards/src/model/DashboardResource.ts @@ -0,0 +1,27 @@ +// Copyright The Perses 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 { DashboardSpec } from '@perses-dev/spec'; + +export type DashboardKind = 'Dashboard' | 'EphemeralDashboard'; +export type DashboardMetaData = { name: string; project: string }; + +/* TODO: There is an open and ongoing issue whether the meta-data should be removed or not. + Such a decision would affect DashbaordProvider and buildDatasourceProxyUrl + https://github.com/perses/perses/issues/4016 +*/ +export interface DashboardResource { + kind: DashboardKind; + spec: DashboardSpec; + metadata: DashboardMetaData; +} diff --git a/dashboards/src/model/index.ts b/dashboards/src/model/index.ts new file mode 100644 index 0000000..e1eeaee --- /dev/null +++ b/dashboards/src/model/index.ts @@ -0,0 +1,14 @@ +// Copyright The Perses 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 './DashboardResource'; diff --git a/dashboards/src/test/dashboard-provider.tsx b/dashboards/src/test/dashboard-provider.tsx index fa28d2c..c1015eb 100644 --- a/dashboards/src/test/dashboard-provider.tsx +++ b/dashboards/src/test/dashboard-provider.tsx @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DashboardResource } from '@perses-dev/core'; // TODO import { useContext } from 'react'; import { StoreApi } from 'zustand'; import { DashboardContext, DashboardStoreState } from '../context'; +import { DashboardResource } from '../model'; import testDashboard from './testDashboard'; /** diff --git a/dashboards/src/test/testDashboard.ts b/dashboards/src/test/testDashboard.ts index 83e630f..395a28f 100644 --- a/dashboards/src/test/testDashboard.ts +++ b/dashboards/src/test/testDashboard.ts @@ -11,9 +11,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DashboardResource } from '@perses-dev/core'; // TODO +import { DashboardResource } from '../model'; -const testDashboard: DashboardResource = { +type DashboardResourceTest = Omit & { + metadata: { + name: string; + project: string; + createdAt: string; + updatedAt: string; + version: number; + }; +}; + +const testDashboard: DashboardResourceTest = { kind: 'Dashboard', metadata: { name: 'Node Stats', diff --git a/dashboards/src/views/ViewDashboard/DashboardApp.tsx b/dashboards/src/views/ViewDashboard/DashboardApp.tsx index 5609ea5..e94effe 100644 --- a/dashboards/src/views/ViewDashboard/DashboardApp.tsx +++ b/dashboards/src/views/ViewDashboard/DashboardApp.tsx @@ -14,8 +14,8 @@ import { ReactElement, ReactNode, useState } from 'react'; import { Box } from '@mui/material'; import { ChartsProvider, ErrorAlert, ErrorBoundary, useChartsTheme } from '@perses-dev/components'; -import { DashboardResource, EphemeralDashboardResource } from '@perses-dev/core'; // TODO only spec should be used import { useDatasourceStore } from '@perses-dev/plugin-system'; +import { DashboardSpec } from '@perses-dev/spec'; import { PanelDrawer, Dashboard, @@ -30,9 +30,10 @@ import { LeaveDialog, } from '../../components'; import { OnSaveDashboard, useDashboard, useDiscardChangesConfirmationDialog, useEditMode } from '../../context'; +import { DashboardResource } from '../../model'; export interface DashboardAppProps { - dashboardResource: DashboardResource | EphemeralDashboardResource; + dashboardResource: DashboardResource; emptyDashboardProps?: Partial; isReadonly: boolean; isVariableEnabled: boolean; @@ -43,7 +44,7 @@ export interface DashboardAppProps { isLeavingConfirmDialogEnabled?: boolean; dashboardTitleComponent?: ReactNode; onSave?: OnSaveDashboard; - onDiscard?: (entity: DashboardResource) => void; + onDiscard?: (name: string, spec: DashboardSpec) => void; } export const DashboardApp = (props: DashboardAppProps): ReactElement => { @@ -64,10 +65,10 @@ export const DashboardApp = (props: DashboardAppProps): ReactElement => { const chartsTheme = useChartsTheme(); const { isEditMode, setEditMode } = useEditMode(); + const { dashboard, setDashboard } = useDashboard(); - const [originalDashboard, setOriginalDashboard] = useState< - DashboardResource | EphemeralDashboardResource | undefined - >(undefined); + const [originalDashboard, setOriginalDashboard] = useState(undefined); + const { setSavedDatasources } = useDatasourceStore(); const { openDiscardChangesConfirmationDialog, closeDiscardChangesConfirmationDialog } = @@ -81,7 +82,7 @@ export const DashboardApp = (props: DashboardAppProps): ReactElement => { setEditMode(false); closeDiscardChangesConfirmationDialog(); if (onDiscard) { - onDiscard(dashboard as unknown as DashboardResource); + onDiscard(dashboard.metadata.name, dashboard.spec); } };