mirror of
https://github.com/tencentmusic/supersonic.git
synced 2026-04-19 04:44:19 +08:00
[improvement][semantic-fe] Added field type and metric type to the metric creation options. (#655)
* [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.
This commit is contained in:
@@ -74,7 +74,7 @@ const BindMeasuresTable: React.FC<CreateFormProps> = ({
|
||||
<>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
将选中度量添加到指标
|
||||
添加度量
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
|
||||
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState<boolean>(false);
|
||||
const [dataSourceEditOpen, setDataSourceEditOpen] = useState<boolean>(false);
|
||||
const [currentDatabaseId, setCurrentDatabaseId] = useState<number>();
|
||||
const [scriptColumns, setScriptColumns] = useState<any[]>([]);
|
||||
const [scriptColumns, setScriptColumns] = useState<IDataSource.IExecuteSqlColumn[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataSourceItem?.id || !open) {
|
||||
@@ -74,24 +74,36 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
|
||||
useEffect(() => {
|
||||
// queryTableColumnListByScript(dataSourceItem);
|
||||
setSql(dataSourceItem?.modelDetail?.sqlQuery);
|
||||
|
||||
const modelDetailFields = dataSourceItem?.modelDetail?.fields;
|
||||
if (Array.isArray(modelDetailFields)) {
|
||||
setScriptColumns(
|
||||
modelDetailFields.map((item) => {
|
||||
return {
|
||||
nameEn: item.fieldName,
|
||||
type: item.dataType,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [dataSourceItem]);
|
||||
|
||||
const fetchTaskResult = (params) => {
|
||||
setScriptColumns(params.columns);
|
||||
};
|
||||
// const fetchTaskResult = (params) => {
|
||||
// setScriptColumns(params.columns);
|
||||
// };
|
||||
|
||||
const queryTableColumnListByScript = async (dataSource: IDataSource.IDataSourceItem) => {
|
||||
if (!dataSource?.modelDetail?.sqlQuery) {
|
||||
return;
|
||||
}
|
||||
const { code, data } = await excuteSql({
|
||||
sql: dataSource.modelDetail?.sqlQuery,
|
||||
id: dataSource.databaseId,
|
||||
});
|
||||
if (code === 200) {
|
||||
fetchTaskResult(data);
|
||||
}
|
||||
};
|
||||
// const queryTableColumnListByScript = async (dataSource: IDataSource.IDataSourceItem) => {
|
||||
// if (!dataSource?.modelDetail?.sqlQuery) {
|
||||
// return;
|
||||
// }
|
||||
// const { code, data } = await excuteSql({
|
||||
// sql: dataSource.modelDetail?.sqlQuery,
|
||||
// id: dataSource.databaseId,
|
||||
// });
|
||||
// if (code === 200) {
|
||||
// fetchTaskResult(data);
|
||||
// }
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -38,9 +38,9 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
>([]);
|
||||
const [dimensionValueSettingModalVisible, setDimensionValueSettingModalVisible] =
|
||||
useState<boolean>(false);
|
||||
const [pagination, setPagination] = useState({
|
||||
const [pagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
pageSize: 99999,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
@@ -54,16 +54,9 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
modelId,
|
||||
});
|
||||
setLoading(false);
|
||||
const { list, pageSize, pageNum, total } = data || {};
|
||||
const { list } = data || {};
|
||||
let resData: any = {};
|
||||
if (code === 200) {
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageSize: Math.min(pageSize, 100),
|
||||
current: pageNum,
|
||||
total,
|
||||
});
|
||||
|
||||
resData = {
|
||||
data: list || [],
|
||||
success: true,
|
||||
@@ -327,30 +320,6 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
},
|
||||
};
|
||||
|
||||
// const dropdownButtonItems = [
|
||||
// {
|
||||
// key: 'batchStart',
|
||||
// label: '批量启用',
|
||||
// },
|
||||
// {
|
||||
// key: 'batchStop',
|
||||
// label: '批量停用',
|
||||
// },
|
||||
// {
|
||||
// key: 'batchDelete',
|
||||
// label: (
|
||||
// <Popconfirm
|
||||
// title="确定批量删除吗?"
|
||||
// onConfirm={() => {
|
||||
// queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED);
|
||||
// }}
|
||||
// >
|
||||
// <a>批量删除</a>
|
||||
// </Popconfirm>
|
||||
// ),
|
||||
// },
|
||||
// ];
|
||||
|
||||
const onMenuClick = (key: string) => {
|
||||
switch (key) {
|
||||
case 'batchStart':
|
||||
@@ -372,10 +341,8 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
request={queryDimensionList}
|
||||
pagination={pagination}
|
||||
loading={loading}
|
||||
search={{
|
||||
span: 4,
|
||||
defaultCollapsed: false,
|
||||
collapseRender: () => {
|
||||
return <></>;
|
||||
@@ -385,14 +352,6 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
type: 'checkbox',
|
||||
...rowSelection,
|
||||
}}
|
||||
onChange={(data: any) => {
|
||||
const { current, pageSize, total } = data;
|
||||
setPagination({
|
||||
current,
|
||||
pageSize,
|
||||
total,
|
||||
});
|
||||
}}
|
||||
tableAlertRender={() => {
|
||||
return false;
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { message, Button, Space, Popconfirm, Input } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import type { Dispatch } from 'umi';
|
||||
import { StatusEnum } from '../enum';
|
||||
@@ -19,6 +19,7 @@ import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
|
||||
import moment from 'moment';
|
||||
import styles from './style.less';
|
||||
import { ISemantic } from '../data';
|
||||
import { ColumnsConfig } from './MetricTableColumnRender';
|
||||
|
||||
type Props = {
|
||||
dispatch: Dispatch;
|
||||
@@ -92,16 +93,12 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
};
|
||||
|
||||
const columns: ProColumns[] = [
|
||||
{
|
||||
dataIndex: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '指标名称',
|
||||
title: '指标',
|
||||
width: '30%',
|
||||
search: false,
|
||||
render: ColumnsConfig.metricInfo.render,
|
||||
},
|
||||
{
|
||||
dataIndex: 'key',
|
||||
@@ -109,74 +106,25 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
hideInTable: true,
|
||||
renderFormItem: () => <Input placeholder="请输入ID/指标名称/英文名称/标签" />,
|
||||
},
|
||||
{
|
||||
dataIndex: 'alias',
|
||||
title: '别名',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
dataIndex: 'bizName',
|
||||
title: '英文名称',
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
dataIndex: 'sensitiveLevel',
|
||||
title: '敏感度',
|
||||
width: 80,
|
||||
hideInTable: true,
|
||||
valueEnum: SENSITIVE_LEVEL_ENUM,
|
||||
},
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
width: 80,
|
||||
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>;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'createdBy',
|
||||
title: '创建人',
|
||||
width: 100,
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
dataIndex: 'tags',
|
||||
title: '标签',
|
||||
search: false,
|
||||
render: (tags) => {
|
||||
if (Array.isArray(tags)) {
|
||||
return (
|
||||
<Space size={2} wrap>
|
||||
{tags.map((tag) => (
|
||||
<Tag color="blue" key={tag}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return <>--</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'description',
|
||||
title: '描述',
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
width: 200,
|
||||
search: false,
|
||||
render: ColumnsConfig.state.render,
|
||||
},
|
||||
|
||||
{
|
||||
dataIndex: 'updatedAt',
|
||||
title: '更新时间',
|
||||
@@ -302,11 +250,8 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
search={{
|
||||
span: 4,
|
||||
defaultCollapsed: false,
|
||||
collapseRender: () => {
|
||||
return <></>;
|
||||
},
|
||||
optionRender: false,
|
||||
collapsed: false,
|
||||
}}
|
||||
rowSelection={{
|
||||
type: 'checkbox',
|
||||
@@ -327,7 +272,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
total,
|
||||
});
|
||||
}}
|
||||
size="small"
|
||||
size="large"
|
||||
options={{ reload: false, density: false, fullScreen: false }}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { useEffect, forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import type { ForwardRefRenderFunction } from 'react';
|
||||
import { message, Form, Input, Select, Button, Space } from 'antd';
|
||||
import { saveDatabase, testDatabaseConnect } from '../../service';
|
||||
import {
|
||||
saveDatabase,
|
||||
testDatabaseConnect,
|
||||
getDatabaseParameters,
|
||||
getDatabaseDetail,
|
||||
} from '../../service';
|
||||
import { formLayout } from '@/components/FormHelper/utils';
|
||||
import SelectTMEPerson from '@/components/SelectTMEPerson';
|
||||
import { ConfigParametersItem } from '../../../System/types';
|
||||
import { genneratorFormItemList } from '../../utils';
|
||||
import { ISemantic } from '../../data';
|
||||
|
||||
import styles from '../style.less';
|
||||
type Props = {
|
||||
domainId?: number;
|
||||
dataBaseConfig?: ISemantic.IDatabaseItem;
|
||||
databaseId?: number;
|
||||
hideSubmitBtn?: boolean;
|
||||
onSubmit?: (params?: any) => void;
|
||||
};
|
||||
@@ -18,21 +25,59 @@ const FormItem = Form.Item;
|
||||
const TextArea = Input.TextArea;
|
||||
|
||||
const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
|
||||
{ domainId, dataBaseConfig, onSubmit, hideSubmitBtn = false },
|
||||
{ domainId, databaseId, onSubmit, hideSubmitBtn = false },
|
||||
ref,
|
||||
) => {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedDbType, setSelectedDbType] = useState<string>('h2');
|
||||
|
||||
const [selectedDbType, setSelectedDbType] = useState<string>('');
|
||||
const [databaseOptions, setDatabaseOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
const [databaseConfig, setDatabaseConfig] = useState<Record<string, ConfigParametersItem[]>>({});
|
||||
const [testLoading, setTestLoading] = useState<boolean>(false);
|
||||
|
||||
const [dataBaseDetail, setDataBaseDetail] = useState<ISemantic.IDatabaseItem>();
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
if (dataBaseConfig) {
|
||||
form.setFieldsValue({ ...dataBaseConfig });
|
||||
setSelectedDbType(dataBaseConfig?.type);
|
||||
if (dataBaseDetail) {
|
||||
form.setFieldsValue({ ...dataBaseDetail });
|
||||
setSelectedDbType(dataBaseDetail?.type);
|
||||
}
|
||||
}, [dataBaseConfig]);
|
||||
}, [dataBaseDetail]);
|
||||
|
||||
useEffect(() => {
|
||||
if (databaseId) {
|
||||
queryDatabaseDetail(databaseId);
|
||||
}
|
||||
}, [databaseId]);
|
||||
|
||||
useEffect(() => {
|
||||
queryDatabaseConfig();
|
||||
}, []);
|
||||
|
||||
const queryDatabaseDetail = async (id: number) => {
|
||||
const { code, msg, data } = await getDatabaseDetail(id);
|
||||
if (code === 200) {
|
||||
setDataBaseDetail(data);
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const queryDatabaseConfig = async () => {
|
||||
const { code, msg, data } = await getDatabaseParameters();
|
||||
if (code === 200) {
|
||||
const options = Object.keys(data).map((sqlName: string) => {
|
||||
return {
|
||||
value: sqlName,
|
||||
label: sqlName,
|
||||
};
|
||||
});
|
||||
setDatabaseConfig(data);
|
||||
setDatabaseOptions(options);
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const getFormValidateFields = async () => {
|
||||
return await form.validateFields();
|
||||
@@ -47,7 +92,7 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
|
||||
const saveDatabaseConfig = async () => {
|
||||
const values = await form.validateFields();
|
||||
const { code, msg } = await saveDatabase({
|
||||
...dataBaseConfig,
|
||||
...(dataBaseDetail || {}),
|
||||
...values,
|
||||
domainId,
|
||||
});
|
||||
@@ -98,14 +143,12 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请选择数据库类型"
|
||||
options={[
|
||||
{ value: 'h2', label: 'h2' },
|
||||
{ value: 'mysql', label: 'mysql' },
|
||||
{ value: 'clickhouse', label: 'clickhouse' },
|
||||
]}
|
||||
options={databaseOptions}
|
||||
/>
|
||||
</FormItem>
|
||||
{selectedDbType === 'h2' ? (
|
||||
|
||||
{databaseConfig[selectedDbType] && genneratorFormItemList(databaseConfig[selectedDbType])}
|
||||
{/* {selectedDbType === 'h2' ? (
|
||||
<FormItem name="url" label="链接" rules={[{ required: true, message: '请输入链接' }]}>
|
||||
<Input placeholder="请输入链接" />
|
||||
</FormItem>
|
||||
@@ -139,11 +182,10 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
|
||||
]}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
<FormItem
|
||||
)} */}
|
||||
{/* <FormItem
|
||||
name="username"
|
||||
label="用户名"
|
||||
// rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</FormItem>
|
||||
@@ -152,7 +194,7 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
|
||||
</FormItem>
|
||||
<FormItem name="database" label="数据库名称">
|
||||
<Input placeholder="请输入数据库名称" />
|
||||
</FormItem>
|
||||
</FormItem> */}
|
||||
<FormItem
|
||||
name="admins"
|
||||
label="管理员"
|
||||
|
||||
@@ -55,7 +55,7 @@ const DatabaseSettingModal: React.FC<CreateFormProps> = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width={1200}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
title="数据库连接设置"
|
||||
style={{ top: 48 }}
|
||||
@@ -67,7 +67,8 @@ const DatabaseSettingModal: React.FC<CreateFormProps> = ({
|
||||
<DatabaseCreateForm
|
||||
hideSubmitBtn={true}
|
||||
ref={createFormRef}
|
||||
dataBaseConfig={databaseItem}
|
||||
// dataBaseConfig={databaseItem}
|
||||
databaseId={databaseItem?.id}
|
||||
onSubmit={() => {
|
||||
onSubmit?.();
|
||||
}}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Button, message } from 'antd';
|
||||
import DimensionMetricRelationTableTransfer from './DimensionMetricRelationTableTransfer';
|
||||
import { ISemantic } from '../data';
|
||||
import { updateExprMetric } from '../service';
|
||||
import { updateMetric } from '../service';
|
||||
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
||||
|
||||
type Props = {
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
|
||||
const DimensionAndMetricRelationModal: React.FC<Props> = ({
|
||||
open,
|
||||
metricItem = {},
|
||||
metricItem,
|
||||
relationsInitialValue,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
@@ -30,7 +30,7 @@ const DimensionAndMetricRelationModal: React.FC<Props> = ({
|
||||
drillDownDimensions: relationList,
|
||||
},
|
||||
};
|
||||
const { code, msg } = await updateExprMetric(queryParams);
|
||||
const { code, msg } = await updateMetric(queryParams);
|
||||
if (code === 200) {
|
||||
onSubmit(relationList);
|
||||
return;
|
||||
@@ -45,7 +45,11 @@ const DimensionAndMetricRelationModal: React.FC<Props> = ({
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
saveMetric(relationList);
|
||||
if (metricItem?.id) {
|
||||
saveMetric(relationList);
|
||||
} else {
|
||||
onSubmit(relationList);
|
||||
}
|
||||
}}
|
||||
>
|
||||
完成
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Space } from 'antd';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@@ -15,7 +16,12 @@ const FormLabelRequire: React.FC<Props> = ({ title, labelStyles = {} }) => {
|
||||
title={title}
|
||||
style={{ fontSize: '16px', ...labelStyles }}
|
||||
>
|
||||
{title}
|
||||
<Space size={5}>
|
||||
<span style={{ color: '#ff4d4f', fontSize: '18px', position: 'relative', top: 3 }}>
|
||||
*
|
||||
</span>
|
||||
{title}
|
||||
</Space>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -15,19 +15,23 @@ import {
|
||||
Col,
|
||||
Space,
|
||||
Tooltip,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import MetricMeasuresFormTable from './MetricMeasuresFormTable';
|
||||
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
|
||||
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 { getMeasureListByModelId, getModelDetail } from '../service';
|
||||
import { getMetricsToCreateNewMetric, getModelDetail } from '../service';
|
||||
import MetricMetricFormTable from './MetricMetricFormTable';
|
||||
import MetricFieldFormTable from './MetricFieldFormTable';
|
||||
import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal';
|
||||
import TableTitleTooltips from '../components/TableTitleTooltips';
|
||||
import { creatExprMetric, updateExprMetric, mockMetricAlias, getMetricTags } from '../service';
|
||||
import { createMetric, updateMetric, mockMetricAlias, getMetricTags } from '../service';
|
||||
import { ISemantic } from '../data';
|
||||
import { history } from 'umi';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export type CreateFormProps = {
|
||||
datasourceId?: number;
|
||||
@@ -44,6 +48,12 @@ 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,
|
||||
@@ -67,10 +77,31 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
|
||||
const [classMeasureList, setClassMeasureList] = useState<ISemantic.IMeasure[]>([]);
|
||||
|
||||
const [exprTypeParamsState, setExprTypeParamsState] = 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 [exprSql, setExprSql] = useState<string>('');
|
||||
// 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);
|
||||
@@ -87,10 +118,13 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
const forward = () => setCurrentStep(currentStep + 1);
|
||||
const backward = () => setCurrentStep(currentStep - 1);
|
||||
|
||||
const queryClassMeasureList = async () => {
|
||||
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) {
|
||||
@@ -106,7 +140,8 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
queryClassMeasureList();
|
||||
queryModelDetail();
|
||||
queryMetricsToCreateNewMetric();
|
||||
queryMetricTags();
|
||||
}, []);
|
||||
|
||||
@@ -115,10 +150,8 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
const submitForm = {
|
||||
...formValRef.current,
|
||||
...fieldsValue,
|
||||
typeParams: {
|
||||
expr: exprSql,
|
||||
measures: exprTypeParamsState,
|
||||
},
|
||||
metricDefineType: defineType,
|
||||
[queryParamsTypeParamsKey[defineType]]: exprTypeParamsState[defineType],
|
||||
};
|
||||
updateFormVal(submitForm);
|
||||
if (currentStep < 1) {
|
||||
@@ -135,12 +168,16 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
bizName,
|
||||
description,
|
||||
sensitiveLevel,
|
||||
typeParams: typeParams,
|
||||
typeParams,
|
||||
dataFormat,
|
||||
dataFormatType,
|
||||
alias,
|
||||
tags,
|
||||
} = metricItem as any;
|
||||
metricDefineType,
|
||||
metricDefineByMeasureParams,
|
||||
metricDefineByMetricParams,
|
||||
metricDefineByFieldParams,
|
||||
} = metricItem;
|
||||
const isPercent = dataFormatType === 'percent';
|
||||
const isDecimal = dataFormatType === 'decimal';
|
||||
const initValue = {
|
||||
@@ -162,10 +199,39 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
...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);
|
||||
setExprTypeParamsState(typeParams.measures);
|
||||
setExprSql(typeParams.expr);
|
||||
setDefineType(metricDefineType);
|
||||
setIsPercentState(isPercent);
|
||||
setIsDecimalState(isDecimal);
|
||||
};
|
||||
@@ -176,6 +242,37 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
}
|
||||
}, [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,
|
||||
@@ -185,22 +282,22 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
},
|
||||
...fieldsValue,
|
||||
};
|
||||
const { typeParams, alias, dataFormatType } = queryParams;
|
||||
const { alias, dataFormatType } = queryParams;
|
||||
queryParams.alias = Array.isArray(alias) ? alias.join(',') : '';
|
||||
if (!typeParams?.expr) {
|
||||
if (!queryParams[queryParamsTypeParamsKey[defineType]]?.expr) {
|
||||
message.error('请输入度量表达式');
|
||||
return;
|
||||
}
|
||||
if (!dataFormatType) {
|
||||
delete queryParams.dataFormat;
|
||||
}
|
||||
if (!(Array.isArray(typeParams?.measures) && typeParams.measures.length > 0)) {
|
||||
message.error('请添加一个度量');
|
||||
if (isEmptyConditions(defineType, queryParams[queryParamsTypeParamsKey[defineType]])) {
|
||||
return;
|
||||
}
|
||||
let saveMetricQuery = creatExprMetric;
|
||||
|
||||
let saveMetricQuery = createMetric;
|
||||
if (queryParams.id) {
|
||||
saveMetricQuery = updateExprMetric;
|
||||
saveMetricQuery = updateMetric;
|
||||
}
|
||||
const { code, msg } = await saveMetricQuery(queryParams);
|
||||
if (code === 200) {
|
||||
@@ -238,24 +335,141 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
message.error('获取指标标签失败');
|
||||
}
|
||||
};
|
||||
const queryMetricsToCreateNewMetric = async () => {
|
||||
const { code, data } = await getMetricsToCreateNewMetric({ modelId });
|
||||
if (code === 200) {
|
||||
setCreateNewMetricList(data);
|
||||
} else {
|
||||
message.error('获取指标标签失败');
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (currentStep === 1) {
|
||||
return (
|
||||
<MetricMeasuresFormTable
|
||||
datasourceId={datasourceId}
|
||||
typeParams={{
|
||||
measures: exprTypeParamsState,
|
||||
expr: exprSql,
|
||||
}}
|
||||
measuresList={classMeasureList}
|
||||
onFieldChange={(typeParams: any) => {
|
||||
setExprTypeParamsState([...typeParams]);
|
||||
}}
|
||||
onSqlChange={(sql: string) => {
|
||||
setExprSql(sql);
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -456,9 +670,9 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
return (
|
||||
<Modal
|
||||
forceRender
|
||||
width={1300}
|
||||
width={800}
|
||||
style={{ top: 48 }}
|
||||
styles={{ padding: '32px 40px 48px' }}
|
||||
// styles={{ padding: '32px 40px 48px' }}
|
||||
destroyOnClose
|
||||
title={`${isEdit ? '编辑' : '新建'}指标`}
|
||||
maskClosable={false}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Button, Input, Space } from 'antd';
|
||||
import { Button, Input, Space, Tag } from 'antd';
|
||||
import ProTable from '@ant-design/pro-table';
|
||||
import ProCard from '@ant-design/pro-card';
|
||||
import SqlEditor from '@/components/SqlEditor';
|
||||
import BindMeasuresTable from './BindMeasuresTable';
|
||||
import FormLabelRequire from './FormLabelRequire';
|
||||
import styles from './style.less';
|
||||
import { ISemantic } from '../data';
|
||||
|
||||
type Props = {
|
||||
datasourceId?: number;
|
||||
typeParams: ISemantic.ITypeParams;
|
||||
typeParams: ISemantic.IMeasureTypeParams;
|
||||
measuresList: ISemantic.IMeasure[];
|
||||
onFieldChange: (measures: ISemantic.IMeasure[]) => void;
|
||||
onSqlChange: (sql: string) => void;
|
||||
@@ -25,7 +26,6 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
||||
onSqlChange,
|
||||
}) => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
const [measuresModalVisible, setMeasuresModalVisible] = useState<boolean>(false);
|
||||
const [measuresParams, setMeasuresParams] = useState(
|
||||
typeParams || {
|
||||
@@ -44,6 +44,7 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
||||
{
|
||||
dataIndex: 'bizName',
|
||||
title: '度量名称',
|
||||
tooltip: '由模型名称_字段名称拼接而来',
|
||||
},
|
||||
{
|
||||
dataIndex: 'constraint',
|
||||
@@ -74,6 +75,11 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'agg',
|
||||
title: '聚合函数',
|
||||
},
|
||||
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'x',
|
||||
@@ -105,7 +111,6 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
||||
<ProTable
|
||||
actionRef={actionRef}
|
||||
headerTitle={<FormLabelRequire title="度量列表" />}
|
||||
tooltip="基于本主题域下所有数据源的度量来创建指标,且该列表的度量为了加以区分,均已加上数据源名称作为前缀,选中度量后,可基于这几个度量来写表达式,若是选中的度量来自不同的数据源,系统将会自动join来计算该指标"
|
||||
rowKey="name"
|
||||
columns={columns}
|
||||
dataSource={measuresParams?.measures || []}
|
||||
@@ -127,8 +132,24 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
||||
/>
|
||||
<ProCard
|
||||
title={<FormLabelRequire title="度量表达式" />}
|
||||
tooltip="度量表达式由上面选择的度量组成,如选择了度量A和B,则可将表达式写成A+B"
|
||||
// tooltip="由于度量已自带聚合函数,因此通过度量创建指标时,表达式中无需再写聚合函数,如
|
||||
// 通过度量a和度量b来创建指标,由于建模的时候度量a和度量b被指定了聚合函数SUM,因此创建指标时表达式只需要写成 a+b, 而不需要带聚合函数"
|
||||
>
|
||||
<p
|
||||
className={styles.desc}
|
||||
style={{ border: 'unset', padding: 0, marginBottom: 20, marginLeft: 2 }}
|
||||
>
|
||||
在
|
||||
<Tag color="#2499ef14" className={styles.markerTag}>
|
||||
建模时
|
||||
</Tag>
|
||||
度量已指定了
|
||||
<Tag color="#2499ef14" className={styles.markerTag}>
|
||||
聚合函数
|
||||
</Tag>
|
||||
,在度量模式下,表达式无需再写聚合函数,如:
|
||||
通过指定了聚合函数SUM的度量a和度量b来创建指标,表达式只需要写成 a+b
|
||||
</p>
|
||||
<SqlEditor
|
||||
value={exprString}
|
||||
onChange={(sql: string) => {
|
||||
@@ -149,11 +170,12 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
||||
}
|
||||
selectedMeasuresList={measuresParams?.measures || []}
|
||||
onSubmit={async (values: any[]) => {
|
||||
const measures = values.map(({ bizName, name, expr, datasourceId }) => {
|
||||
const measures = values.map(({ bizName, name, expr, datasourceId, agg }) => {
|
||||
return {
|
||||
bizName,
|
||||
name,
|
||||
expr,
|
||||
agg,
|
||||
datasourceId,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -240,6 +240,9 @@
|
||||
.ant-pro-table-search-query-filter {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ant-pro-query-filter {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ant-pro-table-list-toolbar-container {
|
||||
padding-top: 0;
|
||||
}
|
||||
@@ -391,4 +394,26 @@
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 0;
|
||||
padding: 25px;
|
||||
color: #667085;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-top: 1px solid #eee;
|
||||
margin-bottom: 10px;
|
||||
.markerTag {
|
||||
color: #2499ef;
|
||||
font-size: 14px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.textLink {
|
||||
color: #101828;
|
||||
&:hover {
|
||||
color: #69b1ff;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user