[improvement][headless-fe] Added view management functionality. (#701)

* [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

* [improvement][semantic-fe] optimize the presentation of metric trend permissions

* [improvement][semantic-fe] add metric trend download functionality

* [improvement][semantic-fe] fix the dimension initialization issue in metric correlation

* [improvement][semantic-fe] Fix the issue of database changes not taking effect when creating based on an SQL data source.

* [improvement][semantic-fe] Optimizing pagination logic and some CSS styles

* [improvement][semantic-fe] Fixing the API for the indicator list by changing "current" to "pageNum"

* [improvement][semantic-fe] Fixing the default value setting for the indicator list

* [improvement][semantic-fe] Adding batch operations for indicators/dimensions/models

* [improvement][semantic-fe] Replacing the single status update API for indicators/dimensions with a batch update API

* [improvement][semantic-fe] Redesigning the indicator homepage to incorporate trend charts and table functionality for indicators

* [improvement][semantic-fe] Optimizing the logic for setting dimension values and editing data sources, and adding system settings functionality

* [improvement][semantic-fe] Upgrading antd version to 5.x, extracting the batch operation button component, optimizing the interaction for system settings, and expanding the configuration generation types for list-to-select component.

* [improvement][semantic-fe] Adding the ability to filter dimensions based on whether they are tags or not.

* [improvement][semantic-fe] Adding the ability to edit relationships between models in the canvas.

* [improvement][semantic-fe] Updating the datePicker component to use dayjs instead.

* [improvement][semantic-fe] Fixing the issue with passing the model ID for dimensions in the indicator market.

* [improvement][semantic-fe] Fixing the abnormal state of the popup when creating a model.

* [improvement][semantic-fe] Adding permission logic for bulk operations in the indicator market.

* [improvement][semantic-fe] Adding the ability to download and transpose data.

* [improvement][semantic-fe] Fixing the initialization issue with the date selection component in the indicator details page when switching time granularity.

* [improvement][semantic-fe] Fixing the logic error in the dimension value setting.

* [improvement][semantic-fe] Fixing the synchronization issue with the question and answer settings information.

* [improvement][semantic-fe] Optimizing the canvas functionality for better performance and user experience.

* [improvement][semantic-fe] Optimizing the update process for drawing model relationship edges in the canvas.

* [improvement][semantic-fe] Changing the line type for canvas connections.

* [improvement][semantic-fe] Replacing the initialization variable from "semantic" to "headless".

* [improvement][semantic-fe] Fixing the missing migration issue for default drill-down dimension configuration in model editing. Additionally, optimizing the data retrieval method for initializing fields in the model.

* [improvement][semantic-fe] Updating the logic for the fieldName.

* [improvement][semantic-fe] Adjusting the position of the metrics tab.

* [improvement][semantic-fe] Changing the 字段名称 to 英文名称.

* [improvement][semantic-fe] Fix metric measurement deletion.

* [improvement][semantic-fe] UI optimization for metric details page.

* [improvement][semantic-fe] UI optimization for metric details page.

* [improvement][semantic-fe] UI adjustment for metric details page.

* [improvement][semantic-fe] The granularity field in the time type of model editing now supports setting it as empty.

* [improvement][semantic-fe] Added field type and metric type to the metric creation options.

* [improvement][semantic-fe] The organization structure selection feature has been added to the permission management.

* [improvement][semantic-fe] Improved user experience for the metric list.

* [improvement][semantic-fe] fix update the metric list.

* [improvement][headless-fe] Added view management functionality.
This commit is contained in:
tristanliu
2024-01-30 20:35:04 +08:00
committed by GitHub
parent 26aefceb04
commit 31f8c1df35
45 changed files with 2539 additions and 423 deletions

View File

@@ -40,13 +40,7 @@ const ROUTES = [
name: 'semanticModel',
envEnableList: [ENV_KEY.SEMANTIC],
},
{
path: '/database',
name: 'database',
hideInMenu: true,
component: './SemanticModel/components/Database/DatabaseTable',
envEnableList: [ENV_KEY.SEMANTIC],
},
{
path: '/metric',
name: 'metric',
@@ -85,6 +79,13 @@ const ROUTES = [
hideInMenu: true,
component: './Login',
},
{
path: '/database',
name: 'database',
// hideInMenu: true,
component: './SemanticModel/components/Database/DatabaseTable',
envEnableList: [ENV_KEY.SEMANTIC],
},
{
path: '/system',
name: 'system',

View File

@@ -0,0 +1,21 @@
import React, { useEffect, useRef } from 'react';
import { InputNumber } from 'antd';
const DisabledWheelNumberInput: React.FC<any> = ({ ...rest }) => {
const ref = useRef<any>(null);
useEffect(() => {
if (ref.current) {
ref.current.addEventListener('wheel', handleWheel);
}
}, []);
const handleWheel = (event) => {
event.stopPropagation();
event.preventDefault();
};
return <InputNumber ref={ref} {...rest} />;
};
export default DisabledWheelNumberInput;

View File

@@ -298,7 +298,7 @@ const DataSourceFieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSq
return (
<>
<Alert
{/* <Alert
style={{ marginBottom: '10px' }}
banner
message={
@@ -306,7 +306,7 @@ const DataSourceFieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSq
为了保障同一个模型下维度/指标列表唯一,消除歧义,若本模型下的多个数据源存在相同的字段名并且都勾选了快速创建,系统默认这些相同字段的指标维度是同一个,同时列表中将只显示第一次创建的指标/维度。
</div>
}
/>
/> */}
<Table<FieldItem>
dataSource={fields}
columns={columns}

View File

@@ -1,13 +1,15 @@
import { message } from 'antd';
import { message, Tabs, Button, Space } from 'antd';
import React, { useState, useEffect } from 'react';
import { getMetricData, getDimensionList, getDrillDownDimension } from '../service';
import { connect, useParams } from 'umi';
import { connect, useParams, history } from 'umi';
import type { StateType } from '../model';
import styles from './style.less';
import { ArrowLeftOutlined } from '@ant-design/icons';
import MetricTrendSection from '@/pages/SemanticModel/Metric/components/MetricTrendSection';
import { ISemantic } from '../data';
import DimensionAndMetricRelationModal from '../components/DimensionAndMetricRelationModal';
import MetricInfoSider from './MetricInfoSider';
import type { TabsProps } from 'antd';
type Props = Record<string, any>;
@@ -17,6 +19,9 @@ const MetricDetail: React.FC<Props> = () => {
const [metricRelationModalOpenState, setMetricRelationModalOpenState] = useState<boolean>(false);
const [metircData, setMetircData] = useState<ISemantic.IMetricItem>();
const [dimensionList, setDimensionList] = useState<ISemantic.IDimensionItem[]>([]);
const [drillDownDimension, setDrillDownDimension] = useState<ISemantic.IDrillDownDimensionItem[]>(
[],
);
const [relationDimensionOptions, setRelationDimensionOptions] = useState<
{ value: string; label: string; modelId: number }[]
>([]);
@@ -38,6 +43,7 @@ const MetricDetail: React.FC<Props> = () => {
const queryDrillDownDimension = async (metricId: number) => {
const { code, data, msg } = await getDrillDownDimension(metricId);
if (code === 200 && Array.isArray(data)) {
setDrillDownDimension(data);
const ids = data.map((item) => item.dimensionId);
queryDimensionList(ids);
return data;
@@ -70,15 +76,57 @@ const MetricDetail: React.FC<Props> = () => {
return [];
};
const tabItems: TabsProps['items'] = [
{
key: 'metricTrend',
label: '图表',
children: (
<MetricTrendSection
metircData={metircData}
relationDimensionOptions={relationDimensionOptions}
dimensionList={dimensionList}
/>
),
},
// {
// key: 'metricCaliberInput',
// label: '基础信息',
// children: <></>,
// },
// {
// key: 'metricDataRemark',
// label: '备注',
// children: <></>,
// },
];
return (
<>
<div className={styles.metricDetailWrapper}>
<div className={styles.metricDetail}>
<div className={styles.tabContainer}>
<MetricTrendSection
metircData={metircData}
relationDimensionOptions={relationDimensionOptions}
dimensionList={dimensionList}
<Tabs
defaultActiveKey="metricTrend"
items={tabItems}
tabBarExtraContent={{
right: (
<Button
size="middle"
type="link"
key="backListBtn"
onClick={() => {
history.push('/metric/market');
}}
>
<Space>
<ArrowLeftOutlined />
</Space>
</Button>
),
}}
size="large"
className={styles.metricDetailTab}
/>
</div>
<div className={styles.siderContainer}>
@@ -91,20 +139,20 @@ const MetricDetail: React.FC<Props> = () => {
/>
</div>
</div>
<DimensionAndMetricRelationModal
metricItem={metircData}
relationsInitialValue={drillDownDimension}
open={metricRelationModalOpenState}
onCancel={() => {
setMetricRelationModalOpenState(false);
}}
onSubmit={(relations) => {
queryMetricData(metricId);
queryDrillDownDimension(metricId);
setMetricRelationModalOpenState(false);
}}
/>
</div>
<DimensionAndMetricRelationModal
metricItem={metircData}
relationsInitialValue={metircData?.relateDimension?.drillDownDimensions}
open={metricRelationModalOpenState}
onCancel={() => {
setMetricRelationModalOpenState(false);
}}
onSubmit={(relations) => {
queryMetricData(metricId);
queryDrillDownDimension(metricId);
setMetricRelationModalOpenState(false);
}}
/>
</>
);
};

View File

@@ -21,7 +21,6 @@ import moment from 'moment';
import styles from './style.less';
import { ISemantic } from '../data';
import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
import MetricStar from './components/MetricStar';
import { ColumnsConfig } from '../components/MetricTableColumnRender';
type Props = {
@@ -161,13 +160,19 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
};
const columns: ProColumns[] = [
// {
// dataIndex: 'id',
// title: 'ID',
// },
{
dataIndex: 'id',
title: 'ID',
width: 80,
fixed: 'left',
search: false,
},
{
dataIndex: 'name',
title: '指标',
// width: '20%',
width: 280,
fixed: 'left',
render: ColumnsConfig.metricInfo.render,
},
{
@@ -190,16 +195,32 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
return <> {record.modelName}</>;
},
},
{
dataIndex: 'sensitiveLevel',
title: '敏感度',
width: 150,
valueEnum: SENSITIVE_LEVEL_ENUM,
render: ColumnsConfig.sensitiveLevel.render,
},
{
dataIndex: 'description',
title: '描述',
search: false,
width: 300,
render: ColumnsConfig.description.render,
},
{
dataIndex: 'status',
title: '状态',
width: 120,
width: 180,
search: false,
render: ColumnsConfig.state.render,
},
{
dataIndex: 'description',
title: '描述',
dataIndex: 'createdBy',
title: '创建人',
// width: 150,
search: false,
},
{
@@ -214,6 +235,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
title: '操作',
dataIndex: 'x',
valueType: 'option',
width: 180,
render: (_, record) => {
if (record.hasAdminRes) {
return (
@@ -334,7 +356,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
metricList={dataSource}
disabledEdit={true}
onMetricChange={(metricItem: ISemantic.IMetricItem) => {
history.push(`/metric/detail/${metricItem.modelId}/${metricItem.bizName}`);
history.push(`/metric/detail/${metricItem.id}`);
}}
onDeleteBtnClick={(metricItem: ISemantic.IMetricItem) => {
deleteMetricQuery(metricItem.id);
@@ -355,6 +377,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
columns={columns}
pagination={pagination}
size="large"
scroll={{ x: 1500 }}
tableAlertRender={() => {
return false;
}}

View File

@@ -1,4 +1,4 @@
import { Tag, Space, Tooltip } from 'antd';
import { Tag, Space, Tooltip, Typography } from 'antd';
import React from 'react';
import { connect } from 'umi';
import type { StateType } from '../model';
@@ -11,13 +11,15 @@ import {
PartitionOutlined,
PlusOutlined,
AreaChartOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import styles from './style.less';
import { isString } from 'lodash';
import { SENSITIVE_LEVEL_ENUM, SENSITIVE_LEVEL_COLOR } from '../constant';
import { ISemantic } from '../data';
import MetricStar from './components/MetricStar';
const { Text } = Typography;
type Props = {
metircData: ISemantic.IMetricItem;
domainManger: StateType;
@@ -40,7 +42,6 @@ const MetricInfoSider: React.FC<Props> = ({
<Space>
<MetricStar metricId={metircData?.id} initState={metircData?.isCollect} />
{metircData?.name}
{metircData?.alias && `[${metircData.alias}]`}
{metircData?.hasAdminRes && (
<span
className={styles.gotoMetricListIcon}
@@ -105,6 +106,44 @@ const MetricInfoSider: React.FC<Props> = ({
</Space>
</span>
</div>
{isArrayOfValues(metircData?.tags) && (
<div className={styles.item}>
<span className={styles.itemLable}>: </span>
<span className={styles.itemValue}>
<Space size={2} wrap>
{isString(metircData?.alias) &&
metircData?.alias.split(',').map((aliasName: string) => {
return (
<Tag
color="#eee"
key={aliasName}
style={{
borderRadius: 44,
maxWidth: 90,
minWidth: 40,
backgroundColor: 'rgba(18, 31, 67, 0.04)',
}}
>
<Text
style={{
maxWidth: 80,
color: 'rgb(95, 116, 141)',
textAlign: 'center',
fontSize: 12,
}}
ellipsis={{ tooltip: aliasName }}
>
{aliasName}
</Text>
</Tag>
);
})}
</Space>
</span>
</div>
)}
<div className={styles.item}>
<span className={styles.itemLable}>: </span>
<span className={styles.itemValue}>{metircData?.description}</span>

View File

@@ -98,11 +98,11 @@ const MetricFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange
</div>
</StandardFormRow>
<Space size={40}>
<StandardFormRow key="showType" title="切换为卡片" block>
{/* <StandardFormRow key="showType" title="切换为卡片" block>
<FormItem name="showType" valuePropName="checked">
<Switch size="small" />
</FormItem>
</StandardFormRow>
</StandardFormRow> */}
{/* <StandardFormRow key="onlyShowMe" title="仅显示我的" block>
<FormItem name="onlyShowMe" valuePropName="checked">
<Switch size="small" />

View File

@@ -5,15 +5,17 @@ import RemoteSelect, { RemoteSelectImperativeHandle } from '@/components/RemoteS
import { queryDimValue } from '@/pages/SemanticModel/service';
import { OperatorEnum } from '@/pages/SemanticModel/enum';
import { isString } from 'lodash';
import { isArrayOfValues } from '@/utils/utils';
const FormItem = Form.Item;
type Props = {
dimensionOptions: OptionsItem[];
dimensionOptions: (OptionsItem & { modelId: number })[];
modelId: number;
periodDate?: { startDate: string; endDate: string; dateField: string };
value?: FormData;
onChange?: (value: FormData) => void;
afterSolt?: React.ReactNode;
};
export type FormData = {
@@ -24,16 +26,15 @@ export type FormData = {
const MetricTrendDimensionFilter: React.FC<Props> = ({
dimensionOptions,
modelId,
value,
periodDate,
afterSolt,
onChange,
}) => {
const [form] = Form.useForm();
const dimensionValueSearchRef = useRef<RemoteSelectImperativeHandle>();
const queryParams = useRef<{ dimensionBizName?: string }>({});
const [formData, setFormData] = useState<FormData>({ operator: OperatorEnum.IN } as FormData);
useEffect(() => {
if (!value) {
return;
@@ -59,7 +60,10 @@ const MetricTrendDimensionFilter: React.FC<Props> = ({
return;
}
const { dimensionBizName } = queryParams.current;
const targetOptions = dimensionOptions.find((item) => item.value === dimensionBizName) || {};
const targetOptions = dimensionOptions.find((item) => item.value === dimensionBizName);
if (!targetOptions) {
return;
}
const { code, data } = await queryDimValue({
...queryParams.current,
value: searchValue,
@@ -95,7 +99,6 @@ const MetricTrendDimensionFilter: React.FC<Props> = ({
}}
onValuesChange={(value, values) => {
const { operator, dimensionValue } = values;
if (multipleValueOperator.includes(operator) && isString(dimensionValue)) {
const tempDimensionValue = [dimensionValue];
setFormData({ ...values, dimensionValue: tempDimensionValue });
@@ -152,12 +155,14 @@ const MetricTrendDimensionFilter: React.FC<Props> = ({
<Button
type="primary"
icon={<PlusOutlined />}
disabled={!(formData.dimensionBizName && isArrayOfValues(formData.dimensionValue))}
onClick={() => {
const formValues = form.getFieldsValue();
onChange?.(formValues);
}}
/>
</Tooltip>
{afterSolt}
</Space>
</Form>
);

View File

@@ -4,11 +4,12 @@ import MetricTrendDimensionFilter from './MetricTrendDimensionFilter';
import type { FormData } from './MetricTrendDimensionFilter';
type Props = {
dimensionOptions: OptionsItem[];
dimensionOptions: (OptionsItem & { modelId: number })[];
modelId: number;
value?: FormData;
periodDate?: { startDate: string; endDate: string; dateField: string };
onChange?: (value: FormData[]) => void;
afterSolt?: React.ReactNode;
};
type DimensionOptionsMapItem = {
@@ -20,6 +21,7 @@ const MetricTrendDimensionFilterContainer: React.FC<Props> = ({
dimensionOptions,
modelId,
periodDate,
afterSolt,
value,
onChange,
}) => {
@@ -56,6 +58,7 @@ const MetricTrendDimensionFilterContainer: React.FC<Props> = ({
setFilterData(data);
onChange?.(data);
}}
afterSolt={afterSolt}
/>
<Space size={8} wrap style={{ marginTop: 10 }}>
{filterData.map((item: FormData, index: number) => {

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { message, Row, Col, Button, Space, Select, Form, Tooltip } from 'antd';
import { queryStruct } from '@/pages/SemanticModel/service';
import { DownloadOutlined, PoweroffOutlined, ArrowLeftOutlined } from '@ant-design/icons';
import { DownloadOutlined, PoweroffOutlined, SearchOutlined } from '@ant-design/icons';
import TrendChart from '@/pages/SemanticModel/Metric/components/MetricTrend';
import MetricTrendDimensionFilterContainer from './MetricTrendDimensionFilterContainer';
import MDatePicker from '@/components/MDatePicker';
@@ -11,7 +11,6 @@ import StandardFormRow from '@/components/StandardFormRow';
import MetricTable from './Table';
import { ColumnConfig } from '../data';
import dayjs from 'dayjs';
import { history } from 'umi';
import { ISemantic } from '../../data';
import { DateFieldMap } from '@/pages/SemanticModel/constant';
import ProCard from '@ant-design/pro-card';
@@ -119,6 +118,9 @@ const MetricTrendSection: React.FC<Props> = ({
setMetricColumnConfig(targetConfig);
}
setDownloadBtnDisabledState(false);
if (dimensionGroup[dimensionGroup.length - 1]) {
setGroupByDimensionFieldName(dimensionGroup[dimensionGroup.length - 1]);
}
} else {
if (code === 401 || code === 400) {
setAuthMessage(msg);
@@ -135,7 +137,7 @@ const MetricTrendSection: React.FC<Props> = ({
if (metircData?.id) {
getMetricTrendData({ ...queryParams });
}
}, [metircData, periodDate]);
}, [metircData]);
return (
<div className={styles.metricTrendSection}>
@@ -186,8 +188,6 @@ const MetricTrendSection: React.FC<Props> = ({
onChange={(value) => {
const params = { ...queryParams, dimensionGroup: value || [] };
setQueryParams(params);
getMetricTrendData({ ...params });
setGroupByDimensionFieldName(value[value.length - 1]);
}}
/>
</FormItem>
@@ -212,8 +212,20 @@ const MetricTrendSection: React.FC<Props> = ({
dimensionFilters,
};
setQueryParams(params);
getMetricTrendData({ ...params });
}}
afterSolt={
<Button
type="primary"
icon={<SearchOutlined />}
size="middle"
loading={metricTrendLoading}
onClick={() => {
getMetricTrendData({ ...queryParams });
}}
>
</Button>
}
/>
</FormItem>
</StandardFormRow>
@@ -221,31 +233,21 @@ const MetricTrendSection: React.FC<Props> = ({
</Col>
<Col flex="0 1" />
</Row>
<Button
style={{
position: 'absolute',
top: 20,
right: 20,
}}
size="middle"
type="link"
key="backListBtn"
onClick={() => {
history.push('/metric/market');
}}
>
<Space>
<ArrowLeftOutlined />
</Space>
</Button>
{/* <div className={styles.btnWrapper}>
{/* <Row style={{ paddingLeft: 82, paddingBottom: 8 }}>
</div> */}
</Row> */}
</div>
{authMessage && <div style={{ color: '#d46b08', marginBottom: 15 }}>{authMessage}</div>}
<div className={styles.sectionBox}>
<ProCard size="small" title="数据趋势">
<ProCard
size="small"
title={
<>
<span></span>
{authMessage && <div style={{ color: '#d46b08' }}>{authMessage}</div>}
</>
}
>
<TrendChart
data={metricTrendData}
isPer={
@@ -272,7 +274,7 @@ const MetricTrendSection: React.FC<Props> = ({
</ProCard>
</div>
<div className={styles.sectionBox}>
<div className={styles.sectionBox} style={{ paddingBottom: 0 }}>
<ProCard
size="small"
title="数据明细"
@@ -307,7 +309,7 @@ const MetricTrendSection: React.FC<Props> = ({
</Space.Compact>
}
>
<div style={{ minHeight: '528px' }}>
<div style={{ minHeight: '450px' }}>
<MetricTable
loading={metricTrendLoading}
columnConfig={tableColumnConfig}

View File

@@ -2,7 +2,6 @@ import { Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import React, { useEffect, useState } from 'react';
import moment from 'moment';
import styles from '../style.less';
import { ColumnConfig } from '../data';
type Props = {
@@ -57,16 +56,15 @@ const MetricTable: React.FC<Props> = ({
return (
<div style={{ height: '100%' }}>
{Array.isArray(columns) && columns.length > 0 && (
<Table
columns={columns}
dataSource={dataSource}
scroll={{ x: 200, y: 700 }}
// pagination={{ defaultPageSize: 20 }}
loading={loading}
onChange={() => {}}
/>
)}
{/* {Array.isArray(columns) && columns.length > 0 && ( */}
<Table
columns={columns}
dataSource={dataSource}
scroll={{ x: 200, y: 700 }}
loading={loading}
onChange={() => {}}
/>
{/* )} */}
</div>
);
};

View File

@@ -121,6 +121,23 @@
.metricDetailWrapper {
height: calc(100vh - 56px);
overflow: scroll;
.metricDetailTab {
:global {
.ant-tabs-nav {
background-color: rgb(255, 255, 255);
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
margin: 10px 20px 0 20px;
padding: 0 20px;
border-radius: 8px;
}
.ant-tabs-tab {
padding: 12px 0;
color: #344767;
font-weight: 500;
}
}
}
.metricDetail {
display: flex;
padding: 0px;
@@ -158,27 +175,27 @@
.siderContainer {
background-color: rgb(255, 255, 255);
width: 450px;
min-height: 100vh;
margin: 20px 20px 20px 0;
min-height: calc(100vh - 78px);
margin: 10px 20px 20px 0;
border-radius: 6px;
box-shadow: rgba(0, 0, 0, 0.08) 6px 0px 16px 0px, rgba(0, 0, 0, 0.12) 3px 0px 6px -4px, rgba(0, 0, 0, 0.05) 9px 0px 28px 8px;
}
}
}
.metricTrendSection {
.sectionBox {
margin: 20px;
padding: 10px;
box-shadow: #888888 0px 0px 1px, rgba(29, 41, 57, 0.08) 0px 1px 3px;
background-color: rgb(255, 255, 255);
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
border-radius: 6px;
background-image: none;
overflow: hidden;
position: relative;
}
.sectionBox {
margin: 20px;
padding: 10px;
box-shadow: #888888 0px 0px 1px, rgba(29, 41, 57, 0.08) 0px 1px 3px;
background-color: rgb(255, 255, 255);
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
border-radius: 6px;
background-image: none;
overflow: hidden;
position: relative;
}
.metricInfoSider{
padding: 24px;
color: #344767;

View File

@@ -103,12 +103,14 @@ const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
const filterData = dataSourceRef.current.reduce(
(data: ISemantic.IDomainSchemaRelaList, item: ISemantic.IDomainSchemaRelaItem) => {
const { dimensions, metrics } = item;
const dimensionsList = dimensions.filter((dimension) => {
return dimension.name.includes(text);
});
const metricsList = metrics.filter((metric) => {
return metric.name.includes(text);
});
const dimensionsList =
dimensions?.filter((dimension) => {
return dimension.name.includes(text);
}) || [];
const metricsList =
metrics?.filter((metric) => {
return metric.name.includes(text);
}) || [];
data.push({
...item,
dimensions: dimensionsList,

View File

@@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
import { ISemantic } from '../../data';
import { TransType } from '../../enum';
import DimensionMetricVisibleTransfer from '../../components/Entity/DimensionMetricVisibleTransfer';
import { wrapperTransTypeAndId } from '../../components/Entity/utils';
export type ModelCreateFormModalProps = {
dimensionList: ISemantic.IDimensionItem[];
metricList: ISemantic.IMetricItem[];
modelId?: number;
selectedTransferKeys: React.Key[];
onCancel: () => void;
onSubmit: (values: any, selectedKeys: React.Key[]) => void;
};
const DimensionMetricTransferModal: React.FC<ModelCreateFormModalProps> = ({
modelId,
selectedTransferKeys,
metricList,
dimensionList,
onSubmit,
}) => {
const [sourceList, setSourceList] = useState<any[]>([]);
const [selectedItemList, setSelectedItemList] = useState<any[]>([]);
const addItemKey = (item: any, transType: TransType) => {
const { id } = item;
const key = wrapperTransTypeAndId(transType, id);
return {
...item,
transType,
key,
};
};
useEffect(() => {
const sourceDimensionList = dimensionList.reduce((mergeList: any[], item) => {
mergeList.push(addItemKey(item, TransType.DIMENSION));
return mergeList;
}, []);
const hasDimensionList = selectedItemList
.filter((item) => {
return item.typeEnum === TransType.DIMENSION;
})
.reduce((modelDimensionList: ISemantic.IDimensionItem[], item) => {
const hasItem = sourceDimensionList.find((dataListItem: ISemantic.IDimensionItem) => {
return dataListItem.id === item.id;
});
if (!hasItem) {
modelDimensionList.push(addItemKey(item, TransType.DIMENSION));
}
return modelDimensionList;
}, []);
const sourceMetricList = metricList.reduce((mergeList: any[], item) => {
mergeList.push(addItemKey(item, TransType.METRIC));
return mergeList;
}, []);
const hasMetricList = selectedItemList
.filter((item) => {
return item.typeEnum === TransType.METRIC;
})
.reduce((modelMetricList: ISemantic.IMetricItem[], item) => {
const hasItem = sourceMetricList.find((dataListItem: ISemantic.IMetricItem) => {
return dataListItem.id === item.id;
});
if (!hasItem) {
modelMetricList.push(addItemKey(item, TransType.METRIC));
}
return modelMetricList;
}, []);
setSourceList([
...sourceDimensionList,
...sourceMetricList,
...hasDimensionList,
...hasMetricList,
]);
}, [dimensionList, metricList]);
return (
<DimensionMetricVisibleTransfer
titles={['未关联维度/指标', '已关联维度/指标']}
listStyle={{
width: 520,
height: 600,
}}
targetList={selectedTransferKeys}
sourceList={sourceList}
onChange={(newTargetKeys: string[]) => {
const removeDimensionList: ISemantic.IDimensionItem[] = [];
const removeMetricList: ISemantic.IMetricItem[] = [];
const dimensionItemChangeList = dimensionList.reduce(
(dimensionChangeList: any[], item: any) => {
if (newTargetKeys.includes(wrapperTransTypeAndId(TransType.DIMENSION, item.id))) {
dimensionChangeList.push(item);
} else {
removeDimensionList.push(item.id);
}
return dimensionChangeList;
},
[],
);
const metricItemChangeList = metricList.reduce((metricChangeList: any[], item: any) => {
if (newTargetKeys.includes(wrapperTransTypeAndId(TransType.METRIC, item.id))) {
metricChangeList.push(item);
} else {
removeMetricList.push(item.id);
}
return metricChangeList;
}, []);
setSelectedItemList([...dimensionItemChangeList, ...metricItemChangeList]);
// 如果不是当前选中model中的指标或者维度则先从本地数据中删除避免后续请求数据更新时产生视觉上的界面闪烁
const preUpdateSourceData = sourceList.filter((item) => {
const { typeEnum, id } = item;
if (typeEnum === TransType.DIMENSION) {
if (modelId !== item.modelId && removeDimensionList.includes(id)) {
return false;
}
}
if (typeEnum === TransType.METRIC) {
if (modelId !== item.modelId && removeMetricList.includes(id)) {
return false;
}
}
return true;
});
setSourceList([...preUpdateSourceData]);
const viewModelConfigs = [...dimensionItemChangeList, ...metricItemChangeList].reduce(
(config, item) => {
const { modelId, id, typeEnum } = item;
if (config[modelId]) {
if (typeEnum === TransType.DIMENSION) {
config[modelId].dimensions.push(id);
}
if (typeEnum === TransType.METRIC) {
config[modelId].metrics.push(id);
}
} else {
config[modelId] = {
id: modelId,
metrics: typeEnum === TransType.METRIC ? [id] : [],
dimensions: typeEnum === TransType.DIMENSION ? [id] : [],
};
}
return config;
},
{},
);
onSubmit?.(viewModelConfigs, newTargetKeys);
}}
/>
);
};
export default DimensionMetricTransferModal;

View File

@@ -0,0 +1,773 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Form,
Button,
Modal,
Steps,
Input,
Select,
Radio,
Switch,
InputNumber,
message,
Result,
Row,
Col,
Space,
Tooltip,
Tag,
} from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import MetricMeasuresFormTable from './MetricMeasuresFormTable';
import { SENSITIVE_LEVEL_OPTIONS, METRIC_DEFINE_TYPE } from '../constant';
import { formLayout } from '@/components/FormHelper/utils';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import styles from './style.less';
import { getMetricsToCreateNewMetric, getModelDetail, getDrillDownDimension } from '../service';
import MetricMetricFormTable from './MetricMetricFormTable';
import MetricFieldFormTable from './MetricFieldFormTable';
import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal';
import TableTitleTooltips from '../components/TableTitleTooltips';
import { createMetric, updateMetric, mockMetricAlias, getMetricTags } from '../service';
import { ISemantic } from '../data';
import { history } from 'umi';
export type CreateFormProps = {
datasourceId?: number;
domainId: number;
modelId: number;
createModalVisible: boolean;
metricItem: any;
onCancel?: () => void;
onSubmit?: (values: any) => void;
};
const { Step } = Steps;
const FormItem = Form.Item;
const { TextArea } = Input;
const { Option } = Select;
const queryParamsTypeParamsKey = {
[METRIC_DEFINE_TYPE.MEASURE]: 'metricDefineByMeasureParams',
[METRIC_DEFINE_TYPE.METRIC]: 'metricDefineByMetricParams',
[METRIC_DEFINE_TYPE.FIELD]: 'metricDefineByFieldParams',
};
const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
datasourceId,
domainId,
modelId,
onCancel,
createModalVisible,
metricItem,
onSubmit,
}) => {
const isEdit = !!metricItem?.id;
const [currentStep, setCurrentStep] = useState(0);
const formValRef = useRef({} as any);
const [form] = Form.useForm();
const updateFormVal = (val: any) => {
const formVal = {
...formValRef.current,
...val,
};
formValRef.current = formVal;
};
const [classMeasureList, setClassMeasureList] = useState<ISemantic.IMeasure[]>([]);
const [exprTypeParamsState, setExprTypeParamsState] = useState<{
[METRIC_DEFINE_TYPE.MEASURE]: ISemantic.IMeasureTypeParams;
[METRIC_DEFINE_TYPE.METRIC]: ISemantic.IMetricTypeParams;
[METRIC_DEFINE_TYPE.FIELD]: ISemantic.IFieldTypeParams;
}>({
[METRIC_DEFINE_TYPE.MEASURE]: {
measures: [],
expr: '',
},
[METRIC_DEFINE_TYPE.METRIC]: {
metrics: [],
expr: '',
},
[METRIC_DEFINE_TYPE.FIELD]: {
fields: [],
expr: '',
},
} as any);
// const [exprTypeParamsState, setExprTypeParamsState] = useState<ISemantic.IMeasure[]>([]);
const [defineType, setDefineType] = useState(METRIC_DEFINE_TYPE.MEASURE);
const [createNewMetricList, setCreateNewMetricList] = useState<ISemantic.IMetricItem[]>([]);
const [fieldList, setFieldList] = useState<string[]>([]);
const [isPercentState, setIsPercentState] = useState<boolean>(false);
const [isDecimalState, setIsDecimalState] = useState<boolean>(false);
const [hasMeasuresState, setHasMeasuresState] = useState<boolean>(true);
const [llmLoading, setLlmLoading] = useState<boolean>(false);
const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]);
const [metricRelationModalOpenState, setMetricRelationModalOpenState] = useState<boolean>(false);
const [drillDownDimensions, setDrillDownDimensions] = useState<
ISemantic.IDrillDownDimensionItem[]
>([]);
const [drillDownDimensionsConfig, setDrillDownDimensionsConfig] = useState<
ISemantic.IDrillDownDimensionItem[]
>([]);
const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1);
const queryModelDetail = async () => {
// const { code, data } = await getMeasureListByModelId(modelId);
const { code, data } = await getModelDetail({ modelId: modelId || metricItem?.modelId });
if (code === 200) {
if (Array.isArray(data?.modelDetail?.fields)) {
setFieldList(data.modelDetail.fields);
}
if (Array.isArray(data?.modelDetail?.measures)) {
setClassMeasureList(data.modelDetail.measures);
if (datasourceId) {
const hasMeasures = data.some(
(item: ISemantic.IMeasure) => item.datasourceId === datasourceId,
);
setHasMeasuresState(hasMeasures);
}
return;
}
}
setClassMeasureList([]);
};
const queryDrillDownDimension = async (metricId: number) => {
const { code, data, msg } = await getDrillDownDimension(metricId);
if (code === 200 && Array.isArray(data)) {
setDrillDownDimensionsConfig(data);
}
if (code !== 200) {
message.error(msg);
}
return [];
};
useEffect(() => {
queryModelDetail();
queryMetricsToCreateNewMetric();
queryMetricTags();
}, []);
const handleNext = async () => {
const fieldsValue = await form.validateFields();
const submitForm = {
...formValRef.current,
...fieldsValue,
metricDefineType: defineType,
[queryParamsTypeParamsKey[defineType]]: exprTypeParamsState[defineType],
};
updateFormVal(submitForm);
if (currentStep < 1) {
forward();
} else {
await saveMetric(submitForm);
}
};
const initData = () => {
const {
id,
name,
bizName,
description,
sensitiveLevel,
typeParams,
dataFormat,
dataFormatType,
alias,
tags,
metricDefineType,
metricDefineByMeasureParams,
metricDefineByMetricParams,
metricDefineByFieldParams,
} = metricItem;
const isPercent = dataFormatType === 'percent';
const isDecimal = dataFormatType === 'decimal';
const initValue = {
id,
name,
bizName,
sensitiveLevel,
description,
tags,
// isPercent,
dataFormatType: dataFormatType || '',
alias: alias && alias.trim() ? alias.split(',') : [],
dataFormat: dataFormat || {
decimalPlaces: 2,
needMultiply100: false,
},
};
const editInitFormVal = {
...formValRef.current,
...initValue,
};
if (metricDefineType === METRIC_DEFINE_TYPE.MEASURE) {
const { measures, expr } = metricDefineByMeasureParams || {};
setExprTypeParamsState({
...exprTypeParamsState,
[METRIC_DEFINE_TYPE.MEASURE]: {
measures: measures || [],
expr: expr || '',
},
});
}
if (metricDefineType === METRIC_DEFINE_TYPE.METRIC) {
const { metrics, expr } = metricDefineByMetricParams || {};
setExprTypeParamsState({
...exprTypeParamsState,
[METRIC_DEFINE_TYPE.METRIC]: {
metrics: metrics || [],
expr: expr || '',
},
});
}
if (metricDefineType === METRIC_DEFINE_TYPE.FIELD) {
const { fields, expr } = metricDefineByFieldParams || {};
setExprTypeParamsState({
...exprTypeParamsState,
[METRIC_DEFINE_TYPE.FIELD]: {
fields: fields || [],
expr: expr || '',
},
});
}
updateFormVal(editInitFormVal);
form.setFieldsValue(initValue);
setDefineType(metricDefineType);
setIsPercentState(isPercent);
setIsDecimalState(isDecimal);
queryDrillDownDimension(metricItem?.id);
};
useEffect(() => {
if (isEdit) {
initData();
}
}, [metricItem]);
const isEmptyConditions = (
metricDefineType: METRIC_DEFINE_TYPE,
metricDefineParams:
| ISemantic.IMeasureTypeParams
| ISemantic.IMetricTypeParams
| ISemantic.IFieldTypeParams,
) => {
if (metricDefineType === METRIC_DEFINE_TYPE.MEASURE) {
const { measures } = (metricDefineParams as ISemantic.IMeasureTypeParams) || {};
if (!(Array.isArray(measures) && measures.length > 0)) {
message.error('请添加一个度量');
return true;
}
}
if (metricDefineType === METRIC_DEFINE_TYPE.METRIC) {
const { metrics } = (metricDefineParams as ISemantic.IMetricTypeParams) || {};
if (!(Array.isArray(metrics) && metrics.length > 0)) {
message.error('请添加一个指标');
return true;
}
}
if (metricDefineType === METRIC_DEFINE_TYPE.FIELD) {
const { fields } = (metricDefineParams as ISemantic.IFieldTypeParams) || {};
if (!(Array.isArray(fields) && fields.length > 0)) {
message.error('请添加一个字段');
return true;
}
}
return false;
};
const saveMetric = async (fieldsValue: any) => {
const queryParams = {
modelId: isEdit ? metricItem.modelId : modelId,
relateDimension: {
...(metricItem?.relateDimension || {}),
drillDownDimensions,
},
...fieldsValue,
};
const { alias, dataFormatType } = queryParams;
queryParams.alias = Array.isArray(alias) ? alias.join(',') : '';
if (!queryParams[queryParamsTypeParamsKey[defineType]]?.expr) {
message.error('请输入度量表达式');
return;
}
if (!dataFormatType) {
delete queryParams.dataFormat;
}
if (isEmptyConditions(defineType, queryParams[queryParamsTypeParamsKey[defineType]])) {
return;
}
let saveMetricQuery = createMetric;
if (queryParams.id) {
saveMetricQuery = updateMetric;
}
const { code, msg } = await saveMetricQuery(queryParams);
if (code === 200) {
message.success('编辑指标成功');
onSubmit?.(queryParams);
return;
}
message.error(msg);
};
const generatorMetricAlias = async () => {
setLlmLoading(true);
const { code, data } = await mockMetricAlias({ ...metricItem });
const formAlias = form.getFieldValue('alias');
setLlmLoading(false);
if (code === 200) {
form.setFieldValue('alias', Array.from(new Set([...formAlias, ...data])));
} else {
message.error('大语言模型解析异常');
}
};
const queryMetricTags = async () => {
const { code, data } = await getMetricTags();
if (code === 200) {
// form.setFieldValue('alias', Array.from(new Set([...formAlias, ...data])));
setTagOptions(
Array.isArray(data)
? data.map((tag: string) => {
return { label: tag, value: tag };
})
: [],
);
} else {
message.error('获取指标标签失败');
}
};
const queryMetricsToCreateNewMetric = async () => {
const { code, data } = await getMetricsToCreateNewMetric({
modelId: modelId || metricItem?.modelId,
});
if (code === 200) {
setCreateNewMetricList(data);
} else {
message.error('获取指标标签失败');
}
};
const renderContent = () => {
if (currentStep === 1) {
return (
<div>
<div
style={{
padding: '0 0 20px 24px',
// borderBottom: '1px solid #eee',
}}
>
<Radio.Group
buttonStyle="solid"
value={defineType}
onChange={(e) => {
setDefineType(e.target.value);
}}
>
<Radio.Button value={METRIC_DEFINE_TYPE.MEASURE}></Radio.Button>
<Radio.Button value={METRIC_DEFINE_TYPE.METRIC}></Radio.Button>
<Radio.Button value={METRIC_DEFINE_TYPE.FIELD}></Radio.Button>
</Radio.Group>
</div>
{defineType === METRIC_DEFINE_TYPE.MEASURE && (
<>
<MetricMeasuresFormTable
datasourceId={datasourceId}
typeParams={exprTypeParamsState[METRIC_DEFINE_TYPE.MEASURE]}
measuresList={classMeasureList}
onFieldChange={(measures: ISemantic.IMeasure[]) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.MEASURE]: {
...prevState[METRIC_DEFINE_TYPE.MEASURE],
measures,
},
};
});
}}
onSqlChange={(expr: string) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.MEASURE]: {
...prevState[METRIC_DEFINE_TYPE.MEASURE],
expr,
},
};
});
}}
/>
</>
)}
{defineType === METRIC_DEFINE_TYPE.METRIC && (
<>
<p className={styles.desc}>
<Tag color="#2499ef14" className={styles.markerTag}>
</Tag>
<Tag color="#2499ef14" className={styles.markerTag}>
</Tag>
</p>
<MetricMetricFormTable
typeParams={exprTypeParamsState[METRIC_DEFINE_TYPE.METRIC]}
metricList={createNewMetricList}
onFieldChange={(metrics: ISemantic.IMetricTypeParamsItem[]) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.METRIC]: {
...prevState[METRIC_DEFINE_TYPE.METRIC],
metrics,
},
};
});
}}
onSqlChange={(expr: string) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.METRIC]: {
...prevState[METRIC_DEFINE_TYPE.METRIC],
expr,
},
};
});
}}
/>
</>
)}
{defineType === METRIC_DEFINE_TYPE.FIELD && (
<>
<MetricFieldFormTable
typeParams={exprTypeParamsState[METRIC_DEFINE_TYPE.FIELD]}
fieldList={fieldList}
onFieldChange={(fields: ISemantic.IFieldTypeParamsItem[]) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.FIELD]: {
...prevState[METRIC_DEFINE_TYPE.FIELD],
fields,
},
};
});
}}
onSqlChange={(expr: string) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.FIELD]: {
...prevState[METRIC_DEFINE_TYPE.FIELD],
expr,
},
};
});
}}
/>
</>
)}
</div>
);
}
return (
<>
<FormItem hidden={true} name="id" label="ID">
<Input placeholder="id" />
</FormItem>
<FormItem
name="name"
label="指标名称"
rules={[{ required: true, message: '请输入指标名称' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="英文名称"
rules={[{ required: true, message: '请输入英文名称' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>
<FormItem label="别名">
<Row>
<Col flex="1 1 200px">
<FormItem name="alias" noStyle>
<Select
mode="tags"
placeholder="输入别名后回车确认,多别名输入、复制粘贴支持英文逗号自动分隔"
tokenSeparators={[',']}
maxTagCount={9}
/>
</FormItem>
</Col>
{isEdit && (
<Col flex="0 1 75px">
<Button
type="link"
loading={llmLoading}
size="small"
style={{ top: '2px' }}
onClick={() => {
generatorMetricAlias();
}}
>
<Space>
<Tooltip title="智能填充将根据指标相关信息,使用大语言模型获取指标别名">
<InfoCircleOutlined />
</Tooltip>
</Space>
</Button>
</Col>
)}
</Row>
</FormItem>
<FormItem name="tags" label="标签">
<Select
mode="tags"
placeholder="输入别名后回车确认,多别名输入、复制粘贴支持英文逗号自动分隔"
tokenSeparators={[',']}
maxTagCount={9}
options={tagOptions}
/>
</FormItem>
<FormItem
name="sensitiveLevel"
label="敏感度"
rules={[{ required: true, message: '请选择敏感度' }]}
>
<Select placeholder="请选择敏感度">
{SENSITIVE_LEVEL_OPTIONS.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</FormItem>
<FormItem
name="description"
label={
<TableTitleTooltips
title="业务口径"
overlayInnerStyle={{ width: 600 }}
tooltips={
<>
<p>
使使
</p>
<p>1. </p>
<p>2. </p>
<p>3. 使使</p>
<p>4. </p>
<p>
便
</p>
</>
}
/>
}
rules={[{ required: true, message: '请输入业务口径' }]}
>
<TextArea placeholder="请输入业务口径" />
</FormItem>
<FormItem
label={
<FormItemTitle
title={'下钻维度配置'}
subTitle={'配置下钻维度后,将可以在指标卡中进行下钻'}
/>
}
>
<Button
type="primary"
onClick={() => {
setMetricRelationModalOpenState(true);
}}
>
</Button>
</FormItem>
<FormItem
label={
<FormItemTitle
title={'数据格式化'}
// subTitle={'开启后指标数据展示时会根据配置进行格式化如0.02 -> 2%'}
/>
}
name="dataFormatType"
>
<Radio.Group buttonStyle="solid" size="middle">
<Radio.Button value=""></Radio.Button>
<Radio.Button value="decimal"></Radio.Button>
<Radio.Button value="percent"></Radio.Button>
</Radio.Group>
</FormItem>
{(isPercentState || isDecimalState) && (
<FormItem
label={
<FormItemTitle
title={'小数位数'}
subTitle={`对小数位数进行设置如保留两位0.021252 -> 0.02${
isPercentState ? '%' : ''
}`}
/>
}
name={['dataFormat', 'decimalPlaces']}
>
<InputNumber placeholder="请输入需要保留小数位数" style={{ width: '300px' }} />
</FormItem>
)}
{isPercentState && (
<>
<FormItem
label={
<FormItemTitle
title={'原始值是否乘以100'}
subTitle={'如 原始值0.001 ->展示值0.1% '}
/>
}
name={['dataFormat', 'needMultiply100']}
valuePropName="checked"
>
<Switch />
</FormItem>
</>
)}
</>
);
};
const renderFooter = () => {
if (!hasMeasuresState) {
return <Button onClick={onCancel}></Button>;
}
if (currentStep === 1) {
return (
<>
<Button style={{ float: 'left' }} onClick={backward}>
</Button>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</>
);
}
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</>
);
};
return (
<Modal
forceRender
width={800}
style={{ top: 48 }}
// styles={{ padding: '32px 40px 48px' }}
destroyOnClose
title={`${isEdit ? '编辑' : '新建'}指标`}
maskClosable={false}
open={createModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
{hasMeasuresState ? (
<>
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
<Step title="基本信息" />
<Step title="度量信息" />
</Steps>
<Form
{...formLayout}
form={form}
initialValues={{
...formValRef.current,
dataFormatType: '',
}}
onValuesChange={(value, values: any) => {
const { isPercent, dataFormatType } = values;
// if (isPercent !== undefined) {
// setIsPercentState(isPercent);
// }
if (dataFormatType === 'percent') {
setIsPercentState(true);
setIsDecimalState(false);
}
if (dataFormatType === 'decimal') {
setIsPercentState(false);
setIsDecimalState(true);
}
if (!dataFormatType) {
setIsPercentState(false);
setIsDecimalState(false);
}
}}
className={styles.form}
>
{renderContent()}
</Form>
<DimensionAndMetricRelationModal
metricItem={metricItem}
relationsInitialValue={drillDownDimensionsConfig}
open={metricRelationModalOpenState}
onCancel={() => {
setMetricRelationModalOpenState(false);
}}
onSubmit={(relations) => {
setDrillDownDimensions(relations);
setMetricRelationModalOpenState(false);
}}
onRefreshRelationData={() => {
queryDrillDownDimension(metricItem?.id);
}}
/>
</>
) : (
<Result
status="warning"
subTitle="当前数据源缺少度量,无法创建指标。请前往数据源配置中,将字段设置为度量"
extra={
<Button
type="primary"
key="console"
onClick={() => {
history.replace(`/model/${domainId}/${modelId || metricItem?.modelId}/dataSource`);
onCancel?.();
}}
>
</Button>
}
/>
)}
</Modal>
);
};
export default MetricInfoCreateForm;

View File

@@ -0,0 +1,236 @@
import React, { useState, useEffect, useRef } from 'react';
import { Form, Button, Modal, Input, Select, Steps } from 'antd';
import styles from '../../components/style.less';
import { message } from 'antd';
import { formLayout } from '@/components/FormHelper/utils';
import { createView, updateView } from '../../service';
import { ISemantic } from '../../data';
import { isString } from 'lodash';
import ViewModelConfigTable from './ViewModelConfigTable';
import ViewModelConfigTransfer from './ViewModelConfigTransfer';
const FormItem = Form.Item;
export type ModelCreateFormModalProps = {
domainId: number;
viewItem: any;
modelList: ISemantic.IModelItem[];
onCancel: () => void;
onSubmit: (values: any) => void;
};
const { Step } = Steps;
const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
viewItem,
domainId,
onCancel,
onSubmit,
modelList,
}) => {
const [currentStep, setCurrentStep] = useState(0);
const [formVals, setFormVals] = useState<ISemantic.IModelItem>({
...viewItem,
currentModel: modelList[0]?.id,
});
const [submitData, setSubmitData] = useState({});
const [saveLoading, setSaveLoading] = useState<boolean>(false);
const [modalWidth, setModalWidth] = useState<number>(800);
const [selectedModelItem, setSelectedModelItem] = useState<ISemantic.IModelItem | undefined>(
modelList[0],
);
const [form] = Form.useForm();
const configTableRef = useRef<any>();
useEffect(() => {
form.setFieldsValue({
...viewItem,
});
}, [viewItem]);
const handleConfirm = async () => {
const fieldsValue = await form.validateFields();
const viewModelConfigsMap = configTableRef?.current.getViewModelConfigs() || {};
const queryData: ISemantic.IModelItem = {
...formVals,
...fieldsValue,
...submitData,
viewDetail: {
viewModelConfigs: Object.values(viewModelConfigsMap),
},
domainId,
};
setFormVals(queryData);
setSaveLoading(true);
const { code, msg } = await (!queryData.id ? createView : updateView)(queryData);
setSaveLoading(false);
if (code === 200) {
onSubmit?.(queryData);
} else {
message.error(msg);
}
};
const footer = (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" loading={saveLoading} onClick={handleConfirm}>
</Button>
</>
);
const forward = () => {
setModalWidth(1200);
setCurrentStep(currentStep + 1);
};
const backward = () => {
setModalWidth(800);
setCurrentStep(currentStep - 1);
};
const handleNext = async () => {
const fieldsValue = await form.validateFields();
const submitForm = {
...submitData,
...fieldsValue,
};
setSubmitData(submitForm);
if (currentStep < 1) {
forward();
} else {
// await saveMetric(submitForm);
}
};
const renderFooter = () => {
if (currentStep === 1) {
return (
<>
<Button style={{ float: 'left' }} onClick={backward}>
</Button>
<Button onClick={onCancel}></Button>
{/* <Button type="primary" onClick={handleNext}> */}
<Button
type="primary"
onClick={() => {
handleConfirm();
}}
>
</Button>
</>
);
}
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</>
);
};
const renderContent = () => {
if (currentStep === 1) {
return (
<div>
<FormItem
name="currentModel"
label="选择模型"
rules={[{ required: true, message: '请选择模型!' }]}
>
<Select
placeholder="请选择模型,获取当前模型下指标维度信息"
onChange={(val) => {
const modelItem = modelList.find((item) => item.id === val);
setSelectedModelItem(modelItem);
}}
options={modelList.map((item) => {
return { label: item.name, value: item.id };
})}
/>
</FormItem>
<ViewModelConfigTransfer
modelItem={selectedModelItem}
viewItem={viewItem}
ref={configTableRef}
/>
</div>
);
}
return (
<>
<FormItem
name="name"
label="视图名称"
rules={[{ required: true, message: '请输入视图名称!' }]}
>
<Input placeholder="视图名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="视图英文名称"
rules={[{ required: true, message: '请输入视图英文名称!' }]}
>
<Input placeholder="请输入视图英文名称" />
</FormItem>
<FormItem
name="alias"
label="别名"
getValueFromEvent={(value) => {
return Array.isArray(value) ? value.join(',') : '';
}}
getValueProps={(value) => {
return {
value: isString(value) ? value.split(',') : [],
};
}}
>
<Select
mode="tags"
placeholder="输入别名后回车确认,多别名输入、复制粘贴支持英文逗号自动分隔"
tokenSeparators={[',']}
maxTagCount={9}
/>
</FormItem>
<FormItem name="description" label="视图描述">
<Input.TextArea placeholder="视图描述" />
</FormItem>
</>
);
};
return (
<Modal
width={modalWidth}
destroyOnClose
title={'视图信息'}
open={true}
// footer={footer}
footer={renderFooter()}
onCancel={onCancel}
>
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
<Step title="基本信息" />
<Step title="关联信息" />
</Steps>
<Form
{...formLayout}
form={form}
initialValues={{
...formVals,
}}
onValuesChange={(value, values) => {}}
className={styles.form}
>
{renderContent()}
</Form>
</Modal>
);
};
export default ViewCreateFormModal;

View File

@@ -0,0 +1,188 @@
import { message } from 'antd';
// import { InfoCircleOutlined } from '@ant-design/icons';
import React, { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import type { Ref } from 'react';
import DimensionMetricTransferModal from './DimensionMetricTransferModal';
// import styles from '../../components/style.less';
import { TransType } from '../../enum';
import { getDimensionList, queryMetric } from '../../service';
import { wrapperTransTypeAndId } from '../../components/Entity/utils';
import { ISemantic } from '../../data';
import { isArrayOfValues } from '@/utils/utils';
type Props = {
// modelList: ISemantic.IModelItem[];
viewItem: ISemantic.IViewItem;
modelItem?: ISemantic.IModelItem;
[key: string]: any;
};
const ViewModelConfigTransfer: React.FC<Props> = forwardRef(
({ viewItem, modelItem }: Props, ref: Ref<any>) => {
// const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [selectedTransferKeys, setSelectedTransferKeys] = useState<React.Key[]>([]);
const [viewModelConfigsMap, setViewModelConfigsMap] = useState({});
const [dimensionList, setDimensionList] = useState<ISemantic.IDimensionItem[]>([]);
const [metricList, setMetricList] = useState<ISemantic.IMetricItem[]>([]);
useImperativeHandle(ref, () => ({
getViewModelConfigs: () => {
return viewModelConfigsMap;
},
}));
const queryDimensionListByIds = async (ids: number[]) => {
if (!isArrayOfValues(ids)) {
queryDimensionList([]);
return;
}
const { code, data, msg } = await getDimensionList({ ids });
if (code === 200 && Array.isArray(data?.list)) {
queryDimensionList(data.list);
} else {
message.error(msg);
}
};
const queryMetricListByIds = async (ids: number[]) => {
if (!isArrayOfValues(ids)) {
queryMetricList([]);
return;
}
const { code, data, msg } = await queryMetric({ ids });
if (code === 200 && Array.isArray(data?.list)) {
queryMetricList(data.list);
} else {
message.error(msg);
}
};
const queryDimensionList = async (selectedDimensionList: ISemantic.IDimensionItem[]) => {
const { code, data, msg } = await getDimensionList({ modelId: modelItem?.id });
if (code === 200 && Array.isArray(data?.list)) {
const mergeList = selectedDimensionList.reduce(
(modelDimensionList: ISemantic.IDimensionItem[], item) => {
const hasItem = data.list.find((dataListItem: ISemantic.IDimensionItem) => {
return dataListItem.id === item.id;
});
if (!hasItem) {
return [item, ...modelDimensionList];
}
return modelDimensionList;
},
data.list,
);
setDimensionList(mergeList);
} else {
message.error(msg);
}
};
const queryMetricList = async (selectedMetricList: ISemantic.IMetricItem[]) => {
const { code, data, msg } = await queryMetric({ modelId: modelItem?.id });
if (code === 200 && Array.isArray(data?.list)) {
const mergeList = selectedMetricList.reduce(
(modelMetricList: ISemantic.IMetricItem[], item) => {
const hasItem = data.list.find((dataListItem: ISemantic.IMetricItem) => {
return dataListItem.id === item.id;
});
if (!hasItem) {
return [item, ...modelMetricList];
}
return modelMetricList;
},
data.list,
);
setMetricList(mergeList);
} else {
message.error(msg);
}
};
useEffect(() => {
const viewModelConfigs = viewItem?.viewDetail?.viewModelConfigs;
if (Array.isArray(viewModelConfigs)) {
const idList: number[] = [];
const transferKeys: React.Key[] = [];
const viewConfigMap = {};
const allMetrics: number[] = [];
const allDimensions: number[] = [];
viewModelConfigs.forEach((item: ISemantic.IViewModelConfigItem) => {
const { id, metrics, dimensions } = item;
idList.push(id);
allMetrics.push(...metrics);
allDimensions.push(...dimensions);
viewConfigMap[id] = { ...item };
if (Array.isArray(metrics)) {
metrics.forEach((metricId: number) => {
transferKeys.push(wrapperTransTypeAndId(TransType.METRIC, metricId));
});
}
if (Array.isArray(dimensions)) {
dimensions.forEach((dimensionId: number) => {
transferKeys.push(wrapperTransTypeAndId(TransType.DIMENSION, dimensionId));
});
}
});
setSelectedTransferKeys(transferKeys);
// setSelectedRowKeys(idList);
setViewModelConfigsMap(viewConfigMap);
}
}, []);
useEffect(() => {
const viewModelConfigs = isArrayOfValues(Object.values(viewModelConfigsMap))
? (Object.values(viewModelConfigsMap) as ISemantic.IViewModelConfigItem[])
: viewItem?.viewDetail?.viewModelConfigs;
if (isArrayOfValues(viewModelConfigs)) {
const allMetrics: number[] = [];
const allDimensions: number[] = [];
viewModelConfigs.forEach((item: ISemantic.IViewModelConfigItem) => {
const { metrics, dimensions } = item;
allMetrics.push(...metrics);
allDimensions.push(...dimensions);
});
queryDimensionListByIds(allDimensions);
queryMetricListByIds(allMetrics);
} else {
queryDimensionList([]);
queryMetricList([]);
}
}, [modelItem]);
return (
<>
<DimensionMetricTransferModal
modelId={modelItem?.id}
dimensionList={dimensionList}
metricList={metricList}
selectedTransferKeys={selectedTransferKeys}
onSubmit={(
submitData: Record<string, ISemantic.IViewModelConfigItem>,
selectedKeys: React.Key[],
) => {
const viewModelConfigs = Object.values(submitData) as ISemantic.IViewModelConfigItem[];
if (isArrayOfValues(viewModelConfigs)) {
const allMetrics: number[] = [];
const allDimensions: number[] = [];
viewModelConfigs.forEach((item: ISemantic.IViewModelConfigItem) => {
const { metrics, dimensions } = item;
allMetrics.push(...metrics);
allDimensions.push(...dimensions);
});
queryDimensionListByIds(allDimensions);
queryMetricListByIds(allMetrics);
}
setViewModelConfigsMap(submitData);
setSelectedTransferKeys(selectedKeys);
}}
onCancel={() => {}}
/>
</>
);
},
);
export default ViewModelConfigTransfer;

View File

@@ -0,0 +1,231 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Button, Space, Popconfirm, Input, Tag } from 'antd';
import React, { useRef, useState, useEffect } from 'react';
import { StatusEnum } from '../../enum';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import { deleteView, updateView, getViewList } from '../../service';
import ViewCreateFormModal from './ViewCreateFormModal';
import moment from 'moment';
import styles from '../../components/style.less';
import { ISemantic } from '../../data';
import { ColumnsConfig } from '../../components/MetricTableColumnRender';
type Props = {
disabledEdit?: boolean;
modelList: ISemantic.IModelItem[];
dispatch: Dispatch;
domainManger: StateType;
};
const ViewTable: React.FC<Props> = ({ disabledEdit = false, modelList, domainManger }) => {
const { selectDomainId } = domainManger;
const [viewItem, setViewItem] = useState<ISemantic.IViewItem>();
const [saveLoading, setSaveLoading] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
const actionRef = useRef<ActionType>();
const updateViewStatus = async (modelData: ISemantic.IViewItem) => {
setSaveLoading(true);
const { code, msg } = await updateView({
...modelData,
});
setSaveLoading(false);
if (code === 200) {
queryViewList();
} else {
message.error(msg);
}
};
const [viewList, setViewList] = useState([]);
useEffect(() => {
queryViewList();
}, []);
const queryViewList = async () => {
setLoading(true);
const { code, data, msg } = await getViewList(selectDomainId);
setLoading(false);
if (code === 200) {
setViewList(data);
} else {
message.error(msg);
}
};
const columns: ProColumns[] = [
{
dataIndex: 'id',
title: 'ID',
width: 80,
search: false,
},
{
dataIndex: 'name',
title: '视图名称',
search: false,
// render: (_, record) => {
// return <a>{_}</a>;
// },
},
{
dataIndex: 'alias',
title: '别名',
width: 150,
ellipsis: true,
search: false,
},
{
dataIndex: 'bizName',
title: '英文名称',
search: false,
},
{
dataIndex: 'status',
title: '状态',
search: false,
render: ColumnsConfig.state.render,
},
{
dataIndex: 'createdBy',
title: '创建人',
search: false,
},
{
dataIndex: 'description',
title: '描述',
search: false,
},
{
dataIndex: 'updatedAt',
title: '更新时间',
search: false,
render: (value: any) => {
return value && value !== '-' ? moment(value).format('YYYY-MM-DD HH:mm:ss') : '-';
},
},
];
if (!disabledEdit) {
columns.push({
title: '操作',
dataIndex: 'x',
valueType: 'option',
width: 150,
render: (_, record) => {
return (
<Space className={styles.ctrlBtnContainer}>
<a
key="metricEditBtn"
onClick={() => {
setViewItem(record);
setCreateDataSourceModalOpen(true);
}}
>
</a>
{record.status === StatusEnum.ONLINE ? (
<Button
type="link"
key="editStatusOfflineBtn"
onClick={() => {
updateViewStatus({
...record,
status: StatusEnum.OFFLINE,
});
}}
>
</Button>
) : (
<Button
type="link"
key="editStatusOnlineBtn"
onClick={() => {
updateViewStatus({
...record,
status: StatusEnum.ONLINE,
});
}}
>
</Button>
)}
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={async () => {
const { code, msg } = await deleteView(record.id);
if (code === 200) {
queryViewList();
} else {
message.error(msg);
}
}}
>
<a key="modelDeleteBtn"></a>
</Popconfirm>
</Space>
);
},
});
}
return (
<>
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
actionRef={actionRef}
rowKey="id"
search={false}
columns={columns}
loading={loading}
dataSource={viewList}
tableAlertRender={() => {
return false;
}}
size="small"
options={{ reload: false, density: false, fullScreen: false }}
toolBarRender={() =>
disabledEdit
? [<></>]
: [
<Button
key="create"
type="primary"
onClick={() => {
setViewItem(undefined);
setCreateDataSourceModalOpen(true);
}}
>
</Button>,
]
}
/>
{createDataSourceModalOpen && (
<ViewCreateFormModal
domainId={selectDomainId}
viewItem={viewItem}
modelList={modelList}
onSubmit={() => {
queryViewList();
setCreateDataSourceModalOpen(false);
}}
onCancel={() => {
setCreateDataSourceModalOpen(false);
}}
/>
)}
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ViewTable);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { ISemantic } from '../data';
import { connect } from 'umi';
import type { Dispatch } from 'umi';
import type { StateType } from '../model';
import ViewTable from './components/ViewTable';
type Props = {
disabledEdit?: boolean;
modelList: ISemantic.IModelItem[];
onModelChange?: (model?: ISemantic.IModelItem) => void;
domainManger: StateType;
dispatch: Dispatch;
};
const View: React.FC<Props> = ({ modelList, disabledEdit = false, onModelChange }) => {
return (
<div style={{ padding: '15px 20px' }}>
<ViewTable modelList={modelList} disabledEdit={disabledEdit} onModelChange={onModelChange} />
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(View);

View File

@@ -6,7 +6,7 @@ import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../model';
import { StatusEnum } from '../enum';
import { SENSITIVE_LEVEL_ENUM } from '../constant';
import { SENSITIVE_LEVEL_ENUM, SENSITIVE_LEVEL_OPTIONS } from '../constant';
import {
getModelList,
getDimensionList,
@@ -15,10 +15,11 @@ import {
} from '../service';
import DimensionInfoModal from './DimensionInfoModal';
import DimensionValueSettingModal from './DimensionValueSettingModal';
// import { updateDimension } from '../service';
import { ISemantic, IDataSource } from '../data';
import TableHeaderFilter from './TableHeaderFilter';
import moment from 'moment';
import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
import { ColumnsConfig } from './MetricTableColumnRender';
import styles from './style.less';
type Props = {
@@ -31,6 +32,8 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
const [dataSourceList, setDataSourceList] = useState<IDataSource.IDataSourceItem[]>([]);
const [tableData, setTableData] = useState<ISemantic.IMetricItem[]>([]);
const [filterParams, setFilterParams] = useState<Record<string, any>>({});
const [loading, setLoading] = useState<boolean>(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [dimensionValueSettingList, setDimensionValueSettingList] = useState<
@@ -38,38 +41,37 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
>([]);
const [dimensionValueSettingModalVisible, setDimensionValueSettingModalVisible] =
useState<boolean>(false);
const [pagination] = useState({
const defaultPagination = {
current: 1,
pageSize: 99999,
pageSize: 20,
total: 0,
});
};
const [pagination, setPagination] = useState(defaultPagination);
const actionRef = useRef<ActionType>();
const queryDimensionList = async (params: any) => {
setLoading(true);
const { code, data, msg } = await getDimensionList({
...params,
...pagination,
...params,
modelId,
});
setLoading(false);
const { list } = data || {};
let resData: any = {};
const { list, pageSize, pageNum, total } = data || {};
if (code === 200) {
resData = {
data: list || [],
success: true,
};
setPagination({
...pagination,
pageSize: Math.min(pageSize, 100),
current: pageNum,
total,
});
setTableData(list);
} else {
message.error(msg);
resData = {
data: [],
total: 0,
success: false,
};
setTableData([]);
}
return resData;
};
const queryDataSourceList = async () => {
@@ -81,25 +83,14 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
}
};
useEffect(() => {
queryDimensionList({ ...filterParams, ...defaultPagination });
}, [filterParams]);
useEffect(() => {
queryDataSourceList();
}, [modelId]);
// const updateDimensionStatus = async (dimensionData: ISemantic.IDimensionItem) => {
// const { code, msg } = await updateDimension(dimensionData);
// if (code === 200) {
// actionRef?.current?.reload();
// dispatch({
// type: 'domainManger/queryDimensionList',
// payload: {
// modelId,
// },
// });
// return;
// }
// message.error(msg);
// };
const queryBatchUpdateStatus = async (ids: React.Key[], status: StatusEnum) => {
if (Array.isArray(ids) && ids.length === 0) {
return;
@@ -111,7 +102,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
});
setLoading(false);
if (code === 200) {
actionRef?.current?.reload();
queryDimensionList({ ...filterParams, ...defaultPagination });
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
@@ -127,6 +118,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
{
dataIndex: 'id',
title: 'ID',
fixed: 'left',
width: 80,
order: 100,
search: false,
@@ -135,46 +127,26 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
dataIndex: 'key',
title: '维度搜索',
hideInTable: true,
renderFormItem: () => <Input placeholder="请输入ID/维度名称/英文名称" />,
},
{
dataIndex: 'name',
title: '维度名称',
title: '维度',
fixed: 'left',
width: 280,
render: ColumnsConfig.dimensionInfo.render,
search: false,
},
{
dataIndex: 'alias',
title: '别名',
width: 150,
ellipsis: true,
search: false,
},
{
dataIndex: 'bizName',
title: '英文名称',
search: false,
// order: 9,
},
{
dataIndex: 'sensitiveLevel',
title: '敏感度',
width: 80,
width: 150,
valueEnum: SENSITIVE_LEVEL_ENUM,
render: ColumnsConfig.sensitiveLevel.render,
},
{
dataIndex: 'isTag',
title: '是否为标签',
// search: false,
renderFormItem: () => (
<Select
placeholder="请选择标签状态"
allowClear
options={[
{ value: 1, label: '是' },
{ value: 0, label: '否' },
]}
/>
),
width: 120,
render: (isTag) => {
switch (isTag) {
case 0:
@@ -189,27 +161,14 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
{
dataIndex: 'status',
title: '状态',
width: 80,
width: 150,
search: false,
render: (status) => {
switch (status) {
case StatusEnum.ONLINE:
return <Tag color="success"></Tag>;
case StatusEnum.OFFLINE:
return <Tag color="warning"></Tag>;
case StatusEnum.INITIALIZED:
return <Tag color="processing"></Tag>;
case StatusEnum.DELETED:
return <Tag color="default"></Tag>;
default:
return <Tag color="default"></Tag>;
}
},
render: ColumnsConfig.state.render,
},
{
dataIndex: 'createdBy',
title: '创建人',
width: 100,
width: 180,
search: false,
},
@@ -217,6 +176,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
dataIndex: 'description',
title: '描述',
search: false,
render: ColumnsConfig.description.render,
},
{
@@ -233,7 +193,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
title: '操作',
dataIndex: 'x',
valueType: 'option',
width: 200,
width: 250,
render: (_, record) => {
return (
<Space className={styles.ctrlBtnContainer}>
@@ -292,7 +252,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const { code, msg } = await deleteDimension(record.id);
if (code === 200) {
setDimensionItem(undefined);
actionRef.current?.reload();
queryDimensionList({ ...filterParams, ...defaultPagination });
} else {
message.error(msg);
}
@@ -336,27 +296,102 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
return (
<>
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft} ${styles.disabledSearchTable}`}
actionRef={actionRef}
rowKey="id"
columns={columns}
request={queryDimensionList}
loading={loading}
search={{
defaultCollapsed: false,
collapseRender: () => {
return <></>;
},
}}
headerTitle={
<TableHeaderFilter
components={[
{
label: '维度搜索',
component: (
<Input.Search
style={{ width: 280 }}
placeholder="请输入ID/维度名称/英文名称"
onSearch={(value) => {
setFilterParams((preState) => {
return {
...preState,
key: value,
};
});
}}
/>
),
},
{
label: '敏感度',
component: (
<Select
style={{ width: 140 }}
options={SENSITIVE_LEVEL_OPTIONS}
placeholder="请选择敏感度"
allowClear
onChange={(value) => {
setFilterParams((preState) => {
return {
...preState,
sensitiveLevel: value,
};
});
}}
/>
),
},
{
label: '标签状态',
component: (
<Select
style={{ width: 145 }}
placeholder="请选择标签状态"
allowClear
onChange={(value) => {
setFilterParams((preState) => {
return {
...preState,
isTag: value,
};
});
}}
options={[
{ value: 1, label: '是' },
{ value: 0, label: '否' },
]}
/>
),
},
]}
/>
}
search={false}
// search={{
// optionRender: false,
// collapsed: false,
// }}
rowSelection={{
type: 'checkbox',
...rowSelection,
}}
dataSource={tableData}
pagination={pagination}
tableAlertRender={() => {
return false;
}}
size="small"
size="large"
scroll={{ x: 1500 }}
options={{ reload: false, density: false, fullScreen: false }}
onChange={(data: any) => {
const { current, pageSize, total } = data;
const currentPagin = {
current,
pageSize,
total,
};
setPagination(currentPagin);
queryDimensionList({ ...filterParams, ...currentPagin });
}}
toolBarRender={() => [
<Button
key="create"
@@ -388,7 +423,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
dataSourceList={dataSourceList}
onSubmit={() => {
setCreateModalVisible(false);
actionRef?.current?.reload();
queryDimensionList({ ...filterParams, ...defaultPagination });
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
@@ -417,7 +452,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
setDimensionValueSettingModalVisible(false);
}}
onSubmit={() => {
actionRef?.current?.reload();
queryDimensionList({ ...filterParams, ...defaultPagination });
dispatch({
type: 'domainManger/queryDimensionList',
payload: {

View File

@@ -13,9 +13,9 @@ import {
batchUpdateMetricStatus,
batchDownloadMetric,
} from '../service';
import MetricInfoCreateForm from './MetricInfoCreateForm';
import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
import TableHeaderFilter from './TableHeaderFilter';
import moment from 'moment';
import styles from './style.less';
import { ISemantic } from '../data';
@@ -39,6 +39,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
total: 0,
};
const [pagination, setPagination] = useState(defaultPagination);
const [filterParams, setFilterParams] = useState<Record<string, any>>({});
const actionRef = useRef<ActionType>();
@@ -94,10 +95,19 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
};
const columns: ProColumns[] = [
{
dataIndex: 'id',
title: 'ID',
width: 80,
fixed: 'left',
search: false,
},
{
dataIndex: 'name',
title: '指标',
width: '30%',
width: 280,
fixed: 'left',
// width: '30%',
search: false,
render: ColumnsConfig.metricInfo.render,
},
@@ -105,54 +115,35 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
dataIndex: 'key',
title: '指标搜索',
hideInTable: true,
renderFormItem: () => (
<Input.Search
placeholder="请输入ID/指标名称/英文名称/标签"
onSearch={(value) => {
setFilterParams((preState) => {
return {
...preState,
key: value,
};
});
}}
/>
),
},
{
dataIndex: 'sensitiveLevel',
title: '敏感度',
hideInTable: true,
width: 160,
valueEnum: SENSITIVE_LEVEL_ENUM,
renderFormItem: () => (
<Select
options={SENSITIVE_LEVEL_OPTIONS}
placeholder="请选择敏感度"
allowClear
onChange={(value) => {
setFilterParams((preState) => {
return {
...preState,
sensitiveLevel: value,
};
});
}}
/>
),
render: ColumnsConfig.sensitiveLevel.render,
},
{
dataIndex: 'description',
title: '描述',
width: 300,
search: false,
render: ColumnsConfig.description.render,
},
{
dataIndex: 'status',
title: '状态',
width: 200,
width: 160,
search: false,
render: ColumnsConfig.state.render,
},
{
dataIndex: 'createdBy',
title: '创建人',
width: 150,
search: false,
},
{
dataIndex: 'updatedAt',
title: '更新时间',
@@ -274,14 +265,53 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
return (
<>
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft} ${styles.disabledSearchTable} `}
actionRef={actionRef}
headerTitle={
<TableHeaderFilter
components={[
{
label: '指标搜索',
component: (
<Input.Search
style={{ width: 280 }}
placeholder="请输入ID/指标名称/英文名称/标签"
onSearch={(value) => {
setFilterParams((preState) => {
return {
...preState,
key: value,
};
});
}}
/>
),
},
{
label: '敏感度',
component: (
<Select
style={{ width: 140 }}
options={SENSITIVE_LEVEL_OPTIONS}
placeholder="请选择敏感度"
allowClear
onChange={(value) => {
setFilterParams((preState) => {
return {
...preState,
sensitiveLevel: value,
};
});
}}
/>
),
},
]}
/>
}
rowKey="id"
loading={loading}
search={{
optionRender: false,
collapsed: false,
}}
search={false}
rowSelection={{
type: 'checkbox',
...rowSelection,
@@ -303,6 +333,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
setPagination(currentPagin);
queryMetricList({ ...filterParams, ...currentPagin });
}}
scroll={{ x: 1500 }}
sticky={{ offsetHeader: 0 }}
size="large"
options={{ reload: false, density: false, fullScreen: false }}

View File

@@ -120,7 +120,7 @@ const DatabaseTable: React.FC<Props> = ({}) => {
return (
<div style={{ margin: 20 }}>
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
// className={`${styles.classTable}`}
actionRef={actionRef}
rowKey="id"
columns={columns}

View File

@@ -11,6 +11,7 @@ type Props = {
metricItem?: ISemantic.IMetricItem;
relationsInitialValue?: ISemantic.IDrillDownDimensionItem[];
onSubmit: (relations: ISemantic.IDrillDownDimensionItem[]) => void;
onRefreshRelationData?: () => void;
};
const DimensionAndMetricRelationModal: React.FC<Props> = ({
@@ -19,6 +20,7 @@ const DimensionAndMetricRelationModal: React.FC<Props> = ({
relationsInitialValue,
onCancel,
onSubmit,
onRefreshRelationData,
}) => {
const [relationList, setRelationList] = useState<ISemantic.IDrillDownDimensionItem[]>([]);
@@ -30,9 +32,11 @@ const DimensionAndMetricRelationModal: React.FC<Props> = ({
drillDownDimensions: relationList,
},
};
const { code, msg } = await updateMetric(queryParams);
if (code === 200) {
onSubmit(relationList);
onRefreshRelationData?.();
return;
}
message.error(msg);

View File

@@ -1,4 +1,4 @@
import { Table, Transfer, Checkbox, message } from 'antd';
import { Table, Transfer, Checkbox, message, Tooltip } from 'antd';
import type { ColumnsType, TableRowSelection } from 'antd/es/table/interface';
import type { TransferItem } from 'antd/es/transfer';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
@@ -9,13 +9,15 @@ import type { StateType } from '../model';
import TransTypeTag from './TransTypeTag';
import TableTitleTooltips from '../components/TableTitleTooltips';
import { ISemantic } from '../data';
import { getDimensionList, getDimensionInModelCluster } from '../service';
import { EnvironmentOutlined } from '@ant-design/icons';
import { getDimensionInModelCluster } from '../service';
import { SemanticNodeType, TransType } from '../enum';
interface RecordType {
id: number;
key: string;
name: string;
disabled?: boolean;
transType: TransType.DIMENSION | TransType.METRIC;
}
@@ -39,7 +41,7 @@ const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
);
const [dimensionList, setDimensionList] = useState<ISemantic.IDimensionItem[]>([]);
const [transferData, setTransferData] = useState<RecordType[]>([]);
useEffect(() => {
queryDimensionList();
}, [metricItem, relationsInitialValue]);
@@ -53,16 +55,29 @@ const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
}
};
useEffect(() => {
const data = dimensionList.map((item) => {
const transType = TransType.DIMENSION;
const { id } = item;
return {
...item,
transType,
disabled: checkedMap[id]?.inheritFromModel,
key: `${id}`,
};
});
setTransferData(data);
}, [checkedMap, dimensionList]);
useEffect(() => {
if (!Array.isArray(relationsInitialValue)) {
return;
}
const ids = relationsInitialValue.map((item) => `${item.dimensionId}`);
const relationMap = relationsInitialValue.reduce((relationCheckedMap, item: any) => {
const { dimensionId, necessary } = item;
const { dimensionId } = item;
relationCheckedMap[dimensionId] = {
dimensionId: Number(dimensionId),
necessary: necessary,
...item,
};
return relationCheckedMap;
}, {});
@@ -101,11 +116,15 @@ const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
(relationList: ISemantic.IDrillDownDimensionItem[], dimensionId: string) => {
const target = relationCheckedMap[dimensionId];
if (target) {
if (target.inheritFromModel === true && !target.necessary) {
return relationList;
}
relationList.push(target);
} else {
relationList.push({
dimensionId: Number(dimensionId),
necessary: false,
inheritFromModel: false,
});
}
return relationList;
@@ -175,15 +194,7 @@ const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
<Transfer
showSearch
titles={['未关联维度', '已关联维度']}
dataSource={dimensionList.map((item) => {
const transType = TransType.DIMENSION;
const { id } = item;
return {
...item,
transType,
key: `${id}`,
};
})}
dataSource={transferData}
listStyle={{
width: 500,
height: 600,
@@ -207,11 +218,16 @@ const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
onItemSelectAll,
onItemSelect,
selectedKeys: listSelectedKeys,
disabled: listDisabled,
}) => {
const columns = direction === 'left' ? leftColumns : rightColumns;
const rowSelection: TableRowSelection<TransferItem> = {
getCheckboxProps: (item) => ({ disabled: listDisabled || item.disabled }),
onSelectAll(selected, selectedRows) {
const treeSelectedKeys = selectedRows.map(({ key }) => key);
const treeSelectedKeys = selectedRows
.filter((item) => !item.disabled)
.map(({ key }) => key);
const diffKeys = selected
? difference(treeSelectedKeys, listSelectedKeys)
: difference(listSelectedKeys, treeSelectedKeys);
@@ -221,6 +237,16 @@ const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
onItemSelect(key as string, selected);
},
selectedRowKeys: listSelectedKeys,
renderCell: function (checked, record, index, originNode) {
if (checkedMap[record.id]?.inheritFromModel === true) {
return (
<Tooltip title="来自模型默认设置维度">
<EnvironmentOutlined style={{ color: '#0958d9' }} />
</Tooltip>
);
}
return originNode;
},
};
return (
@@ -229,10 +255,17 @@ const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
columns={columns}
dataSource={filteredItems as any}
size="small"
rowClassName={(record) => {
if (checkedMap[record.id]?.inheritFromModel) {
return 'inherit-from-model-row';
}
return '';
}}
pagination={false}
scroll={{ y: 450 }}
onRow={({ key }) => ({
onRow={({ key, disabled: itemDisabled }) => ({
onClick: () => {
if (itemDisabled || listDisabled) return;
onItemSelect(key as string, !listSelectedKeys.includes(key as string));
},
})}

View File

@@ -114,7 +114,7 @@ const DomainListTree: FC<DomainListProps> = ({
};
const titleRender = (node: any) => {
const { id, name, path, hasEditPermission } = node as any;
const { id, name, path, hasEditPermission, parentId } = node as any;
return (
<div className={styles.projectItem}>
<span
@@ -126,7 +126,7 @@ const DomainListTree: FC<DomainListProps> = ({
{name}
</span>
{createDomainBtnVisible && hasEditPermission && (
<span className={styles.operation}>
<span className={`${styles.operation} ${parentId ? styles.rowHover : ''}`}>
{Array.isArray(path) && path.length < 2 && (
<PlusOutlined
className={styles.icon}
@@ -179,7 +179,7 @@ const DomainListTree: FC<DomainListProps> = ({
return (
<div className={styles.domainList}>
<div className={styles.searchContainer}>
<Row style={{ gap: 20 }}>
<Row style={{ gap: 10 }}>
<Col flex="1 1 215px">
<Search
allowClear

View File

@@ -13,8 +13,9 @@ import { HomeOutlined, FundViewOutlined } from '@ant-design/icons';
import { ISemantic } from '../data';
import SemanticGraphCanvas from '../SemanticGraphCanvas';
import RecommendedQuestionsSection from '../components/Entity/RecommendedQuestionsSection';
import CommonDimensionTable from './CommonDimension/CommonDimensionTable';
import DatabaseTable from '../components/Database/DatabaseTable';
import View from '../View';
// import CommonDimensionTable from './CommonDimension/CommonDimensionTable';
// import DatabaseTable from '../components/Database/DatabaseTable';
import type { Dispatch } from 'umi';
@@ -54,6 +55,18 @@ const DomainManagerTab: React.FC<Props> = ({
/>
),
},
{
label: '视图管理',
key: 'viewManange',
children: (
<View
modelList={modelList}
onModelChange={(model) => {
handleModelChange(model);
}}
/>
),
},
{
label: '画布',
key: 'xflow',
@@ -68,11 +81,11 @@ const DomainManagerTab: React.FC<Props> = ({
key: 'permissonSetting',
children: <PermissionSection permissionTarget={'domain'} />,
},
{
label: '数据库管理',
key: 'database',
children: <DatabaseTable />,
},
// {
// label: '数据库管理',
// key: 'database',
// children: <DatabaseTable />,
// },
// {
// label: '公共维度',
// key: 'commonDimension',
@@ -88,12 +101,12 @@ const DomainManagerTab: React.FC<Props> = ({
const isModelItem = [
{
label: '指标',
label: '指标管理',
key: 'metric',
children: <ClassMetricTable />,
},
{
label: '维度',
label: '维度管理',
key: 'dimenstion',
children: <ClassDimensionTable />,
},

View File

@@ -23,12 +23,8 @@ type Props = {
[key: string]: any;
};
// type TaskStateMap = Record<string, DictTaskState>;
const DimensionMetricVisibleTableTransfer: React.FC<Props> = ({
// domainManger,
knowledgeInfosMap,
// onKnowledgeInfosMapChange,
...restProps
}) => {
let rightColumns: ColumnsType<RecordType> = [
@@ -58,6 +54,11 @@ const DimensionMetricVisibleTableTransfer: React.FC<Props> = ({
return <TransTypeTag type={type} />;
},
},
{
dataIndex: 'modelName',
title: '所属模型',
},
];
if (!knowledgeInfosMap) {
rightColumns = leftColumns;

View File

@@ -3,7 +3,7 @@ import { IChatConfig } from '../../data';
import DimensionMetricVisibleTableTransfer from './DimensionMetricVisibleTableTransfer';
interface RecordType {
key: string;
key: React.Key;
name: string;
type: 'dimension' | 'metric';
}
@@ -11,11 +11,10 @@ interface RecordType {
type Props = {
knowledgeInfosMap?: IChatConfig.IKnowledgeInfosItemMap;
sourceList: any[];
targetList: string[];
targetList: React.Key[];
titles?: string[];
onKnowledgeInfosMapChange?: (knowledgeInfosMap: IChatConfig.IKnowledgeInfosItemMap) => void;
onChange?: (params?: any) => void;
// transferProps?: Record<string, any>;
[key: string]: any;
};
@@ -29,25 +28,27 @@ const DimensionMetricVisibleTransfer: React.FC<Props> = ({
...rest
}) => {
const [transferData, setTransferData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>(targetList);
const [targetKeys, setTargetKeys] = useState<React.Key[]>(targetList);
useEffect(() => {
setTransferData(
sourceList.map(({ key, id, name, bizName, transType }) => {
sourceList.map(({ key, id, name, bizName, transType, modelName }) => {
return {
key,
name,
bizName,
id,
modelName,
type: transType,
};
}),
);
}, [sourceList]);
useEffect(() => {
setTargetKeys(targetList);
}, [targetList]);
const keyList: React.Key[] = sourceList.map((item) => item.key);
const filterTargetList = targetList.filter((key: React.Key) => {
return keyList.includes(key);
});
setTargetKeys(filterTargetList);
}, [sourceList, targetList]);
const handleChange = (newTargetKeys: string[]) => {
setTargetKeys(newTargetKeys);

View File

@@ -1,6 +1,5 @@
import { message, Space } from 'antd';
import { useEffect, useState, forwardRef, useImperativeHandle, Ref } from 'react';
import type { ForwardRefRenderFunction } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';

View File

@@ -23,7 +23,7 @@ import { SENSITIVE_LEVEL_OPTIONS, METRIC_DEFINE_TYPE } from '../constant';
import { formLayout } from '@/components/FormHelper/utils';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import styles from './style.less';
import { getMetricsToCreateNewMetric, getModelDetail } from '../service';
import { getMetricsToCreateNewMetric, getModelDetail, getDrillDownDimension } from '../service';
import MetricMetricFormTable from './MetricMetricFormTable';
import MetricFieldFormTable from './MetricFieldFormTable';
import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal';
@@ -31,7 +31,6 @@ import TableTitleTooltips from '../components/TableTitleTooltips';
import { createMetric, updateMetric, mockMetricAlias, getMetricTags } from '../service';
import { ISemantic } from '../data';
import { history } from 'umi';
import { cloneDeep } from 'lodash';
export type CreateFormProps = {
datasourceId?: number;
@@ -113,7 +112,11 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
const [drillDownDimensions, setDrillDownDimensions] = useState<
ISemantic.IDrillDownDimensionItem[]
>(metricItem?.relateDimension?.drillDownDimensions || []);
>([]);
const [drillDownDimensionsConfig, setDrillDownDimensionsConfig] = useState<
ISemantic.IDrillDownDimensionItem[]
>([]);
const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1);
@@ -139,6 +142,17 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
setClassMeasureList([]);
};
const queryDrillDownDimension = async (metricId: number) => {
const { code, data, msg } = await getDrillDownDimension(metricId);
if (code === 200 && Array.isArray(data)) {
setDrillDownDimensionsConfig(data);
}
if (code !== 200) {
message.error(msg);
}
return [];
};
useEffect(() => {
queryModelDetail();
queryMetricsToCreateNewMetric();
@@ -234,6 +248,7 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
setDefineType(metricDefineType);
setIsPercentState(isPercent);
setIsDecimalState(isDecimal);
queryDrillDownDimension(metricItem?.id);
};
useEffect(() => {
@@ -719,7 +734,7 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
</Form>
<DimensionAndMetricRelationModal
metricItem={metricItem}
relationsInitialValue={drillDownDimensions}
relationsInitialValue={drillDownDimensionsConfig}
open={metricRelationModalOpenState}
onCancel={() => {
setMetricRelationModalOpenState(false);
@@ -728,6 +743,9 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
setDrillDownDimensions(relations);
setMetricRelationModalOpenState(false);
}}
onRefreshRelationData={() => {
queryDrillDownDimension(metricItem?.id);
}}
/>
</>
) : (

View File

@@ -1,7 +1,7 @@
import { Space, Tag, Typography } from 'antd';
import { StatusEnum } from '../enum';
import { SENSITIVE_LEVEL_ENUM, SENSITIVE_LEVEL_COLOR } from '../constant';
import { TagsOutlined, UserOutlined, FieldNumberOutlined, ReadOutlined } from '@ant-design/icons';
import { TagsOutlined, ReadOutlined } from '@ant-design/icons';
import { history } from 'umi';
import { ISemantic } from '../data';
import { isString } from 'lodash';
@@ -9,51 +9,167 @@ import { isArrayOfValues } from '@/utils/utils';
import styles from './style.less';
import MetricStar from '../Metric/components/MetricStar';
const { Text, Link } = Typography;
const { Text, Paragraph } = Typography;
export const ColumnsConfig = {
metricInfo: {
description: {
render: (_, record: ISemantic.IMetricItem) => {
const { name, alias, bizName, sensitiveLevel, createdBy, tags, id, isCollect } = record;
const { description } = record;
return (
<Paragraph
ellipsis={{ tooltip: description, rows: 3 }}
style={{ width: 250, marginBottom: 0 }}
>
{description}
</Paragraph>
);
},
},
dimensionInfo: {
render: (_, record: ISemantic.IDimensionItem) => {
const { name, alias, bizName } = record;
return (
<>
<div>
<Space>
<MetricStar metricId={id} initState={isCollect} />
<span style={{ fontWeight: 500 }}>{name}</span>
</Space>
</div>
<div style={{ color: '#5f748d', fontSize: 14, marginTop: 5, marginLeft: 0 }}>
{bizName}
</div>
{alias && (
<div style={{ marginTop: 8 }}>
<Space direction="vertical" size={4}>
{alias && (
<Space size={4} style={{ color: '#5f748d', fontSize: 12, margin: '5px 0 5px 0' }}>
<ReadOutlined />
<div style={{ width: 'max-content' }}>:</div>
<span style={{ marginLeft: 2 }}>
<Space size={[0, 8]} wrap>
{isString(alias) &&
alias.split(',').map((aliasName: string) => {
return (
<Tag
color="#eee"
key={aliasName}
style={{
borderRadius: 44,
maxWidth: 90,
minWidth: 40,
backgroundColor: 'rgba(18, 31, 67, 0.04)',
}}
>
<Text
style={{
maxWidth: 80,
color: 'rgb(95, 116, 141)',
textAlign: 'center',
fontSize: 12,
}}
ellipsis={{ tooltip: aliasName }}
>
{aliasName}
</Text>
</Tag>
);
})}
</Space>
</span>
</Space>
)}
</Space>
</div>
)}
</>
);
},
},
metricInfo: {
render: (_, record: ISemantic.IMetricItem) => {
const { name, alias, bizName, tags, id, isCollect } = record;
return (
<>
<div>
<Space>
<a
className={styles.textLink}
style={{ fontWeight: 500, marginRight: 10 }}
onClick={() => {
style={{ fontWeight: 500 }}
onClick={(event: any) => {
history.push(`/metric/detail/${id}`);
event.preventDefault();
event.stopPropagation();
}}
href={`/webapp/metric/detail/${id}`}
>
{name}
</a>
<Tag
<MetricStar metricId={id} initState={isCollect} />
{/* <Tag
color={SENSITIVE_LEVEL_COLOR[sensitiveLevel]}
style={{ lineHeight: '16px', position: 'relative', top: '-1px' }}
>
{SENSITIVE_LEVEL_ENUM[sensitiveLevel]}
</Tag>
</Tag> */}
</Space>
</div>
<div style={{ color: '#5f748d', fontSize: 14, marginTop: 5 }}>{bizName}</div>
<div style={{ color: '#5f748d', fontSize: 14, marginTop: 5, marginLeft: 0 }}>
{bizName}
</div>
<div style={{ marginTop: 8 }}>
<Space direction="vertical" size={4}>
{alias && (
<Space size={4} style={{ color: '#5f748d', fontSize: 12 }}>
<ReadOutlined />
<div style={{ width: 'max-content' }}>:</div>
<span style={{ marginLeft: 2 }}>
<Space size={[0, 8]} wrap>
{isString(alias) &&
alias.split(',').map((aliasName: string) => {
{(alias || isArrayOfValues(tags)) && (
<div style={{ marginTop: 8 }}>
<Space direction="vertical" size={4}>
{alias && (
<Space size={4} style={{ color: '#5f748d', fontSize: 12, margin: '5px 0 5px 0' }}>
<ReadOutlined />
<div style={{ width: 'max-content' }}>:</div>
<span style={{ marginLeft: 2 }}>
<Space size={[0, 8]} wrap>
{isString(alias) &&
alias.split(',').map((aliasName: string) => {
return (
<Tag
color="#eee"
key={aliasName}
style={{
borderRadius: 44,
maxWidth: 90,
minWidth: 40,
backgroundColor: 'rgba(18, 31, 67, 0.04)',
}}
>
<Text
style={{
maxWidth: 80,
color: 'rgb(95, 116, 141)',
textAlign: 'center',
fontSize: 12,
}}
ellipsis={{ tooltip: aliasName }}
>
{aliasName}
</Text>
</Tag>
);
})}
</Space>
</span>
</Space>
)}
{isArrayOfValues(tags) && (
<Space size={4} style={{ color: '#5f748d', fontSize: 12, margin: '5px 0 5px 0' }}>
<TagsOutlined />
<div style={{ width: 'max-content' }}>:</div>
<span style={{ marginLeft: 2 }}>
<Space size={[0, 8]} wrap>
{tags.map((tag: string) => {
return (
<Tag
color="#eee"
key={aliasName}
key={tag}
style={{
borderRadius: 44,
maxWidth: 90,
@@ -68,93 +184,90 @@ export const ColumnsConfig = {
textAlign: 'center',
fontSize: 12,
}}
ellipsis={{ tooltip: aliasName }}
ellipsis={{ tooltip: tag }}
>
{aliasName}
{tag}
</Text>
</Tag>
);
})}
</Space>
</span>
</Space>
)}
{isArrayOfValues(tags) && (
<Space size={2} style={{ color: '#5f748d', fontSize: 12 }}>
<TagsOutlined />
<div style={{ width: 'max-content' }}>:</div>
<span style={{ marginLeft: 2 }}>
<Space size={[0, 8]} wrap>
{tags.map((tag: string) => {
return (
<Tag
color="#eee"
key={tag}
style={{
borderRadius: 44,
maxWidth: 90,
minWidth: 40,
backgroundColor: 'rgba(18, 31, 67, 0.04)',
}}
>
<Text
style={{
maxWidth: 80,
color: 'rgb(95, 116, 141)',
textAlign: 'center',
fontSize: 12,
}}
ellipsis={{ tooltip: tag }}
>
{tag}
</Text>
</Tag>
);
})}
</Space>
</span>
</Space>
)}
<Space size={10}>
<Space
size={2}
style={{ color: '#5f748d', fontSize: 12, position: 'relative', top: '1px' }}
>
<FieldNumberOutlined style={{ fontSize: 16, position: 'relative', top: '1px' }} />
<span>:</span>
<span style={{ marginLeft: 0 }}>{id}</span>
</Space>
<Space size={2} style={{ color: '#5f748d', fontSize: 12 }}>
<UserOutlined />
<span>:</span>
<span style={{ marginLeft: 0 }}>{createdBy}</span>
</Space>
</Space>
</span>
</Space>
)}
{/* <Space size={10}>
<Space
size={2}
style={{ color: '#5f748d', fontSize: 12, position: 'relative', top: '1px' }}
>
<FieldNumberOutlined style={{ fontSize: 16, position: 'relative', top: '1px' }} />
<span>:</span>
<span style={{ marginLeft: 0 }}>{id}</span>
</Space>
<Space size={2} style={{ color: '#5f748d', fontSize: 12 }}>
<UserOutlined />
<span>:</span>
<span style={{ marginLeft: 0 }}>{createdBy}</span>
</Space>
</Space> */}
</Space>
</Space>
</div>
</div>
)}
</>
);
},
},
sensitiveLevel: {
render: (_, record: ISemantic.IMetricItem) => {
const { sensitiveLevel } = record;
return SENSITIVE_LEVEL_COLOR[sensitiveLevel] ? (
<Tag
color={SENSITIVE_LEVEL_COLOR[sensitiveLevel]}
style={{
borderRadius: '40px',
padding: '2px 16px',
fontSize: '13px',
// color: '#8ca3ba',
}}
>
{SENSITIVE_LEVEL_ENUM[sensitiveLevel]}
</Tag>
) : (
<Tag
style={{
borderRadius: '40px',
padding: '2px 16px',
fontSize: '13px',
}}
>
</Tag>
);
},
},
state: {
render: (status) => {
let tagProps = {
let tagProps: { color: string; label: string; style?: any } = {
color: 'default',
label: '未知',
style: {},
};
switch (status) {
case StatusEnum.ONLINE:
tagProps = {
color: 'success',
// color: '#87d068',
// color: 'success',
color: 'geekblue',
label: '已启用',
};
break;
case StatusEnum.OFFLINE:
tagProps = {
color: 'warning',
color: 'default',
label: '未启用',
style: {
color: 'rgb(95, 116, 141)',
fontWeight: 400,
},
};
break;
case StatusEnum.INITIALIZED:
@@ -180,6 +293,7 @@ export const ColumnsConfig = {
padding: '2px 16px',
fontSize: '13px',
fontWeight: 500,
...tagProps.style,
}}
>
{tagProps.label}

View File

@@ -8,6 +8,7 @@ import { connect } from 'umi';
import type { StateType } from '../model';
import { deleteModel, updateModel } from '../service';
import ClassDataSourceTypeModal from './ClassDataSourceTypeModal';
import { ColumnsConfig } from './MetricTableColumnRender';
import moment from 'moment';
import styles from './style.less';
@@ -85,20 +86,7 @@ const ModelTable: React.FC<Props> = ({ modelList, disabledEdit = false, onModelC
dataIndex: 'status',
title: '状态',
search: false,
render: (status) => {
switch (status) {
case StatusEnum.ONLINE:
return <Tag color="success"></Tag>;
case StatusEnum.OFFLINE:
return <Tag color="warning"></Tag>;
case StatusEnum.INITIALIZED:
return <Tag color="processing"></Tag>;
case StatusEnum.DELETED:
return <Tag color="default"></Tag>;
default:
return <Tag color="default"></Tag>;
}
},
render: ColumnsConfig.state.render,
},
{
dataIndex: 'createdBy',

View File

@@ -15,7 +15,7 @@ type Props = {
const OverView: React.FC<Props> = ({ modelList, disabledEdit = false, onModelChange }) => {
return (
<div style={{ padding: '20px' }}>
<div style={{ padding: '15px 20px' }}>
<ModelTable modelList={modelList} disabledEdit={disabledEdit} onModelChange={onModelChange} />
</div>
);

View File

@@ -102,7 +102,7 @@ const PermissionAdminForm: React.FC<Props> = ({
>
<SelectTMEPerson placeholder="请邀请团队成员" />
</FormItem>
{/* {APP_TARGET === 'inner' && ( */}
{/* {APP_TARGET === 'inner'} */}
<FormItem name="adminOrgs" label="按组织">
<SelectPartner
type="selectedDepartment"
@@ -111,7 +111,6 @@ const PermissionAdminForm: React.FC<Props> = ({
}}
/>
</FormItem>
{/* )} */}
<Form.Item
label={
<FormItemTitle
@@ -128,7 +127,7 @@ const PermissionAdminForm: React.FC<Props> = ({
</Form.Item>
{!isOpenState && (
<>
{/* {APP_TARGET === 'inner' && ( */}
{/* {APP_TARGET === 'inner' && } */}
<FormItem name="viewOrgs" label="按组织">
<SelectPartner
type="selectedDepartment"
@@ -137,7 +136,6 @@ const PermissionAdminForm: React.FC<Props> = ({
}}
/>
</FormItem>
{/* )} */}
<FormItem name="viewers" label="按个人">
<SelectTMEPerson placeholder="请选择需要授权的个人" />
</FormItem>

View File

@@ -32,7 +32,6 @@ const PermissionCreateDrawer: React.FC<Props> = ({
const basicInfoFormRef = useRef<any>(null);
const [selectedDimensionKeyList, setSelectedDimensionKeyList] = useState<string[]>([]);
const [selectedMetricKeyList, setSelectedMetricKeyList] = useState<string[]>([]);
const [selectedKeyList, setSelectedKeyList] = useState<string[]>([]);
const saveAuth = async () => {

View File

@@ -8,7 +8,7 @@ import styles from '../style.less';
type Props = {
permissonData: any;
onSubmit?: (data?: any) => void;
onValuesChange?: (value, values) => void;
onValuesChange?: (value: any, values: any) => void;
};
const FormItem = Form.Item;
@@ -17,7 +17,6 @@ const PermissionCreateForm: ForwardRefRenderFunction<any, Props> = (
{ permissonData, onValuesChange },
ref,
) => {
const { APP_TARGET } = process.env;
const [form] = Form.useForm();
useImperativeHandle(ref, () => ({
@@ -51,7 +50,6 @@ const PermissionCreateForm: ForwardRefRenderFunction<any, Props> = (
<FormItem name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="请输入名称" />
</FormItem>
{/* {APP_TARGET === 'inner' && ( */}
<FormItem name="authorizedDepartmentIds" label="按组织">
<SelectPartner
type="selectedDepartment"
@@ -60,7 +58,6 @@ const PermissionCreateForm: ForwardRefRenderFunction<any, Props> = (
}}
/>
</FormItem>
{/* )} */}
<FormItem name="authorizedUsers" label="按个人">
<SelectTMEPerson placeholder="请选择需要授权的个人" />

View File

@@ -0,0 +1,28 @@
import { Space } from 'antd';
import React, { ReactNode } from 'react';
import styles from './style.less';
type Props = {
components: {
label: string;
component: ReactNode;
}[];
};
const TableHeaderFilter: React.FC<Props> = ({ components }) => {
return (
<>
<Space className={styles.tableHeaderTitle} size={20}>
{components.map(({ label, component }) => (
<Space size={8} key={`TableHeaderFilter-${label}`}>
<span className={styles.headerTitleLabel}>{label}:</span>
{component}
</Space>
))}
</Space>
</>
);
};
export default TableHeaderFilter;

View File

@@ -75,44 +75,47 @@
line-height: 34px;
text-align: right;
background: #fff;
// border-bottom: 1px solid #d9d9d9;
.title {
float: left;
}
}
.search {
// width: calc(100% - 100px);
width: 100%;
margin: 10px 0 10px 10px;
width: 205px;
margin: 10px 0 10px 16px;
}
.tree {
flex: 1;
padding: 10px;
padding-right: 18px;
.projectItem {
display: flex;
width: 100%;
cursor: auto;
line-height: 30px;
height: 30px;
line-height: 25px;
height: 25px;
color: rgb(95, 116, 141);
font-weight: 500;
.projectItemTitle {
font-size: 16px;
font-size: 15px;
flex: 1;
cursor: pointer;
}
.operation {
.icon {
margin-left: 6px;
cursor: pointer;
}
}
.rowHover {
display: none;
}
&:hover {
.rowHover {
display: block;
}
}
}
}
}
@@ -179,6 +182,8 @@
}
.ant-tabs-tab {
padding-bottom:15px;
color: rgb(95, 116, 141);
font-weight: 500;
}
}
.backBtn {
@@ -237,6 +242,10 @@
}
.disabledSearchTable {
padding-top:20px;
}
.classTable {
// padding: 0 20px;
:global {
@@ -419,4 +428,15 @@
&:hover {
color: #69b1ff;
}
}
.tableHeaderTitle {
margin-left: 20px;
.headerTitleLabel {
color: #667085;
font-size: 14px;
font-weight: 400;
width: max-content;
display: flex;
}
}

View File

@@ -37,9 +37,11 @@ export const IS_TAG_ENUM = {
};
export const SENSITIVE_LEVEL_COLOR = {
[SENSITIVE_LEVEL.LOW]: 'geekblue',
[SENSITIVE_LEVEL.MID]: 'warning',
[SENSITIVE_LEVEL.HIGH]: 'error',
[SENSITIVE_LEVEL.LOW]: 'default',
// [SENSITIVE_LEVEL.MID]: 'orange',
[SENSITIVE_LEVEL.MID]: 'geekblue',
[SENSITIVE_LEVEL.HIGH]: 'volcano',
// [SENSITIVE_LEVEL.HIGH]: '#1677ff',
};
export const SEMANTIC_NODE_TYPE_CONFIG = {

View File

@@ -139,6 +139,31 @@ export declare namespace ISemantic {
updatedAt: ISODateString;
}
interface IViewModelConfigItem {
id: number;
includesAll: boolean;
metrics: number[];
dimensions: number[];
}
interface IViewItem {
createdBy: UserName;
updatedBy: UserName;
createdAt: ISODateString;
updatedAt: ISODateString;
id: number;
name: string;
bizName: string;
description: string;
status?: StatusEnum;
typeEnum?: any;
sensitiveLevel: number;
domainId: number;
viewDetail: {
viewModelConfigs: IViewModelConfigItem[];
};
}
interface IDimensionItem {
createdBy: string;
updatedBy: string;
@@ -209,6 +234,7 @@ export declare namespace ISemantic {
interface IDrillDownDimensionItem {
dimensionId: number;
inheritFromModel?: boolean;
necessary?: boolean;
}

View File

@@ -4,14 +4,14 @@ export enum ChatConfigType {
}
export enum TransType {
DIMENSION = 'dimension',
METRIC = 'metric',
DIMENSION = 'DIMENSION',
METRIC = 'METRIC',
}
export enum SemanticNodeType {
DATASOURCE = 'datasource',
DIMENSION = 'dimension',
METRIC = 'metric',
DATASOURCE = 'DATASOURCE',
DIMENSION = 'DIMENSION',
METRIC = 'METRIC',
}
export enum MetricTypeWording {

View File

@@ -514,7 +514,7 @@ export async function queryStruct({
args: null,
},
],
orders: [],
orders: [{ column: dateField, direction: 'desc' }],
metricFilters: [],
params: [],
dateInfo: {
@@ -560,3 +560,29 @@ export function getDatabaseParameters(): Promise<any> {
export function getDatabaseDetail(id: number): Promise<any> {
return request.get(`${process.env.API_BASE_URL}database/${id}`);
}
export function getViewList(domainId: number): Promise<any> {
return request(`${process.env.API_BASE_URL}view/getViewList`, {
method: 'GET',
params: { domainId },
});
}
export function createView(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}view`, {
method: 'POST',
data,
});
}
export function updateView(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}view`, {
method: 'PUT',
data,
});
}
export function deleteView(viewId: number): Promise<any> {
return request(`${process.env.API_BASE_URL}view/${viewId}`, {
method: 'DELETE',
});
}

View File

@@ -3,6 +3,7 @@ import { ISemantic } from './data';
import type { DataNode } from 'antd/lib/tree';
import { Form, Input, InputNumber, Switch, Select } from 'antd';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import DisabledWheelNumberInput from '@/components/DisabledWheelNumberInput';
import { ConfigParametersItem } from '../System/types';
const FormItem = Form.Item;
const { TextArea } = Input;
@@ -20,7 +21,7 @@ export const changeTreeData = (treeData: API.DomainList, auth?: boolean): DataNo
};
export const addPathInTreeData = (treeData: API.DomainList, loopPath: any[] = []): any => {
return treeData.map((item: any) => {
return treeData?.map((item: any) => {
const { children, parentId = [] } = item;
const path = loopPath.slice();
path.push(parentId);
@@ -39,7 +40,7 @@ export const addPathInTreeData = (treeData: API.DomainList, loopPath: any[] = []
};
export const constructorClassTreeFromList = (list: any[], parentId: number = 0) => {
const tree = list.reduce((nodeList, nodeItem) => {
const tree = list?.reduce((nodeList, nodeItem) => {
if (nodeItem.parentId == parentId) {
const children = constructorClassTreeFromList(list, nodeItem.id);
if (children.length) {
@@ -47,7 +48,7 @@ export const constructorClassTreeFromList = (list: any[], parentId: number = 0)
}
nodeItem.key = nodeItem.id;
nodeItem.value = nodeItem.id;
nodeItem.title = nodeItem.name;
nodeItem.title = nodeItem.name || nodeItem.categoryName;
nodeList.push(nodeItem);
}
return nodeList;
@@ -57,7 +58,7 @@ export const constructorClassTreeFromList = (list: any[], parentId: number = 0)
export const treeParentKeyLists = (treeData: API.DomainList): string[] => {
let keys: string[] = [];
treeData.forEach((item: any) => {
treeData?.forEach((item: any) => {
if (item.children && item.children.length > 0) {
keys.push(item.id);
keys = keys.concat(treeParentKeyLists(item.children));
@@ -133,7 +134,8 @@ export const findLeafNodesFromDomainList = (
export const genneratorFormItemList = (itemList: ConfigParametersItem[]) => {
return itemList.map((item) => {
const { dataType, name, comment, placeholder, description, require } = item;
const { dataType, name, comment, placeholder, description, require, value } = item;
let defaultItem = <Input />;
switch (dataType) {
case 'string':
@@ -148,7 +150,10 @@ export const genneratorFormItemList = (itemList: ConfigParametersItem[]) => {
defaultItem = <TextArea placeholder={placeholder} style={{ height: 100 }} />;
break;
case 'number':
defaultItem = <InputNumber placeholder={placeholder} style={{ width: '100%' }} />;
// defaultItem = <InputNumber placeholder={placeholder} style={{ width: '100%' }} />;
defaultItem = (
<DisabledWheelNumberInput placeholder={placeholder} style={{ width: '100%' }} />
);
break;
case 'bool':
return (

Binary file not shown.