Skip to content

Commit

Permalink
[FEATURE] Support Ephemeral dashboards on the UI
Browse files Browse the repository at this point in the history
Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr>
  • Loading branch information
AntoineThebaud committed Feb 27, 2024
1 parent d735d6d commit e1e73b9
Show file tree
Hide file tree
Showing 35 changed files with 1,380 additions and 59 deletions.
4 changes: 2 additions & 2 deletions dev/config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
security:
readonly: false
encryption_key: "=tW$56zytgB&3jN2E%7-+qrGZE?v6LCc"
enable_auth: true
enable_auth: false
authorization:
guest_permissions:
- actions:
Expand Down Expand Up @@ -45,4 +45,4 @@ information: |-
# Hello World
## File Database setup
ephemeral_dashboards_cleanup_interval: "1h"
ephemeral_dashboards_cleanup_interval: "1m"
4 changes: 4 additions & 0 deletions ui/app/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const ProjectView = lazy(() => import('./views/projects/ProjectView'));
const CreateDashboardView = lazy(() => import('./views/projects/dashboards/CreateDashboardView'));
const DashboardView = lazy(() => import('./views/projects/dashboards/DashboardView'));
const ExploreView = lazy(() => import('./views/projects/explore/ExploreView'));
const CreateEphemeralDashboardView = lazy(() => import('./views/projects/dashboards/CreateEphemeralDashboardView'));
const EphemeralDashboardView = lazy(() => import('./views/projects/dashboards/EphemeralDashboardView'));

function Router() {
const isAuthEnable = useIsAuthEnable();
Expand All @@ -60,6 +62,8 @@ function Router() {
<Route path=":tab" element={<ProjectView />} />
<Route path="dashboard/new" element={<CreateDashboardView />} />
<Route path="dashboards/:dashboardName" element={<DashboardView />} />
<Route path="ephemeraldashboard/new" element={<CreateEphemeralDashboardView />} />
<Route path="ephemeraldashboards/:ephemeralDashboardName" element={<EphemeralDashboardView />} />
</Route>
<Route path="/" element={<HomeView />} />
<Route path="*" element={<Navigate replace to="/" />} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2023 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 { Stack, Typography } from '@mui/material';
import {
DataGrid,
GridColDef,
GridToolbarContainer,
GridToolbarColumnsButton,
GridToolbarFilterButton,
GridToolbarQuickFilter,
GridRow,
GridColumnHeaders,
} from '@mui/x-data-grid';
import { memo, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity';

const DATA_GRID_INITIAL_STATE = {
columns: {
columnVisibilityModel: {},
},
sorting: {
sortModel: [{ field: 'displayName', sort: 'asc' }],
},
pagination: {
paginationModel: { pageSize: 10, page: 0 },
},
};

// https://mui.com/x/react-data-grid/performance/
const MemoizedRow = memo(GridRow);
const MemoizedColumnHeaders = memo(GridColumnHeaders);

export interface Row {
project: string;
name: string;
displayName: string;
version: number;
createdAt: string;
updatedAt: string;
expireAt: Date;
}

function DashboardsGridToolbar() {
return (
<GridToolbarContainer>
<Stack direction="row" width="100%" gap={4} m={2}>
<Stack sx={{ flexShrink: 1 }} width="100%">
<GridToolbarQuickFilter sx={{ width: '100%' }} />
</Stack>
<Stack direction="row" sx={{ flexShrink: 3 }} width="100%">
<GridToolbarColumnsButton sx={{ width: '100%' }} />
<GridToolbarFilterButton sx={{ width: '100%' }} />
</Stack>
</Stack>
</GridToolbarContainer>
);
}

function NoEphemeralDashboardRowOverlay() {
return (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', height: '100%' }}>
<Typography>No ephemeral dashboards</Typography>
</Stack>
);
}

export interface EphemeralDashboardDataGridProperties {
columns: Array<GridColDef<Row>>;
rows: Row[];
initialState?: GridInitialStateCommunity;
hideToolbar?: boolean;
isLoading?: boolean;
}

export function EphemeralDashboardDataGrid(props: EphemeralDashboardDataGridProperties) {
const { columns, rows, initialState, hideToolbar, isLoading } = props;

const navigate = useNavigate();

// Merging default initial state with the props initial state (props initial state will overwrite properties)
const mergedInitialState = useMemo(() => {
return {
...DATA_GRID_INITIAL_STATE,
...(initialState || {}),
} as GridInitialStateCommunity;
}, [initialState]);

return (
<DataGrid
autoHeight={true}
onRowClick={(params) => navigate(`/projects/${params.row.project}/ephemeraldashboards/${params.row.name}`)}
rows={rows}
columns={columns}
getRowId={(row) => row.name}
loading={isLoading}
slots={
hideToolbar
? { noRowsOverlay: NoEphemeralDashboardRowOverlay }
: {
toolbar: DashboardsGridToolbar,
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
noRowsOverlay: NoEphemeralDashboardRowOverlay,
}
}
pageSizeOptions={[10, 25, 50, 100]}
initialState={mergedInitialState}
sx={{
// disable cell selection style
'.MuiDataGrid-columnHeader:focus': {
outline: 'none',
},
// disable cell selection style
'.MuiDataGrid-cell:focus': {
outline: 'none',
},
// pointer cursor on ALL rows
'& .MuiDataGrid-row:hover': {
cursor: 'pointer',
},
}}
></DataGrid>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright 2024 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 {
getDashboardDisplayName,
EphemeralDashboardResource,
DashboardResource,
parseDurationString,
} from '@perses-dev/core';
import { Box, Stack, Tooltip } from '@mui/material';
import { GridColDef, GridRowParams, GridValueGetterParams } from '@mui/x-data-grid';
import DeleteIcon from 'mdi-material-ui/DeleteOutline';
import PencilIcon from 'mdi-material-ui/Pencil';
import { useCallback, useMemo, useState } from 'react';
import { intlFormatDistance, add } from 'date-fns';
import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity';
import { DeleteEphemeralDashboardDialog, UpdateEphemeralDashboardDialog } from '../dialogs';
import { CRUDGridActionsCellItem } from '../CRUDButton/CRUDGridActionsCellItem';
import { EphemeralDashboardDataGrid, Row } from './EphemeralDashboardDataGrid';

export interface EphemeralDashboardListProperties {
ephemeralDashboardList: EphemeralDashboardResource[];
hideToolbar?: boolean;
initialState?: GridInitialStateCommunity;
isLoading?: boolean;
}

/**
* Display ephemeral dashboards in a table style.
* @param props.ephemeralDashboardList Contains all ephemeral dashboards to display
* @param props.hideToolbar Hide toolbar if enabled
* @param props.initialState Provide a way to override default initialState
* @param props.isLoading Display a loading circle if enabled
*/
export function EphemeralDashboardList(props: EphemeralDashboardListProperties) {
const { ephemeralDashboardList, hideToolbar, isLoading, initialState } = props;

const getDashboard = useCallback(
(project: string, name: string) => {
return ephemeralDashboardList.find(
(ephemeralDashboard) =>
ephemeralDashboard.metadata.project === project && ephemeralDashboard.metadata.name === name
);
},
[ephemeralDashboardList]
);

const getExpirationDate = useCallback((ephemeralDashboard: EphemeralDashboardResource): Date => {
return add(
ephemeralDashboard.metadata.updatedAt ? new Date(ephemeralDashboard.metadata.updatedAt) : new Date(),
parseDurationString(ephemeralDashboard.spec.ttl)
);
}, []);

const rows = useMemo(() => {
return ephemeralDashboardList.map(
(ephemeralDashboard) =>
({
project: ephemeralDashboard.metadata.project,
name: ephemeralDashboard.metadata.name,
displayName: getDashboardDisplayName(ephemeralDashboard as unknown as DashboardResource),
expireAt: getExpirationDate(ephemeralDashboard),
version: ephemeralDashboard.metadata.version,
createdAt: ephemeralDashboard.metadata.createdAt,
updatedAt: ephemeralDashboard.metadata.updatedAt,
}) as Row
);
}, [ephemeralDashboardList, getExpirationDate]);

const [targetedEphemeralDashboard, setTargetedDashboard] = useState<EphemeralDashboardResource>();
const [isRenameEphemeralDashboardDialogStateOpened, setRenameEphemeralDashboardDialogStateOpened] =
useState<boolean>(false);
const [isDeleteEphemeralDashboardDialogStateOpened, setDeleteEphemeralDashboardDialogStateOpened] =
useState<boolean>(false);

const onRenameButtonClick = useCallback(
(project: string, name: string) => () => {
setTargetedDashboard(getDashboard(project, name));
setRenameEphemeralDashboardDialogStateOpened(true);
},
[getDashboard]
);

const onDeleteButtonClick = useCallback(
(project: string, name: string) => () => {
setTargetedDashboard(getDashboard(project, name));
setDeleteEphemeralDashboardDialogStateOpened(true);
},
[getDashboard]
);

const columns = useMemo<Array<GridColDef<Row>>>(
() => [
{ field: 'project', headerName: 'Project', type: 'string', flex: 2, minWidth: 150 },
{ field: 'displayName', headerName: 'Display Name', type: 'string', flex: 3, minWidth: 150 },
{
field: 'version',
headerName: 'Version',
type: 'number',
align: 'right',
headerAlign: 'right',
flex: 1,
minWidth: 80,
},
{
field: 'expireAt',
headerName: 'Expiration Date',
type: 'dateTime',
flex: 3,
minWidth: 150,
renderCell: (params) => (
<Tooltip title={params.value.toUTCString()} placement="top">
<span>{intlFormatDistance(params.value, new Date())}</span>
</Tooltip>
),
},
{
field: 'createdAt',
headerName: 'Creation Date',
type: 'dateTime',
flex: 1,
minWidth: 125,
valueGetter: (params: GridValueGetterParams) => new Date(params.row.createdAt),
renderCell: (params) => (
<Tooltip title={params.value.toUTCString()} placement="top">
<span>{intlFormatDistance(params.value, new Date())}</span>
</Tooltip>
),
},
{
field: 'updatedAt',
headerName: 'Last Update',
type: 'dateTime',
flex: 1,
minWidth: 125,
valueGetter: (params: GridValueGetterParams) => new Date(params.row.updatedAt),
renderCell: (params) => (
<Tooltip title={params.value.toUTCString()} placement="top">
<span>{intlFormatDistance(params.value, new Date())}</span>
</Tooltip>
),
},
{
field: 'actions',
headerName: 'Actions',
type: 'actions',
flex: 0.5,
minWidth: 100,
getActions: (params: GridRowParams<Row>) => [
<CRUDGridActionsCellItem
key={params.id + '-edit'}
icon={<PencilIcon />}
label="Rename"
action="update"
scope="Dashboard"
project={params.row.project}
onClick={onRenameButtonClick(params.row.project, params.row.name)}
/>,
<CRUDGridActionsCellItem
key={params.id + '-delete'}
icon={<DeleteIcon />}
label="Delete"
action="delete"
scope="Dashboard"
project={params.row.project}
onClick={onDeleteButtonClick(params.row.project, params.row.name)}
/>,
],
},
],
[onRenameButtonClick, onDeleteButtonClick]
);

return (
<Stack width="100%">
<EphemeralDashboardDataGrid
rows={rows}
columns={columns}
initialState={initialState}
hideToolbar={hideToolbar}
isLoading={isLoading}
/>
<Box>
{targetedEphemeralDashboard && (
<>
<UpdateEphemeralDashboardDialog
open={isRenameEphemeralDashboardDialogStateOpened}
onClose={() => setRenameEphemeralDashboardDialogStateOpened(false)}
ephemeralDashboard={targetedEphemeralDashboard}
/>
<DeleteEphemeralDashboardDialog
open={isDeleteEphemeralDashboardDialogStateOpened}
onClose={() => setDeleteEphemeralDashboardDialogStateOpened(false)}
ephemeralDashboard={targetedEphemeralDashboard}
/>
</>
)}
</Box>
</Stack>
);
}
Loading

0 comments on commit e1e73b9

Please sign in to comment.