Edit metric title types

This commit is contained in:
Nate Kelley 2025-03-03 23:12:15 -07:00
parent 58c8c1f4ab
commit 0bd385799f
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
8 changed files with 66 additions and 483 deletions

View File

@ -8,14 +8,15 @@ import {
SelectTrigger,
SelectValue
} from './SelectBase';
import { useMemoizedFn } from 'ahooks';
interface SelectItemGroup {
interface SelectItemGroup<T = string> {
label: string;
items: SelectItem[];
items: SelectItem<T>[];
}
export interface SelectItem {
value: string;
export interface SelectItem<T = string> {
value: T;
label: string; //this will be used in the select item text
secondaryLabel?: string;
icon?: React.ReactNode;
@ -23,10 +24,10 @@ export interface SelectItem {
disabled?: boolean;
}
export interface SelectProps {
items: SelectItem[] | SelectItemGroup[];
export interface SelectProps<T = string> {
items: SelectItem<T>[] | SelectItemGroup[];
disabled?: boolean;
onChange: (value: string) => void;
onChange: (value: T) => void;
placeholder?: string;
value?: string;
onOpenChange?: (open: boolean) => void;
@ -35,44 +36,49 @@ export interface SelectProps {
className?: string;
}
export const Select: React.FC<SelectProps> = React.memo(
({
items,
showIndex,
disabled,
onChange,
placeholder,
value,
onOpenChange,
open,
className = ''
}) => {
return (
<SelectBase
disabled={disabled}
onOpenChange={onOpenChange}
open={open}
onValueChange={onChange}>
<SelectTrigger className={className}>
<SelectValue placeholder={placeholder} defaultValue={value} />
</SelectTrigger>
<SelectContent>
{items.map((item, index) => (
<SelectItemSelector key={index} item={item} index={index} showIndex={showIndex} />
))}
</SelectContent>
</SelectBase>
);
}
);
const _Select = <T,>({
items,
showIndex,
disabled,
onChange,
placeholder,
value,
onOpenChange,
open,
className = ''
}: SelectProps<T>) => {
const onValueChange = useMemoizedFn((value: string) => {
onChange(value as T);
});
return (
<SelectBase
disabled={disabled}
onOpenChange={onOpenChange}
open={open}
onValueChange={onValueChange}>
<SelectTrigger className={className}>
<SelectValue placeholder={placeholder} defaultValue={value} />
</SelectTrigger>
<SelectContent>
{items.map((item, index) => (
<SelectItemSelector key={index} item={item} index={index} showIndex={showIndex} />
))}
</SelectContent>
</SelectBase>
);
};
_Select.displayName = 'Select';
export const Select = React.memo(_Select) as typeof _Select;
Select.displayName = 'Select';
const SelectItemSelector: React.FC<{
item: SelectItem | SelectItemGroup;
const SelectItemSelector = <T,>({
item,
index,
showIndex
}: {
item: SelectItem<T> | SelectItemGroup;
index: number;
showIndex?: boolean;
}> = React.memo(({ item, index, showIndex }) => {
}) => {
const isGroup = typeof item === 'object' && 'items' in item;
if (isGroup) {
@ -101,7 +107,7 @@ const SelectItemSelector: React.FC<{
{label}
</SelectItem>
);
});
};
SelectItemSelector.displayName = 'SelectItemSelector';
const SelectItemSecondaryText: React.FC<{ children: React.ReactNode }> = ({ children }) => {

View File

@ -0,0 +1 @@
export * from './AppSwitch';

View File

@ -1,10 +1,11 @@
import { AppMaterialIcons, AppPopover } from '@/components/ui';
import { Button } from 'antd';
import { Button } from '@/components/ui/buttons';
import React, { useMemo } from 'react';
import { SelectAxisContainerId } from '../config';
import { SelectAxisSettingContent } from './SelectAxisSettingContent';
import { useSelectAxisContextSelector } from '../useSelectAxisContext';
import { zoneIdToAxisSettingContent } from './config';
import { Popover } from '@/components/ui/tooltip/Popover';
import { Sliders3 } from '@/components/ui/icons';
export const SelectAxisSettingsButton: React.FC<{
zoneId: SelectAxisContainerId;
@ -20,14 +21,13 @@ export const SelectAxisSettingsButton: React.FC<{
if (!canUseAxisSetting) return null;
return (
<AppPopover
<Popover
content={<SelectAxisSettingContent zoneId={zoneId} />}
trigger="click"
destroyTooltipOnHide
performant
placement="leftBottom">
<Button type="text" icon={<AppMaterialIcons icon="tune" />} />
</AppPopover>
align="end"
side="left">
<Button variant="ghost" prefix={<Sliders3 />} />
</Popover>
);
});
SelectAxisSettingsButton.displayName = 'SelectAxisSettingsButton';

View File

@ -1,375 +0,0 @@
import type { IBusterMetricChartConfig, ColumnMetaData } from '@/api/asset_interfaces';
import { AppPopover, AppMaterialIcons } from '@/components/ui';
import type { IColumnLabelFormat, DerivedMetricTitle } from '@/components/ui/charts';
import { formatLabel, isNumericColumnType, isNumericColumnStyle } from '@/lib';
import { useMemoizedFn } from 'ahooks';
import { Input, Button, Divider, Switch, Select } from 'antd';
import { createStyles } from 'antd-style';
import last from 'lodash/last';
import React, { useMemo } from 'react';
import { LabelAndInput } from '../../Common';
import { AGGREGATE_OPTIONS } from './EditMetricType';
import { Text } from '@/components/ui';
import { createColumnFieldOptions } from './helpers';
const DEFAULT_METRIC_HEADER: Required<IBusterMetricChartConfig['metricHeader']> = {
columnId: '',
useValue: false,
aggregate: 'sum'
};
type NonNullableHeader =
| NonNullable<IBusterMetricChartConfig['metricHeader']>
| NonNullable<IBusterMetricChartConfig['metricSubHeader']>;
export const DerivedTitleInput: React.FC<{
type: 'header' | 'subHeader';
header: IBusterMetricChartConfig['metricHeader'] | IBusterMetricChartConfig['metricSubHeader'];
columnLabelFormat: IColumnLabelFormat;
columnLabelFormats: IBusterMetricChartConfig['columnLabelFormats'];
metricColumnId: IBusterMetricChartConfig['metricColumnId'];
columnMetadata: ColumnMetaData[];
onUpdateHeaderConfig: (newMetricHeader: IBusterMetricChartConfig['metricHeader']) => void;
}> = React.memo(
({
type,
header: headerProp,
columnLabelFormats,
columnLabelFormat,
metricColumnId,
columnMetadata,
onUpdateHeaderConfig
}) => {
const header = useMemo(() => {
const isStringHeader = typeof headerProp === 'string';
if (isStringHeader) return headerProp;
if (headerProp === null) {
return {
useValue: false,
columnId: metricColumnId,
aggregate: 'sum'
} as DerivedMetricTitle;
}
return headerProp;
}, [headerProp]);
const isStringHeader = typeof header === 'string';
const onUpdateHeader = useMemoizedFn(
(
newHeader:
| Partial<IBusterMetricChartConfig['metricHeader']>
| Partial<IBusterMetricChartConfig['metricSubHeader']>
) => {
if (typeof newHeader === 'string') {
return onUpdateHeaderConfig(newHeader);
} else {
if (typeof header === 'string') {
return onUpdateHeaderConfig({
...DEFAULT_METRIC_HEADER,
columnId: metricColumnId,
...newHeader
});
} else {
return onUpdateHeaderConfig({ ...DEFAULT_METRIC_HEADER, ...header, ...newHeader });
}
}
}
);
const value = useMemo(() => {
if (isStringHeader) {
return header;
}
}, [isStringHeader, header]);
const placeholder = useMemo(() => {
if (isStringHeader) {
return 'Type or link a value';
}
const { useValue, columnId, aggregate } = header;
const columnLabelFormat = columnLabelFormats[columnId];
let label = formatLabel(columnId, columnLabelFormat, true);
if (useValue && aggregate) {
const aggregateLabel =
AGGREGATE_OPTIONS.find(({ value }) => value === aggregate)?.label || '';
label = `${label} (${aggregateLabel})`;
}
return label;
}, [value]);
return (
<div className="relative">
<Input
className="w-full"
value={value}
disabled={false}
placeholder={placeholder}
onChange={(e) => {
onUpdateHeader(e.target.value);
}}
suffix={
<DerivedTitleSuffix
columnLabelFormat={columnLabelFormat}
header={header}
columnMetadata={columnMetadata}
metricColumnId={metricColumnId}
columnLabelFormats={columnLabelFormats}
type={type}
onUpdateHeader={onUpdateHeader}
/>
}
/>
</div>
);
}
);
DerivedTitleInput.displayName = 'DerivedTitleInput';
const DerivedTitleSuffix: React.FC<{
columnLabelFormat: IColumnLabelFormat;
columnLabelFormats: IBusterMetricChartConfig['columnLabelFormats'];
metricColumnId: IBusterMetricChartConfig['metricColumnId'];
columnMetadata: ColumnMetaData[];
header: NonNullableHeader;
onUpdateHeader: OnUpdateHeaderType;
type: 'header' | 'subHeader';
}> = ({
columnLabelFormat,
columnLabelFormats,
header,
onUpdateHeader,
metricColumnId,
columnMetadata,
type
}) => {
const isStringHeader = typeof header === 'string';
const buttonIcon = isStringHeader ? 'link_off' : 'link';
return (
<div className="flex" onClick={(e) => e.stopPropagation()}>
<AppPopover
placement="topLeft"
trigger="click"
destroyTooltipOnHide
content={
<DerivedTitleSuffixContent
columnLabelFormat={columnLabelFormat}
header={header}
metricColumnId={metricColumnId}
columnMetadata={columnMetadata}
columnLabelFormats={columnLabelFormats}
onUpdateHeader={onUpdateHeader}
type={type}
/>
}>
<Button className="h-[18px]!" type="text" icon={<AppMaterialIcons icon={buttonIcon} />} />
</AppPopover>
</div>
);
};
const DerivedTitleSuffixContent: React.FC<{
type: 'header' | 'subHeader';
columnLabelFormat: IColumnLabelFormat;
metricColumnId: IBusterMetricChartConfig['metricColumnId'];
columnMetadata: ColumnMetaData[];
columnLabelFormats: IBusterMetricChartConfig['columnLabelFormats'];
header: NonNullableHeader;
onUpdateHeader: OnUpdateHeaderType;
}> = ({ type, columnLabelFormats, header, onUpdateHeader, columnMetadata }) => {
const isStringHeader = typeof header === 'string';
const headerColumnId = typeof header === 'object' ? header.columnId : '';
const headerUseValue = typeof header === 'object' && header.useValue;
const ComponentsLoop: {
key: string;
enabled: boolean;
Component: React.ReactNode;
}[] = [
{
key: 'link',
enabled: true,
Component: (
<ToggleHeaderLink isStringHeader={isStringHeader} onUpdateHeader={onUpdateHeader} />
)
},
{
key: 'columnId',
enabled: !isStringHeader,
Component: (
<DerivedTitleColumnId
headerColumnId={headerColumnId}
onUpdateHeader={onUpdateHeader}
columnMetadata={columnMetadata}
columnLabelFormats={columnLabelFormats}
/>
)
},
{
key: 'useValue',
enabled: !isStringHeader,
Component: <ToggleUseColumnValue onUpdateHeader={onUpdateHeader} useValue={headerUseValue} />
},
{
key: 'aggregate',
enabled: !isStringHeader && headerUseValue,
Component: (
<DerivedTitleAggregate
onUpdateHeader={onUpdateHeader}
aggregate={typeof header === 'object' && header.aggregate ? header.aggregate : 'sum'}
columnLabelFormat={columnLabelFormats[headerColumnId]}
/>
)
}
];
return (
<div className="flex w-[285px] max-w-[285px] flex-col">
<DerivedTitleSuffixContentHeader type={type} />
<Divider />
<div className="flex flex-col space-y-2 p-3">
{ComponentsLoop.map(({ enabled, key, Component }) => {
if (!enabled) return null;
return <React.Fragment key={key}>{Component}</React.Fragment>;
})}
</div>
</div>
);
};
const DerivedTitleSuffixContentHeader: React.FC<{
type: 'header' | 'subHeader';
}> = React.memo(
({ type }) => {
const title = type === 'header' ? 'Header settings' : 'Sub-header settings';
return (
<div className="p-3">
<Text>{title}</Text>
</div>
);
},
() => true
);
DerivedTitleSuffixContentHeader.displayName = 'DerivedTitleSuffixContentHeader';
const ToggleUseColumnValue: React.FC<{
onUpdateHeader: OnUpdateHeaderType;
useValue: boolean;
}> = React.memo(({ onUpdateHeader, useValue }) => {
return (
<LabelAndInput label="Use column value">
<div className="flex justify-end">
<Switch checked={useValue} onChange={(v) => onUpdateHeader({ useValue: v })} />
</div>
</LabelAndInput>
);
});
ToggleUseColumnValue.displayName = 'ToggleUseColumnValue';
const ToggleHeaderLink: React.FC<{
isStringHeader: boolean;
onUpdateHeader: OnUpdateHeaderType;
}> = React.memo(({ isStringHeader, onUpdateHeader }) => {
return (
<LabelAndInput label="Use column">
<div className="flex justify-end">
<Switch
checked={!isStringHeader}
onChange={(v) => {
onUpdateHeader(v ? {} : '');
}}
/>
</div>
</LabelAndInput>
);
});
ToggleHeaderLink.displayName = 'ToggleHeaderLink';
const DerivedTitleColumnId: React.FC<{
headerColumnId: IBusterMetricChartConfig['metricColumnId'];
onUpdateHeader: OnUpdateHeaderType;
columnMetadata: ColumnMetaData[];
columnLabelFormats: IBusterMetricChartConfig['columnLabelFormats'];
}> = React.memo(({ headerColumnId, onUpdateHeader, columnMetadata, columnLabelFormats }) => {
const { styles } = useStyles();
const columnOptions = useMemo(() => {
return createColumnFieldOptions(columnMetadata, columnLabelFormats, styles.icon);
}, [columnMetadata, columnLabelFormats, styles.icon]);
const selectedColumn = useMemo(() => {
return columnOptions.find((option) => option.value === headerColumnId);
}, [headerColumnId, columnOptions]);
const onChangeSelect = useMemoizedFn((v: string) => {
const columnLabelFormat = columnLabelFormats[v];
const isNumberColumn = isNumericColumnType(columnLabelFormat?.columnType);
const isNumericStyle = isNumericColumnStyle(columnLabelFormat?.style);
const newHeader: Partial<IBusterMetricChartConfig['metricHeader']> = { columnId: v };
if (!isNumberColumn || !isNumericStyle) {
newHeader.aggregate = 'first';
}
onUpdateHeader(newHeader);
});
return (
<LabelAndInput label="Column ID">
<Select
className="w-full overflow-hidden"
options={columnOptions}
value={selectedColumn?.value}
onChange={onChangeSelect}
/>
</LabelAndInput>
);
});
DerivedTitleColumnId.displayName = 'DerivedTitleColumnId';
const DerivedTitleAggregate: React.FC<{
onUpdateHeader: OnUpdateHeaderType;
aggregate: Required<DerivedMetricTitle>['aggregate'];
columnLabelFormat: IColumnLabelFormat;
}> = React.memo(({ onUpdateHeader, aggregate, columnLabelFormat }) => {
const isNumberColumn = isNumericColumnType(columnLabelFormat?.columnType);
const isNumericStyle = isNumericColumnStyle(columnLabelFormat?.style);
const disableOptions = !isNumberColumn || !isNumericStyle;
const selectedOption = useMemo(() => {
if (!disableOptions) {
return AGGREGATE_OPTIONS.find((option) => option.value === aggregate)?.value;
}
return last(AGGREGATE_OPTIONS)?.value;
}, [aggregate, disableOptions]);
return (
<LabelAndInput label="Aggregate">
<Select
options={AGGREGATE_OPTIONS}
value={selectedOption}
disabled={disableOptions}
onChange={(v) =>
onUpdateHeader({ aggregate: v as Required<DerivedMetricTitle>['aggregate'] })
}
/>
</LabelAndInput>
);
});
DerivedTitleAggregate.displayName = 'DerivedTitleAggregate';
type OnUpdateHeaderType = (
header:
| Partial<IBusterMetricChartConfig['metricHeader']>
| Partial<IBusterMetricChartConfig['metricSubHeader']>
) => void;
const useStyles = createStyles(({ token }) => ({
icon: token.colorIcon
}));

View File

@ -1,5 +1,5 @@
import { IBusterMetricChartConfig } from '@/api/asset_interfaces';
import { isNumericColumnStyle, isNumericColumnType } from '@/lib';
import { isNumericColumnStyle, isNumericColumnType } from '@/lib/messages';
import React, { useMemo } from 'react';
import { LabelAndInput } from '../../Common';
import { Button, Select } from 'antd';

View File

@ -1,44 +0,0 @@
import React from 'react';
import { LabelAndInput } from '../../Common';
import type { ColumnMetaData, IBusterMetricChartConfig } from '@/api/asset_interfaces';
import { useMemoizedFn } from 'ahooks';
import { DerivedTitleInput } from './EditDerivedHeader';
export const EditMetricSubHeader: React.FC<{
metricSubHeader: IBusterMetricChartConfig['metricSubHeader'];
columnLabelFormats: IBusterMetricChartConfig['columnLabelFormats'];
metricColumnId: IBusterMetricChartConfig['metricColumnId'];
columnMetadata: ColumnMetaData[];
onUpdateChartConfig: (chartConfig: Partial<IBusterMetricChartConfig>) => void;
}> = React.memo(
({
metricSubHeader,
columnMetadata,
columnLabelFormats,
metricColumnId,
onUpdateChartConfig
}) => {
const columnLabelFormat = columnLabelFormats[metricColumnId];
const onUpdateMetricHeader = useMemoizedFn(
(newMetricSubHeader: IBusterMetricChartConfig['metricSubHeader']) => {
onUpdateChartConfig({ metricSubHeader: newMetricSubHeader });
}
);
return (
<LabelAndInput label={'Sub-header'}>
<DerivedTitleInput
type="subHeader"
header={metricSubHeader}
columnLabelFormat={columnLabelFormat}
metricColumnId={metricColumnId}
columnMetadata={columnMetadata}
columnLabelFormats={columnLabelFormats}
onUpdateHeaderConfig={onUpdateMetricHeader}
/>
</LabelAndInput>
);
}
);
EditMetricSubHeader.displayName = 'EditMetricSubHeader';

View File

@ -6,11 +6,9 @@ import last from 'lodash/last';
import { useMemoizedFn } from 'ahooks';
import { isNumericColumnStyle, isNumericColumnType } from '@/lib';
import { ColumnLabelFormat } from '@/components/ui/charts';
import { SelectItem } from '@/components/ui/select';
export const AGGREGATE_OPTIONS: {
label: string;
value: IBusterMetricChartConfig['metricValueAggregate'];
}[] = [
export const AGGREGATE_OPTIONS: SelectItem<IBusterMetricChartConfig['metricValueAggregate']>[] = [
{ label: 'Sum', value: 'sum' },
{ label: 'Average', value: 'average' },
{ label: 'Median', value: 'median' },

View File

@ -1,24 +1,21 @@
import { ColumnMetaData, IBusterMetricChartConfig } from '@/api/asset_interfaces';
import { formatLabel } from '@/lib';
import { ColumnTypeIcon } from '../SelectAxis/config';
import { type SelectItem } from '@/components/ui/select';
export const createColumnFieldOptions = (
columnMetadata: ColumnMetaData[],
columnLabelFormats: IBusterMetricChartConfig['columnLabelFormats'],
iconClass: string
) => {
return columnMetadata.map((column) => {
): SelectItem[] => {
return columnMetadata.map<SelectItem>((column) => {
const labelFormat = columnLabelFormats[column.name];
const formattedLabel = formatLabel(column.name, labelFormat, true);
const Icon = ColumnTypeIcon[labelFormat.style];
return {
label: (
<div className="flex w-full items-center space-x-1.5 overflow-hidden">
<div className={`${iconClass} flex`}>{Icon.icon}</div>
<span className="truncate">{formattedLabel}</span>
</div>
),
icon: Icon.icon,
label: formattedLabel,
value: column.name
};
});