Skip to content

Commit 8af9e34

Browse files
author
Iris Ye
committed
Table: gauge cells, conditional colors, range inputs
- gauge chart fixed in cell - gauge chart colors aligned with cell settings - range function debugged to allow only number (letters except e(used as scientific notation) cannot be entered instead of returning NaN) - range function allow decimals not just integers Signed-off-by: Iris Ye <iris.ye@sap.com>
1 parent 733f10c commit 8af9e34

4 files changed

Lines changed: 356 additions & 31 deletions

File tree

table/src/components/ColumnsEditor/ColumnEditor.tsx

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14-
import { Button, ButtonGroup, Stack, StackProps, Switch, TextField } from '@mui/material';
14+
import { Button, ButtonGroup, Stack, StackProps, Switch, TextField, Typography } from '@mui/material';
1515
import { ReactElement, useState } from 'react';
1616
import {
1717
AlignSelector,
@@ -28,6 +28,7 @@ import { PluginKindSelect } from '@perses-dev/plugin-system';
2828
import { ColumnSettings } from '../../models';
2929
import { ConditionalPanel } from '../ConditionalPanel';
3030
import { DataLinkEditor } from './DataLinkEditorDialog';
31+
import { EmbeddedPanelOptionsEditor } from './EmbeddedPanelOptionsEditor';
3132

3233
const DEFAULT_FORMAT: FormatOptions = {
3334
unit: 'decimal',
@@ -131,30 +132,37 @@ export function ColumnEditor({ column, onChange, ...others }: ColumnEditorProps)
131132
}
132133
/>
133134
<OptionsEditorControl
134-
label="Display"
135+
label="Cell display"
135136
control={
136-
<ButtonGroup aria-label="Display" size="small">
137-
<Button
138-
variant={!column.plugin ? 'contained' : 'outlined'}
139-
onClick={() => onChange({ ...column, plugin: undefined })}
140-
>
141-
Text
142-
</Button>
143-
<Button
144-
variant={column.plugin ? 'contained' : 'outlined'}
145-
onClick={() => onChange({ ...column, plugin: { kind: 'StatChart', spec: {} } })}
146-
>
147-
Embedded Panel
148-
</Button>
149-
</ButtonGroup>
137+
<Stack spacing={1}>
138+
<ButtonGroup aria-label="Cell display" size="small">
139+
<Button
140+
variant={!column.plugin ? 'contained' : 'outlined'}
141+
onClick={() => onChange({ ...column, plugin: undefined })}
142+
>
143+
Text
144+
</Button>
145+
<Button
146+
variant={column.plugin ? 'contained' : 'outlined'}
147+
onClick={() => onChange({ ...column, plugin: { kind: 'GaugeChart', spec: {} } })}
148+
>
149+
Visualization
150+
</Button>
151+
</ButtonGroup>
152+
<Typography variant="caption" color="text.secondary" sx={{ maxWidth: 360 }}>
153+
Visualizations reuse panel settings (thresholds, units, colors). Text mode uses value formatting
154+
below.
155+
</Typography>
156+
</Stack>
150157
}
151158
/>
152159
{column.plugin ? (
153160
<OptionsEditorControl
154-
label="Panel Type"
161+
label="Visualization type"
155162
control={
156163
<PluginKindSelect
157164
pluginTypes={['Panel']}
165+
size="small"
158166
value={{ type: 'Panel', kind: column.plugin.kind }}
159167
onChange={(event) => onChange({ ...column, plugin: { kind: event.kind, spec: {} } })}
160168
/>
@@ -214,7 +222,23 @@ export function ColumnEditor({ column, onChange, ...others }: ColumnEditorProps)
214222
</OptionsEditorGroup>
215223
</OptionsEditorColumn>
216224
</OptionsEditorGrid>
217-
<Stack sx={{ px: 8 }}>
225+
{column.plugin?.kind === 'GaugeChart' && (
226+
<Stack sx={{ px: 8, mt: 4, width: '100%' }} spacing={2}>
227+
<OptionsEditorGroup title="Visualization settings">
228+
<EmbeddedPanelOptionsEditor
229+
kind="GaugeChart"
230+
spec={column.plugin.spec}
231+
onChange={(nextSpec) =>
232+
onChange({
233+
...column,
234+
plugin: { kind: 'GaugeChart', spec: nextSpec },
235+
})
236+
}
237+
/>
238+
</OptionsEditorGroup>
239+
</Stack>
240+
)}
241+
<Stack sx={{ px: 8, mt: column.plugin?.kind === 'GaugeChart' ? 3 : 0 }}>
218242
<OptionsEditorGroup title="Conditional Cell Format">
219243
<ConditionalPanel
220244
cellSettings={column.cellSettings}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import { CircularProgress, Stack, Typography } from '@mui/material';
15+
import { UnknownSpec } from '@perses-dev/core';
16+
import { OptionsEditorTabs, PanelPlugin, usePlugin } from '@perses-dev/plugin-system';
17+
import merge from 'lodash/merge';
18+
import { ReactElement, useEffect, useMemo, useRef } from 'react';
19+
20+
export interface EmbeddedPanelOptionsEditorProps {
21+
kind: string;
22+
spec: UnknownSpec;
23+
onChange: (next: UnknownSpec) => void;
24+
}
25+
26+
function isSpecEmpty(spec: UnknownSpec | undefined): boolean {
27+
if (spec === undefined || spec === null) return true;
28+
if (typeof spec !== 'object') return false;
29+
return Object.keys(spec as object).length === 0;
30+
}
31+
32+
function mergeWithPluginDefaults(plugin: PanelPlugin, spec: UnknownSpec | undefined): UnknownSpec {
33+
const initial = plugin.createInitialOptions() ?? {};
34+
return merge({}, initial, spec ?? {}) as UnknownSpec;
35+
}
36+
37+
/**
38+
* Renders a panel plugin's settings tabs (thresholds, units, colors, …).
39+
* Used for embedded GaugeChart columns only; other embedded panel kinds use defaults.
40+
*/
41+
export function EmbeddedPanelOptionsEditor({ kind, spec, onChange }: EmbeddedPanelOptionsEditorProps): ReactElement {
42+
const { data: plugin, isLoading, isError, error } = usePlugin('Panel', kind);
43+
44+
const panelPlugin = plugin as PanelPlugin | undefined;
45+
46+
const mergedSpec = useMemo(() => {
47+
if (!panelPlugin) {
48+
return spec;
49+
}
50+
return mergeWithPluginDefaults(panelPlugin, spec);
51+
}, [panelPlugin, spec]);
52+
53+
const onChangeRef = useRef(onChange);
54+
onChangeRef.current = onChange;
55+
56+
// Persist plugin defaults when the column still has an empty spec (e.g. after switching panel kind).
57+
useEffect(() => {
58+
if (!panelPlugin || !isSpecEmpty(spec)) {
59+
return;
60+
}
61+
onChangeRef.current(mergeWithPluginDefaults(panelPlugin, spec));
62+
}, [panelPlugin, kind, spec]);
63+
64+
if (isLoading) {
65+
return (
66+
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 1 }}>
67+
<CircularProgress size={22} />
68+
<Typography variant="body2" color="text.secondary">
69+
Loading panel settings…
70+
</Typography>
71+
</Stack>
72+
);
73+
}
74+
75+
if (isError || !plugin) {
76+
return (
77+
<Typography variant="body2" color="error">
78+
{error?.message ?? 'Could not load panel plugin.'}
79+
</Typography>
80+
);
81+
}
82+
83+
const loadedPlugin = plugin as PanelPlugin;
84+
const editorTabs = loadedPlugin.panelOptionsEditorComponents ?? [];
85+
86+
if (editorTabs.length === 0) {
87+
return (
88+
<Typography variant="body2" color="text.secondary">
89+
This visualization has no editable settings.
90+
</Typography>
91+
);
92+
}
93+
94+
return (
95+
<Stack spacing={2.5} sx={{ width: '100%', py: 1 }}>
96+
<OptionsEditorTabs
97+
tabs={editorTabs.map((tab) => {
98+
const Content = tab.content;
99+
return {
100+
label: tab.label,
101+
content: (
102+
<Content
103+
value={mergedSpec}
104+
onChange={(next) => {
105+
onChange(next as UnknownSpec);
106+
}}
107+
/>
108+
),
109+
};
110+
})}
111+
/>
112+
</Stack>
113+
);
114+
}

0 commit comments

Comments
 (0)