[improvement][semantic-fe] enhance the analysis of metric trends (#234)

* [improvement][semantic-fe] Add model alias setting & Add view permission restrictions to the model permission management tab.
[improvement][semantic-fe] Add permission control to the action buttons for the main domain; apply high sensitivity filtering to the authorization of metrics/dimensions.
[improvement][semantic-fe] Optimize the editing mode in the dimension/metric/datasource components to use the modelId stored in the database for data, instead of relying on the data from the state manager.

* [improvement][semantic-fe] Add time granularity setting in the data source configuration.

* [improvement][semantic-fe] Dictionary import for dimension values supported in Q&A visibility

* [improvement][semantic-fe] Modification of data source creation prompt wording"

* [improvement][semantic-fe] metric market experience optimization

* [improvement][semantic-fe] enhance the analysis of metric trends
This commit is contained in:
tristanliu
2023-10-16 06:10:37 -05:00
committed by GitHub
parent 37bb9ff767
commit c5536aa25d
23 changed files with 2997 additions and 173 deletions

View File

@@ -0,0 +1,134 @@
import { CheckCard } from '@ant-design/pro-components';
import React, { useState } from 'react';
import { Dropdown, Popconfirm, Typography } from 'antd';
import { EllipsisOutlined } from '@ant-design/icons';
import { ISemantic } from '../../data';
import { connect } from 'umi';
import icon from '../../../../assets/icon/sourceState.svg';
import type { Dispatch } from 'umi';
import type { StateType } from '../../model';
import { SemanticNodeType } from '../../enum';
import styles from '../style.less';
const { Paragraph } = Typography;
type Props = {
disabledEdit?: boolean;
metricList: ISemantic.IMetricItem[];
onMetricChange?: (metricItem: ISemantic.IMetricItem) => void;
onEditBtnClick?: (metricItem: ISemantic.IMetricItem) => void;
onDeleteBtnClick?: (metricItem: ISemantic.IMetricItem) => void;
domainManger: StateType;
dispatch: Dispatch;
};
const MetricCardList: React.FC<Props> = ({
metricList,
disabledEdit = false,
onMetricChange,
onEditBtnClick,
onDeleteBtnClick,
domainManger,
}) => {
const [currentNodeData, setCurrentNodeData] = useState<any>({});
const descNode = (metricItem: ISemantic.IMetricItem) => {
const { modelName, createdBy } = metricItem;
return (
<>
<div className={styles.overviewExtraContainer}>
<div className={styles.extraWrapper}>
<div className={styles.extraStatistic}>
<div className={styles.extraTitle}>:</div>
<div className={styles.extraValue}>
<Paragraph style={{ maxWidth: 70, margin: 0 }} ellipsis={{ tooltip: modelName }}>
<span>{modelName}</span>
</Paragraph>
</div>
</div>
</div>
<div className={styles.extraWrapper}>
<div className={styles.extraStatistic}>
<div className={styles.extraTitle}>:</div>
<div className={styles.extraValue}>
<Paragraph style={{ maxWidth: 70, margin: 0 }} ellipsis={{ tooltip: createdBy }}>
<span>{createdBy}</span>
</Paragraph>
</div>
</div>
</div>
</div>
</>
);
};
const extraNode = (metricItem: ISemantic.IMetricItem) => {
return (
<Dropdown
placement="top"
menu={{
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
if (key === 'edit') {
onEditBtnClick?.(metricItem);
}
},
items: [
{
label: '编辑',
key: 'edit',
},
{
label: (
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={() => {
onDeleteBtnClick?.(metricItem);
}}
>
<a key="modelDeleteBtn"></a>
</Popconfirm>
),
key: 'delete',
},
],
}}
>
<EllipsisOutlined
style={{ fontSize: 22, color: 'rgba(0,0,0,0.5)' }}
onClick={(e) => e.stopPropagation()}
/>
</Dropdown>
);
};
return (
<div style={{ padding: '0px 20px 20px' }}>
<CheckCard.Group value={currentNodeData.id} defaultValue={undefined}>
{metricList &&
metricList.map((metricItem: ISemantic.IMetricItem) => {
return (
<CheckCard
style={{ width: 350 }}
avatar={icon}
title={`${metricItem.name}`}
key={metricItem.id}
value={metricItem.id}
description={descNode(metricItem)}
extra={!disabledEdit && extraNode(metricItem)}
onClick={() => {
setCurrentNodeData({ ...metricItem, nodeType: SemanticNodeType.METRIC });
onMetricChange?.(metricItem);
}}
/>
);
})}
</CheckCard.Group>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(MetricCardList);

View File

@@ -1,4 +1,4 @@
import { Form, Input, Space, Row, Col } from 'antd';
import { Form, Input, Space, Row, Col, Switch } from 'antd';
import StandardFormRow from '@/components/StandardFormRow';
import TagSelect from '@/components/TagSelect';
import React, { useEffect } from 'react';
@@ -10,20 +10,21 @@ import styles from '../style.less';
const FormItem = Form.Item;
type Props = {
filterValues?: any;
initFilterValues?: any;
onFiltersChange: (_: any, values: any) => void;
};
const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) => {
const MetricFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange }) => {
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue({
...filterValues,
...initFilterValues,
});
}, [form, filterValues]);
}, [form]);
const handleValuesChange = (value: any, values: any) => {
localStorage.setItem('metricMarketShowType', !!values.showType ? '1' : '0');
onFiltersChange(value, values);
};
@@ -32,17 +33,6 @@ const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) =
};
const filterList = [
// {
// title: '指标类型',
// key: 'type',
// options: [
// {
// value: 'ATOMIC',
// label: '原子指标',
// },
// { value: 'DERIVED', label: '衍生指标' },
// ],
// },
{
title: '敏感度',
key: 'sensitiveLevel',
@@ -94,6 +84,11 @@ const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) =
</div>
</StandardFormRow>
<Space size={80}>
<StandardFormRow key="showType" title="切换为卡片" block>
<FormItem name="showType" valuePropName="checked">
<Switch size="small" />
</FormItem>
</StandardFormRow>
<StandardFormRow key="domainIds" title="所属主题域" block>
<FormItem name="domainIds">
<DomainTreeSelect />
@@ -103,17 +98,15 @@ const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) =
const { title, key, options } = item;
return (
<StandardFormRow key={key} title={title} block>
<div style={{ marginLeft: -30 }}>
<FormItem name={key}>
<TagSelect reverseCheckAll single>
{options.map((item: any) => (
<TagSelect.Option key={item.value} value={item.value}>
{item.label}
</TagSelect.Option>
))}
</TagSelect>
</FormItem>
</div>
<FormItem name={key}>
<TagSelect reverseCheckAll single>
{options.map((item: any) => (
<TagSelect.Option key={item.value} value={item.value}>
{item.label}
</TagSelect.Option>
))}
</TagSelect>
</FormItem>
</StandardFormRow>
);
})}

View File

@@ -0,0 +1,217 @@
import { CHART_SECONDARY_COLOR } from '@/common/constants';
import {
formatByDecimalPlaces,
formatByPercentageData,
getFormattedValueData,
} from '@/utils/utils';
import { Skeleton, Button, Tooltip } from 'antd';
import { DownloadOutlined } from '@ant-design/icons';
import type { ECharts } from 'echarts';
import * as echarts from 'echarts';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import styles from '../style.less';
import moment from 'moment';
type Props = {
title?: string;
tip?: string;
data: any[];
fields: any[];
// columnFieldName: string;
// valueFieldName: string;
loading: boolean;
isPer?: boolean;
isPercent?: boolean;
dateFieldName?: string;
dateFormat?: string;
height?: number;
renderType?: string;
decimalPlaces?: number;
onDownload?: () => void;
};
const TrendChart: React.FC<Props> = ({
title,
tip,
data,
fields,
loading,
isPer,
isPercent,
dateFieldName,
// columnFieldName,
// valueFieldName,
dateFormat,
height,
renderType,
decimalPlaces,
onDownload,
}) => {
const chartRef = useRef<any>();
const [instance, setInstance] = useState<ECharts>();
const renderChart = useCallback(() => {
let instanceObj: ECharts;
if (!instance) {
instanceObj = echarts.init(chartRef.current);
setInstance(instanceObj);
} else {
instanceObj = instance;
if (renderType === 'clear') {
instanceObj.clear();
}
}
const xData = Array.from(
new Set(
data
.map((item) =>
moment(`${(dateFieldName && item[dateFieldName]) || item.sys_imp_date}`).format(
dateFormat ?? 'YYYY-MM-DD',
),
)
.sort((a, b) => {
return moment(a).valueOf() - moment(b).valueOf();
}),
),
);
const seriesData = fields.map((field) => {
const fieldData = {
type: 'line',
name: field.name,
symbol: 'circle',
showSymbol: data.length === 1,
smooth: true,
data: data.reduce((itemData, item) => {
const target = item[field.column];
if (target) {
itemData.push(target);
}
return itemData;
}, []),
};
return fieldData;
});
instanceObj.setOption({
legend: {
left: 0,
top: 0,
icon: 'rect',
itemWidth: 15,
itemHeight: 5,
selected: fields.reduce((result, item) => {
if (item.selected === false) {
result[item.name] = false;
}
return result;
}, {}),
},
xAxis: {
type: 'category',
axisTick: {
alignWithLabel: true,
lineStyle: {
color: CHART_SECONDARY_COLOR,
},
},
axisLine: {
lineStyle: {
color: CHART_SECONDARY_COLOR,
},
},
axisLabel: {
showMaxLabel: true,
color: '#999',
},
data: xData,
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
opacity: 0.3,
},
},
axisLabel: {
formatter: function (value: any) {
return value === 0
? 0
: isPer
? `${formatByDecimalPlaces(value, decimalPlaces ?? 0)}%`
: isPercent
? formatByPercentageData(value, decimalPlaces ?? 0)
: getFormattedValueData(value);
},
},
},
tooltip: {
trigger: 'axis',
formatter: function (params: any[]) {
const param = params[0];
const valueLabels = params
.map(
(item: any) =>
`<div style="margin-top: 3px;">${
item.marker
} <span style="display: inline-block; width: 70px; margin-right: 5px;">${
item.seriesName
}</span><span style="display: inline-block; width: 90px; text-align: right; font-weight: 500;">${
item.value === ''
? '-'
: isPer
? `${formatByDecimalPlaces(item.value, decimalPlaces ?? 2)}%`
: isPercent
? formatByPercentageData(item.value, decimalPlaces ?? 2)
: getFormattedValueData(item.value)
}</span></div>`,
)
.join('');
return `${param.name}<br />${valueLabels}`;
},
},
grid: {
left: '1%',
right: '4%',
bottom: '3%',
top: height && height < 300 ? 45 : 60,
containLabel: true,
},
series: seriesData,
});
instanceObj.resize();
}, [data, fields, instance, isPer, isPercent, dateFieldName, decimalPlaces, renderType]);
useEffect(() => {
if (!loading) {
renderChart();
}
}, [renderChart, loading, data]);
return (
<div className={styles.trendChart}>
{title && (
<div className={styles.top}>
<div className={styles.title}>{title}</div>
{onDownload && (
<Tooltip title="下载">
<Button shape="circle" className={styles.downloadBtn} onClick={onDownload}>
<DownloadOutlined />
</Button>
</Tooltip>
)}
</div>
)}
<Skeleton
className={styles.chart}
style={{ height, display: loading ? 'table' : 'none' }}
paragraph={{ rows: height && height > 300 ? 9 : 6 }}
/>
<div
className={styles.chart}
style={{ height, display: !loading ? 'block' : 'none' }}
ref={chartRef}
/>
</div>
);
};
export default TrendChart;

View File

@@ -0,0 +1,134 @@
import React, { useState, useEffect, useRef } from 'react';
import { SemanticNodeType } from '../../enum';
import moment from 'moment';
import { message } from 'antd';
import { queryStruct } from '@/pages/SemanticModel/service';
import TrendChart from '@/pages/SemanticModel/Metric/components/MetricTrend';
import MDatePicker from '@/components/MDatePicker';
import { DateRangeType, DateSettingType } from '@/components/MDatePicker/type';
import { ISemantic } from '../../data';
type Props = {
nodeData: any;
[key: string]: any;
};
const MetricTrendSection: React.FC<Props> = ({ nodeData }) => {
const dateFieldMap = {
[DateRangeType.DAY]: 'sys_imp_date',
[DateRangeType.WEEK]: 'sys_imp_week',
[DateRangeType.MONTH]: 'sys_imp_month',
};
const indicatorFields = useRef<{ name: string; column: string }[]>([]);
const [metricTrendData, setMetricTrendData] = useState<ISemantic.IMetricTrendItem[]>([]);
const [metricTrendLoading, setMetricTrendLoading] = useState<boolean>(false);
const [metricColumnConfig, setMetricColumnConfig] = useState<ISemantic.IMetricTrendColumn>();
const [authMessage, setAuthMessage] = useState<string>('');
const [periodDate, setPeriodDate] = useState<{
startDate: string;
endDate: string;
dateField: string;
}>({
startDate: moment().subtract('7', 'days').format('YYYY-MM-DD'),
endDate: moment().format('YYYY-MM-DD'),
dateField: dateFieldMap[DateRangeType.DAY],
});
const getMetricTrendData = async () => {
setMetricTrendLoading(true);
const { modelId, bizName, name } = nodeData;
indicatorFields.current = [{ name, column: bizName }];
const { code, data, msg } = await queryStruct({
modelId,
bizName,
dateField: periodDate.dateField,
startDate: periodDate.startDate,
endDate: periodDate.endDate,
});
setMetricTrendLoading(false);
if (code === 200) {
const { resultList, columns, queryAuthorization } = data;
setMetricTrendData(resultList);
const message = queryAuthorization?.message;
if (message) {
setAuthMessage(message);
}
const targetConfig = columns.find((item: ISemantic.IMetricTrendColumn) => {
return item.nameEn === bizName;
});
if (targetConfig) {
setMetricColumnConfig(targetConfig);
}
} else {
message.error(msg);
setMetricTrendData([]);
setMetricColumnConfig(undefined);
}
};
useEffect(() => {
if (nodeData.id && nodeData?.nodeType === SemanticNodeType.METRIC) {
getMetricTrendData();
}
}, [nodeData, periodDate]);
return (
<>
<div style={{ marginBottom: 5 }}>
<MDatePicker
initialValues={{
dateSettingType: 'DYNAMIC',
dynamicParams: {
number: 7,
periodType: 'DAYS',
includesCurrentPeriod: true,
shortCutId: 'last7Days',
dateRangeType: 'DAY',
dynamicAdvancedConfigType: 'last',
dateRangeStringDesc: '最近7天',
dateSettingType: DateSettingType.DYNAMIC,
},
staticParams: {},
}}
onDateRangeChange={(value, config) => {
const [startDate, endDate] = value;
const { dateSettingType, dynamicParams, staticParams } = config;
let dateField = dateFieldMap[DateRangeType.DAY];
if (DateSettingType.DYNAMIC === dateSettingType) {
dateField = dateFieldMap[dynamicParams.dateRangeType];
}
if (DateSettingType.STATIC === dateSettingType) {
dateField = dateFieldMap[staticParams.dateRangeType];
}
setPeriodDate({ startDate, endDate, dateField });
}}
disabledAdvanceSetting={true}
/>
</div>
<div style={{ color: '#d46b08', marginBottom: 15 }}>: {authMessage}</div>
<TrendChart
data={metricTrendData}
isPer={
metricColumnConfig?.dataFormatType === 'percent' &&
metricColumnConfig?.dataFormat?.needMultiply100 === false
? true
: false
}
isPercent={
metricColumnConfig?.dataFormatType === 'percent' &&
metricColumnConfig?.dataFormat?.needMultiply100 === true
? true
: false
}
fields={indicatorFields.current}
loading={metricTrendLoading}
dateFieldName={periodDate.dateField}
height={350}
renderType="clear"
decimalPlaces={metricColumnConfig?.dataFormat?.decimalPlaces || 2}
/>
</>
);
};
export default MetricTrendSection;

View File

@@ -1,6 +1,6 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Space, Popconfirm, Tag } from 'antd';
import { message, Space, Popconfirm, Tag, Spin } from 'antd';
import React, { useRef, useState, useEffect } from 'react';
import type { Dispatch } from 'umi';
import { connect, history } from 'umi';
@@ -9,9 +9,12 @@ import { SENSITIVE_LEVEL_ENUM } from '../constant';
import { queryMetric, deleteMetric } from '../service';
import MetricFilter from './components/MetricFilter';
import MetricInfoCreateForm from '../components/MetricInfoCreateForm';
import MetricCardList from './components/MetricCardList';
import NodeInfoDrawer from '../SemanticGraph/components/NodeInfoDrawer';
import { SemanticNodeType } from '../enum';
import moment from 'moment';
import styles from './style.less';
import { IDataSource, ISemantic } from '../data';
import { ISemantic } from '../data';
type Props = {
dispatch: Dispatch;
@@ -30,19 +33,23 @@ type QueryMetricListParams = {
const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const { selectDomainId, selectModelId: modelId } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [pagination, setPagination] = useState({
const defaultPagination = {
current: 1,
pageSize: 20,
total: 0,
});
};
const [pagination, setPagination] = useState(defaultPagination);
const [loading, setLoading] = useState<boolean>(false);
const [dataSource, setDataSource] = useState<IDataSource.IDataSourceItem[]>([]);
const [dataSource, setDataSource] = useState<ISemantic.IMetricItem[]>([]);
const [metricItem, setMetricItem] = useState<ISemantic.IMetricItem>();
const [filterParams, setFilterParams] = useState<Record<string, any>>({});
const [filterParams, setFilterParams] = useState<Record<string, any>>({
showType: localStorage.getItem('metricMarketShowType') === '1' ? true : false,
});
const [infoDrawerVisible, setInfoDrawerVisible] = useState<boolean>(false);
const actionRef = useRef<ActionType>();
useEffect(() => {
queryMetricList();
queryMetricList(filterParams);
}, []);
const queryMetricList = async (params: QueryMetricListParams = {}, disabledLoading = false) => {
@@ -52,10 +59,9 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const { code, data, msg } = await queryMetric({
...pagination,
...params,
pageSize: params.showType ? 100 : defaultPagination.pageSize,
});
if (!disabledLoading) {
setLoading(false);
}
setLoading(false);
const { list, pageSize, current, total } = data || {};
let resData: any = {};
if (code === 200) {
@@ -81,6 +87,21 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
return resData;
};
const deleteMetricQuery = async (id: number) => {
const { code, msg } = await deleteMetric(id);
if (code === 200) {
setMetricItem(undefined);
queryMetricList(filterParams);
} else {
message.error(msg);
}
};
const handleMetricEdit = (metricItem: ISemantic.IMetricItem) => {
setMetricItem(metricItem);
setCreateModalVisible(true);
};
const columns: ProColumns[] = [
{
dataIndex: 'id',
@@ -90,18 +111,16 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
dataIndex: 'name',
title: '指标名称',
render: (_, record: any) => {
if (record.hasAdminRes) {
return (
<a
onClick={() => {
history.replace(`/model/${record.domainId}/${record.modelId}/metric`);
}}
>
{record.name}
</a>
);
}
return <> {record.name}</>;
return (
<a
onClick={() => {
setMetricItem(record);
setInfoDrawerVisible(true);
}}
>
{record.name}
</a>
);
},
},
// {
@@ -116,6 +135,20 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
{
dataIndex: 'modelName',
title: '所属模型',
render: (_, record: any) => {
if (record.hasAdminRes) {
return (
<a
onClick={() => {
history.replace(`/model/${record.domainId}/${record.modelId}/metric`);
}}
>
{record.modelName}
</a>
);
}
return <> {record.modelName}</>;
},
},
{
dataIndex: 'sensitiveLevel',
@@ -179,27 +212,13 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
<a
key="metricEditBtn"
onClick={() => {
setMetricItem(record);
setCreateModalVisible(true);
handleMetricEdit(record);
}}
>
</a>
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={async () => {
const { code, msg } = await deleteMetric(record.id);
if (code === 200) {
setMetricItem(undefined);
queryMetricList();
} else {
message.error(msg);
}
}}
>
<Popconfirm title="确认删除?" okText="是" cancelText="否" onConfirm={() => {}}>
<a
key="metricDeleteBtn"
onClick={() => {
@@ -238,36 +257,64 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
<>
<div className={styles.metricFilterWrapper}>
<MetricFilter
initFilterValues={filterParams}
onFiltersChange={(_, values) => {
if (_.showType !== undefined) {
setLoading(true);
setDataSource([]);
}
handleFilterChange(values);
}}
/>
</div>
<ProTable
className={`${styles.metricTable}`}
actionRef={actionRef}
rowKey="id"
search={false}
dataSource={dataSource}
columns={columns}
pagination={pagination}
tableAlertRender={() => {
return false;
}}
loading={loading}
onChange={(data: any) => {
const { current, pageSize, total } = data;
const pagin = {
current,
pageSize,
total,
};
setPagination(pagin);
queryMetricList({ ...pagin, ...filterParams });
}}
size="small"
options={{ reload: false, density: false, fullScreen: false }}
/>
<>
{filterParams.showType ? (
<Spin spinning={loading} style={{ minHeight: 500 }}>
<MetricCardList
metricList={dataSource}
disabledEdit={true}
onMetricChange={(metricItem) => {
setInfoDrawerVisible(true);
setMetricItem(metricItem);
}}
onDeleteBtnClick={(metricItem) => {
deleteMetricQuery(metricItem.id);
}}
onEditBtnClick={(metricItem) => {
setMetricItem(metricItem);
setCreateModalVisible(true);
}}
/>
</Spin>
) : (
<ProTable
className={`${styles.metricTable}`}
actionRef={actionRef}
rowKey="id"
search={false}
dataSource={dataSource}
columns={columns}
pagination={pagination}
tableAlertRender={() => {
return false;
}}
loading={loading}
onChange={(data: any) => {
const { current, pageSize, total } = data;
const pagin = {
current,
pageSize,
total,
};
setPagination(pagin);
queryMetricList({ ...pagin, ...filterParams });
}}
size="small"
options={{ reload: false, density: false, fullScreen: false }}
/>
)}
</>
{createModalVisible && (
<MetricInfoCreateForm
domainId={Number(selectDomainId)}
@@ -276,7 +323,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
metricItem={metricItem}
onSubmit={() => {
setCreateModalVisible(false);
queryMetricList();
queryMetricList(filterParams);
dispatch({
type: 'domainManger/queryMetricList',
payload: {
@@ -289,6 +336,26 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
}}
/>
)}
{infoDrawerVisible && (
<NodeInfoDrawer
nodeData={{ ...metricItem, nodeType: SemanticNodeType.METRIC }}
placement="right"
onClose={() => {
setInfoDrawerVisible(false);
}}
width="100%"
open={infoDrawerVisible}
mask={true}
getContainer={false}
onEditBtnClick={(nodeData: any) => {
handleMetricEdit(nodeData);
}}
maskClosable={true}
onNodeChange={({ eventName }: { eventName: string }) => {
setInfoDrawerVisible(false);
}}
/>
)}
</>
);
};

View File

@@ -80,4 +80,38 @@
}
}
}
}
.overviewExtraContainer {
display: flex;
font-size: 14px;
.extraWrapper {
display: flex;
width: 100%;
.extraStatistic {
display: inline-flex;
color: rgba(42, 46, 54, 0.65);
box-sizing: border-box;
margin: 0;
padding: 0;
font-size: 14px;
line-height: 1.5714285714285714;
list-style: none;
.extraTitle {
font-size: 12px;
min-width: 50px;
margin-inline-end: 6px;
margin-block-end: 0;
margin-bottom: 4px;
color: rgba(42, 46, 54, 0.45);
}
.extraValue {
font-size: 12px;
color: rgba(42, 46, 54, 0.65);
display: inline-block;
direction: ltr;
}
}
}
}