diff --git a/api/package.json b/api/package.json index 3aa9a1c1d..3219afbd2 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@devtable/api", - "version": "13.43.13", + "version": "13.44.0", "description": "", "main": "index.js", "scripts": { diff --git a/api/src/api_models/dashboard_content.ts b/api/src/api_models/dashboard_content.ts index ff7db60fb..421a90e7e 100644 --- a/api/src/api_models/dashboard_content.ts +++ b/api/src/api_models/dashboard_content.ts @@ -15,12 +15,12 @@ export class Query { }) id: string; - @IsIn(['postgresql', 'mysql', 'http', 'transform']) + @IsIn(['postgresql', 'mysql', 'http', 'transform', 'merico_metric_system']) @ApiModelProperty({ description: 'Datasource type', required: true, }) - type: 'postgresql' | 'mysql' | 'http' | 'transform'; + type: 'postgresql' | 'mysql' | 'http' | 'transform' | 'merico_metric_system'; @IsString() @ApiModelProperty({ diff --git a/api/src/api_models/datasource.ts b/api/src/api_models/datasource.ts index 900077b20..5a2294a66 100644 --- a/api/src/api_models/datasource.ts +++ b/api/src/api_models/datasource.ts @@ -91,7 +91,7 @@ export class DataSource { @ApiModelProperty({ description: 'type of the datasource', required: true, - enum: ['postgresql', 'mysql', 'http', 'transform'], + enum: ['postgresql', 'mysql', 'http', 'transform', 'merico_metric_system'], }) type: string; @@ -232,13 +232,13 @@ export class DataSourcePaginationResponse implements PaginationResponse { - if (type !== 'http' && type !== 'transform') { + if (type === 'mysql' || type === 'postgresql') { await this.testDatabaseConfiguration(type, config, locale); } maybeEncryptPassword(config); diff --git a/api/tests/unit/validation.test.ts b/api/tests/unit/validation.test.ts index 91283447c..d4e7e1f22 100644 --- a/api/tests/unit/validation.test.ts +++ b/api/tests/unit/validation.test.ts @@ -1520,7 +1520,7 @@ describe('DataSourceCreateRequest', () => { property: 'type', children: [], constraints: { - isIn: 'type must be one of the following values: postgresql, mysql, http, transform', + isIn: 'type must be one of the following values: postgresql, mysql, http, transform, merico_metric_system', isString: 'type must be a string', }, }, diff --git a/dashboard/package.json b/dashboard/package.json index a7168b146..eb0595fab 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@devtable/dashboard", - "version": "13.43.13", + "version": "13.44.0", "license": "Apache-2.0", "publishConfig": { "access": "public", diff --git a/dashboard/src/api-caller/request.ts b/dashboard/src/api-caller/request.ts index a55040684..7d4a1e0ce 100644 --- a/dashboard/src/api-caller/request.ts +++ b/dashboard/src/api-caller/request.ts @@ -20,7 +20,7 @@ export type TQueryPayload = { export type TQueryStructureRequest = { query_type: 'TABLES' | 'COLUMNS' | 'DATA' | 'INDEXES' | 'COUNT'; - type: DataSourceType.Postgresql | DataSourceType.MySQL; + type: DataSourceType.Postgresql | DataSourceType.MySQL | DataSourceType.MericoMetricSystem; key: string; // datasource key table_schema: string; table_name: string; diff --git a/dashboard/src/components/widgets/inline-function-input/index.tsx b/dashboard/src/components/widgets/inline-function-input/index.tsx index 68b2e0df3..f3ab623c1 100644 --- a/dashboard/src/components/widgets/inline-function-input/index.tsx +++ b/dashboard/src/components/widgets/inline-function-input/index.tsx @@ -7,16 +7,16 @@ import { useTranslation } from 'react-i18next'; import { OnMount } from '@monaco-editor/react'; // @ts-expect-error type of this lib import { constrainedEditor } from 'constrained-editor-plugin/dist/esm/constrainedEditor.js'; -interface IInlineFunctionInput { +type Props = { value: TFunctionString; onChange: (v: TFunctionString) => void; defaultValue: TFunctionString; label: string; restrictions?: MonacoEditorRestriction[]; -} +}; -export const InlineFunctionInput = forwardRef( - ({ value, onChange, label, defaultValue, restrictions = [] }: IInlineFunctionInput, _ref: any) => { +export const InlineFunctionInput = forwardRef( + ({ value, onChange, label, defaultValue, restrictions = [] }, _ref) => { const { t } = useTranslation(); const [localValue, setLocalValue] = useState(value); diff --git a/dashboard/src/dashboard-editor/model/dashboard.ts b/dashboard/src/dashboard-editor/model/dashboard.ts index c0af6ad8d..133783f5e 100644 --- a/dashboard/src/dashboard-editor/model/dashboard.ts +++ b/dashboard/src/dashboard-editor/model/dashboard.ts @@ -30,6 +30,20 @@ export const DashboardModel = types content_id: self.content_id, }; }, + get queryVariables() { + const ret: Record = { + context: { + ...self.content.mock_context.current, + ...self.context.current, + }, + filters: self.content.filters.previewValues, + }; + + return ret; + }, + get queryVariablesString() { + return JSON.stringify(this.queryVariables, null, 2); + }, })) .actions((self) => ({ updateCurrentContent(content: DashboardContentDBType) { diff --git a/dashboard/src/dashboard-editor/model/datasources/tables.ts b/dashboard/src/dashboard-editor/model/datasources/tables.ts index 02155779b..9fa7e15df 100644 --- a/dashboard/src/dashboard-editor/model/datasources/tables.ts +++ b/dashboard/src/dashboard-editor/model/datasources/tables.ts @@ -1,5 +1,4 @@ -import { getParent, types } from 'mobx-state-tree'; -import { DataSourceType } from '~/model'; +import { types } from 'mobx-state-tree'; export type TableInfoType = { table_schema: string; diff --git a/dashboard/src/dashboard-editor/model/filters/index.ts b/dashboard/src/dashboard-editor/model/filters/index.ts index 8eb17a130..0e1317af6 100644 --- a/dashboard/src/dashboard-editor/model/filters/index.ts +++ b/dashboard/src/dashboard-editor/model/filters/index.ts @@ -72,6 +72,7 @@ export const FiltersModel = types return self.current.map((f) => ({ label: f.label ?? f.key, value: f.key, + widget: f.type, })); }, get sortedList() { diff --git a/dashboard/src/dashboard-editor/model/queries/query.ts b/dashboard/src/dashboard-editor/model/queries/query.ts index 066a17fbe..0aaff667c 100644 --- a/dashboard/src/dashboard-editor/model/queries/query.ts +++ b/dashboard/src/dashboard-editor/model/queries/query.ts @@ -1,5 +1,5 @@ import { Instance, SnapshotIn } from 'mobx-state-tree'; -import { QueryRenderModel } from '~/model'; +import { QueryRenderModel, QueryUsageType } from '~/model'; export const QueryModel = QueryRenderModel.views((self) => ({ get canPreviewData() { @@ -14,6 +14,24 @@ export const QueryModel = QueryRenderModel.views((self) => ({ } return 'Need to pick a Data Source first'; }, + get usage() { + return self.contentModel.findQueryUsage(self.id) as QueryUsageType[]; + }, + get runBySet() { + return new Set(...self.run_by); + }, + keyInRunBy(key: string) { + return this.runBySet.has(key); + }, +})).actions((self) => ({ + changeRunByRecord(key: string, checked: boolean) { + const set = new Set(self.run_by); + if (!checked) { + set.delete(key); + } else { + set.add(key); + } + }, })); export type QueryModelInstance = Instance; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/data-preview/query-state-message.tsx b/dashboard/src/dashboard-editor/ui/settings/content/data-preview/query-state-message.tsx index 5f58a7546..a28e17d24 100644 --- a/dashboard/src/dashboard-editor/ui/settings/content/data-preview/query-state-message.tsx +++ b/dashboard/src/dashboard-editor/ui/settings/content/data-preview/query-state-message.tsx @@ -1,11 +1,12 @@ import { Text } from '@mantine/core'; +import { observer } from 'mobx-react-lite'; import React from 'react'; import { useRenderContentModelContext } from '~/contexts'; interface IQueryStateMessage { queryID: string; } -export const QueryStateMessage = ({ queryID }: IQueryStateMessage) => { +export const QueryStateMessage = observer(({ queryID }: IQueryStateMessage) => { const model = useRenderContentModelContext(); const { state, error } = model.getDataStuffByID(queryID); const query = React.useMemo(() => model.queries.findByID(queryID), [model, queryID]); @@ -28,4 +29,4 @@ export const QueryStateMessage = ({ queryID }: IQueryStateMessage) => { } return null; -}; +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/db-explorer-modal/index.tsx b/dashboard/src/dashboard-editor/ui/settings/content/db-explorer-modal/index.tsx index 9c0712212..90880ae78 100644 --- a/dashboard/src/dashboard-editor/ui/settings/content/db-explorer-modal/index.tsx +++ b/dashboard/src/dashboard-editor/ui/settings/content/db-explorer-modal/index.tsx @@ -24,7 +24,8 @@ export const DBExplorerModal = observer(({ dataSource, triggerButtonProps = {} } const { t } = useTranslation(); const [opened, setOpened] = useState(false); - if (dataSource.type === 'http') { + const { type } = dataSource; + if (type === 'http' || type === 'merico_metric_system') { return null; } return ( diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/index.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/index.tsx index 66717de07..cdefd1ca9 100644 --- a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/index.tsx +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/index.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react-lite'; import { useEditContentModelContext } from '~/contexts'; import { QueryEditorForm } from './query-editor-form'; import { QueryModelInstance } from '~/dashboard-editor/model/queries'; +import { MericoMetricQueryEditorForm } from './merico-metric-query-editor-form'; export const EditQuery = observer(({ id }: { id: string }) => { const content = useEditContentModelContext(); @@ -20,5 +21,8 @@ export const EditQuery = observer(({ id }: { id: string }) => { ); } + if (query.isMericoMetricQuery) { + return ; + } return ; }); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/index.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/index.tsx new file mode 100644 index 000000000..316f9ce12 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/index.tsx @@ -0,0 +1,73 @@ +import { Group, Stack, TextInput } from '@mantine/core'; +import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; +import { QueryModelInstance } from '~/dashboard-editor/model'; +import { SelectDataSource } from '../query-editor-form/select-data-source'; +import { MoreActions } from './more-actions'; +import { PreviewData } from './preview-data'; +import { QueryTabs } from './query-tabs'; +import { QueryVariablesPreview } from './query-variables-preview'; +import { RunQuery } from './run-query'; +import { SelectMetric } from './select-metric'; + +type Props = { + queryModel: QueryModelInstance; +}; +export const MericoMetricQueryEditorForm = observer(({ queryModel }: Props) => { + const { t } = useTranslation(); + + return ( + + + { + queryModel.setName(e.currentTarget.value); + }} + styles={{ + root: { + flexGrow: 0, + width: 'calc(50% - 44px)', + }, + }} + /> + + + + + + + + + + + + + + + + + + + ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/copy.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/copy.tsx new file mode 100644 index 000000000..d89638f60 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/copy.tsx @@ -0,0 +1,25 @@ +import { MericoIconProps } from './types'; + +export const MericoIconCopy = ({ width, height }: MericoIconProps) => ( + + + + + + + + + + + +); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/delete.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/delete.tsx new file mode 100644 index 000000000..391aae5ab --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/delete.tsx @@ -0,0 +1,20 @@ +import { MericoIconProps } from './types'; + +export const MericoIconDelete = ({ width, height, color = '#EB1212' }: MericoIconProps) => ( + + + + + +); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/external-link.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/external-link.tsx new file mode 100644 index 000000000..5e0d38a76 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/external-link.tsx @@ -0,0 +1,16 @@ +import { MericoIconProps } from './types'; + +export const MericoIconExternalLink = ({ width, height }: MericoIconProps) => ( + + + + +); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/index.ts b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/index.ts new file mode 100644 index 000000000..ba97e4439 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/index.ts @@ -0,0 +1,5 @@ +export * from './external-link'; +export * from './play'; +export * from './copy'; +export * from './delete'; +export * from './more'; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/more.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/more.tsx new file mode 100644 index 000000000..d08f575b6 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/more.tsx @@ -0,0 +1,18 @@ +import { MericoIconProps } from './types'; + +export const MericoIconMore = ({ width, height }: MericoIconProps) => ( + + + + + +); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/play.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/play.tsx new file mode 100644 index 000000000..3a6ad5905 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/play.tsx @@ -0,0 +1,18 @@ +import { MericoIconProps } from './types'; + +export const MericoIconPlay = ({ width, height }: MericoIconProps) => ( + + + + +); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/types.ts b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/types.ts new file mode 100644 index 000000000..27336f91e --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/merico-icons/types.ts @@ -0,0 +1,5 @@ +export type MericoIconProps = { + width: number | string; + height: number | string; + color?: string; +}; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/more-actions.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/more-actions.tsx new file mode 100644 index 000000000..0e4adb0f7 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/more-actions.tsx @@ -0,0 +1,80 @@ +import { ActionIcon, Button, Menu, Tooltip } from '@mantine/core'; +import { useModals } from '@mantine/modals'; +import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; +import { useEditContentModelContext, useEditDashboardContext } from '~/contexts'; +import { QueryModelInstance } from '~/dashboard-editor/model'; +import { MericoIconDelete, MericoIconMore } from './merico-icons'; + +type Props = { + queryModel: QueryModelInstance; +}; + +const DeleteQuery = observer(({ queryModel }: Props) => { + const { t } = useTranslation(); + const model = useEditDashboardContext(); + const content = useEditContentModelContext(); + const usage = content.findQueryUsage(queryModel.id); + const disabled = usage.length > 0; + + const modals = useModals(); + const remove = () => { + modals.openConfirmModal({ + title: `${t('query.delete')}?`, + labels: { confirm: t('common.actions.confirm'), cancel: t('common.actions.cancel') }, + onCancel: () => console.log('Cancel'), + onConfirm: () => { + content.queries.removeQuery(queryModel.id); + model.editor.setPath(['_QUERIES_', '']); + }, + confirmProps: { color: 'red' }, + zIndex: 320, + }); + }; + + if (disabled) { + return ( + + + + ); + } + return ( + } color="red" onClick={remove}> + 删除查询 + + ); +}); + +export const MoreActions = observer(({ queryModel }: Props) => { + return ( + + + + + + + + + {/* }>复制API */} + + + + ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table-with-pagination.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table-with-pagination.tsx new file mode 100644 index 000000000..c4e589d5a --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table-with-pagination.tsx @@ -0,0 +1,26 @@ +import { Box, LoadingOverlay, Stack } from '@mantine/core'; +import { useMemo, useState } from 'react'; +import { ErrorBoundary, errorBoundary } from '~/utils'; +import { DataTable } from './data-table'; +import { PaginationControl } from './pagination-control'; + +export const DataTableWithPagination = errorBoundary(({ data, loading }: { data: TQueryData; loading: boolean }) => { + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(20); + const tableData = useMemo(() => { + const start = (page - 1) * limit; + const end = start + limit; + return data.slice(start, end); + }, [data, page, limit]); + return ( + + + + + + + + + + ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table.style.ts b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table.style.ts new file mode 100644 index 000000000..60e4ca4e2 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table.style.ts @@ -0,0 +1,44 @@ +import { EmotionSx } from '@mantine/emotion'; + +export const TableStyle: EmotionSx = { + width: 'fit-content', + minWidth: '100%', + tableLayout: 'fixed', + + tr: { + width: 'fit-content', + }, + th: { + position: 'relative', + }, + 'th, td': { + wordBreak: 'break-word', + }, + + '.resizer': { + position: 'absolute', + right: 0, + top: '50%', + transform: 'translateY(-50%)', + cursor: 'col-resize', + userSelect: 'none', + touchAction: 'none', + }, + + '.resizer.isResizing': { + color: '#228be6', + opacity: '1', + backgroundColor: 'rgb(248, 249, 250)', + }, + + '@media (hover: hover)': { + '.resizer': { + opacity: '0', + }, + + '*:hover > .resizer': { + color: '#228be6', + opacity: '1', + }, + }, +}; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table.tsx new file mode 100644 index 000000000..a493b6643 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/data-table.tsx @@ -0,0 +1,73 @@ +import { ActionIcon, Box, Table } from '@mantine/core'; +import { IconArrowBarToRight } from '@tabler/icons-react'; +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; +import { AnyObject } from '~/types'; +import { ErrorBoundary } from '~/utils'; +import { TableStyle } from './data-table.style'; + +export function DataTable({ data }: { data: AnyObject[] }) { + const columns = useMemo(() => { + if (!Array.isArray(data) || data.length === 0) { + return []; + } + const columnHelper = createColumnHelper(); + return Object.keys(data[0]).map((k) => { + return columnHelper.accessor(k, { + cell: (info) => info.getValue(), + }); + }); + }, [data]); + + const table = useReactTable({ + data, + columns, + columnResizeMode: 'onChange', + getCoreRowModel: getCoreRowModel(), + }); + if (data.length === 0) { + return ; + } + return ( + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + + + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + + {typeof cell.getValue() === 'object' ? ( +
{JSON.stringify(cell.getValue(), null, 2)}
+ ) : ( + flexRender(cell.column.columnDef.cell, cell.getContext()) + )} +
+
+ ))} +
+ ))} +
+
+
+ ); +} diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/index.ts b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/index.ts new file mode 100644 index 000000000..aad359407 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/index.ts @@ -0,0 +1 @@ +export * from './preview-data'; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/pagination-control.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/pagination-control.tsx new file mode 100644 index 000000000..cbafcc6be --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/preview-data/pagination-control.tsx @@ -0,0 +1,91 @@ +import { Group, Pagination, Select, Text } from '@mantine/core'; +import { SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; + +const limitOptions = [ + { label: '10', value: '10' }, + { label: '20', value: '20' }, + { label: '50', value: '50' }, + { label: '100', value: '100' }, +]; + +const selectorStyles = { + root: { + width: '150px', + }, + section: { + '&[data-position=left]': { + width: '70px', + textAlign: 'center', + }, + }, + input: { + paddingLeft: '70px', + paddingRight: 0, + }, +}; + +type Props = { + data: TQueryData; + page: number; + setPage: React.Dispatch>; + limit: number; + setLimit: React.Dispatch>; +}; +export const PaginationControl = ({ data, page, setPage, limit, setLimit }: Props) => { + const { t } = useTranslation(); + const total = data.length; + const maxPage = Math.ceil(total / limit); + + const changeLimit = (limit: string | null) => { + if (!limit) { + return; + } + + setPage(1); + setLimit(Number(limit)); + }; + + if (total === 0) { + return null; + } + + const hideLimitSelector = maxPage === 1 && total <= 10; + return ( + + + {!hideLimitSelector && ( + + {label} + + ) : null + } + styles={DimensionSelectorStyles} + value={value} + onChange={onChange} + data={options} + maxDropdownHeight={600} + renderOption={renderOption} + /> + ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/dimension-selector/index.ts b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/dimension-selector/index.ts new file mode 100644 index 000000000..1a3428898 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/dimension-selector/index.ts @@ -0,0 +1 @@ +export * from './dimension-selector'; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/dimension-selector/type.ts b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/dimension-selector/type.ts new file mode 100644 index 000000000..4093e3bb8 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/dimension-selector/type.ts @@ -0,0 +1,8 @@ +export type DimensionOption = { + label: string; + value: string; + description: string; + type: 'number' | 'string' | 'date' | 'boolean'; + group_name?: string; + group_value?: string; +}; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/edit-metric-query.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/edit-metric-query.tsx new file mode 100644 index 000000000..224b22f0f --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/edit-metric-query.tsx @@ -0,0 +1,19 @@ +import { Stack } from '@mantine/core'; +import { observer } from 'mobx-react-lite'; +import { QueryModelInstance } from '~/dashboard-editor/model'; +import { LinkMetricsToVariables } from './link-metrics-to-variables'; +import { LinkMetricsToTimeAndStep } from './link-metrics-to-time-and-step'; +import { SetGroupbyMetrics } from './set-groupby-metrics'; + +type Props = { + queryModel: QueryModelInstance; +}; +export const EditMetricQuery = observer(({ queryModel }: Props) => { + return ( + + + + + + ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/index.ts b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/index.ts new file mode 100644 index 000000000..1ed51622c --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/index.ts @@ -0,0 +1 @@ +export * from './edit-metric-query'; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/link-metrics-to-time-and-step.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/link-metrics-to-time-and-step.tsx new file mode 100644 index 000000000..91e6bfdae --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/link-metrics-to-time-and-step.tsx @@ -0,0 +1,76 @@ +import { ActionIcon, Group, Select, Stack, Switch, Table, Text, Tooltip } from '@mantine/core'; +import { IconInfoCircle } from '@tabler/icons-react'; +import { observer } from 'mobx-react-lite'; +import { useState } from 'react'; +import { QueryModelInstance } from '~/dashboard-editor/model'; +import { RunByCheckbox } from './run-by-checkbox'; +import { MetricTableStyles } from './table-styles'; +import { VariableSelector } from './variable-selector'; +import { VariableStat } from './variable-stats'; +import { DimensionSelector } from './dimension-selector/dimension-selector'; + +type Props = { + queryModel: QueryModelInstance; +}; +export const LinkMetricsToTimeAndStep = observer(({ queryModel }: Props) => { + const [timeField, setTimeField] = useState('commit_author_time'); + const [timeVar, setTimeVar] = useState('filter.date_range'); + const [stepVar, setStepVar] = useState('filter.granularity'); + return ( + + + 按时间序列展示 + + + + + + + + + + + + + + + + + + + 看板变量 + 变量值为真时运行查询 + + + + + + + + + + + + + + + + + + + 步长 + + + + + + + + + + + +
+
+ ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/link-metrics-to-variables.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/link-metrics-to-variables.tsx new file mode 100644 index 000000000..9370fcbff --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/link-metrics-to-variables.tsx @@ -0,0 +1,82 @@ +import { ActionIcon, Checkbox, Group, Select, Stack, Table, Text, Tooltip } from '@mantine/core'; +import { IconInfoCircle } from '@tabler/icons-react'; +import { observer } from 'mobx-react-lite'; +import { useEditDashboardContext } from '~/contexts'; +import { QueryModelInstance } from '~/dashboard-editor/model'; +import { MetricTableStyles } from './table-styles'; +import { VariableSelector } from './variable-selector'; +import { VariableStat } from './variable-stats'; +import { DimensionSelector } from './dimension-selector/dimension-selector'; + +const rows = [ + { metric: 'repository_project -> id', variable: 'context.project_ids', checked: true }, + { metric: 'account -> id', variable: 'context.account_id', checked: false }, + { metric: 'organization -> id', variable: 'filters.tree_select', checked: false }, + { metric: 'team -> id', variable: 'filters.multi_select', checked: true }, +]; + +type Props = { + queryModel: QueryModelInstance; +}; +export const LinkMetricsToVariables = observer(({ queryModel }: Props) => { + const model = useEditDashboardContext(); + return ( + + + 看板变量与指标维度关联 + + + + + + + + + + + + + + + + + 指标筛选维度 + 看板变量 + 变量值为真时运行查询 + + + + {rows.map((row) => ( + + {row.metric} + + + + + + + + + + + ))} + + + + + + + + + + +
+
+ ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/run-by-checkbox.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/run-by-checkbox.tsx new file mode 100644 index 000000000..f14ce3eba --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/run-by-checkbox.tsx @@ -0,0 +1,28 @@ +import { Checkbox } from '@mantine/core'; +import { observer } from 'mobx-react-lite'; +import { ChangeEventHandler } from 'react'; +import { QueryModelInstance } from '~/dashboard-editor/model'; + +type CheckerProps = { + queryModel: QueryModelInstance; + variable: string; +}; + +const Checker = observer(({ queryModel, variable }: CheckerProps) => { + const checked = queryModel.keyInRunBy(variable); + const handleCheck: ChangeEventHandler = (e) => { + queryModel.changeRunByRecord(variable, e.currentTarget.checked); + }; + return ; +}); + +type Props = { + queryModel: QueryModelInstance; + variable: string | null; +}; +export const RunByCheckbox = ({ queryModel, variable }: Props) => { + if (!variable) { + return null; + } + return ; +}; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/set-groupby-metrics.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/set-groupby-metrics.tsx new file mode 100644 index 000000000..91b670bff --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/set-groupby-metrics.tsx @@ -0,0 +1,28 @@ +import { MultiSelect } from '@mantine/core'; +import { observer } from 'mobx-react-lite'; +import { QueryModelInstance } from '~/dashboard-editor/model'; + +const options = [ + { label: 'account', value: 'account' }, + { label: 'team', value: 'team' }, +]; + +type Props = { + queryModel: QueryModelInstance; +}; +export const SetGroupbyMetrics = observer(({ queryModel }: Props) => { + return ( + + ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/table-styles.ts b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/table-styles.ts new file mode 100644 index 000000000..79f5eaa3a --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/table-styles.ts @@ -0,0 +1,7 @@ +export const MetricTableStyles = { + tbody: { + td: { + fontFamily: 'monospace', + }, + }, +}; diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/variable-selector.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/variable-selector.tsx new file mode 100644 index 000000000..8f4706c80 --- /dev/null +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/merico-metric-query-editor-form/query-tabs/edit-metric-query/variable-selector.tsx @@ -0,0 +1,112 @@ +import { ComboboxItem, Group, Select, SelectProps, Stack, Text } from '@mantine/core'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { filterTypeNames } from '~/components/filter/filter-settings/filter-setting'; +import { QueryModelInstance } from '~/dashboard-editor/model'; +import { DashboardFilterType } from '~/types'; + +const SelectorStyles: SelectProps['styles'] = { + root: { + maxWidth: 'unset', + }, + option: { + fontFamily: 'monospace', + }, + groupLabel: { + '&::before': { + content: '""', + flex: 1, + insetInline: 0, + height: 'calc(0.0625rem* var(--mantine-scale))', + marginInlineEnd: 'var(--mantine-spacing-xs)', + backgroundColor: 'var(--mantine-color-gray-2)', + }, + }, +}; + +type CustomOption = ComboboxItem & { + description: string; + type: string; + widget: DashboardFilterType; + widget_label: string; +}; +const renderOption: SelectProps['renderOption'] = ({ option, ...rest }) => { + const { t } = useTranslation(); + const o = option as CustomOption; + const showDescription = o.type === 'filter'; + return ( + + + {option.value} + + {showDescription && ( + + + {o.widget_label} + + + {t(filterTypeNames[o.widget])} + + + )} + + ); +}; + +type Props = { + queryModel: QueryModelInstance; + value: string | null; + onChange: (value: string | null, option: CustomOption) => void; +}; + +export const VariableSelector = observer(({ queryModel, value, onChange }: Props) => { + const { t } = useTranslation(); + const options = useMemo(() => { + const groups = queryModel.getConditionOptionsWithInvalidValue(value).optionGroups; + return groups.map((optionGroup) => { + const count = optionGroup.items.length; + const name = t(optionGroup.group); + return { + group: `${name}(${count})`, + items: optionGroup.items.map((item) => ({ + ...item, + label: item.value, + widget_label: item.label, + })), + }; + }); + }, [queryModel.getConditionOptionsWithInvalidValue, t, value]); + + const handleChange = useCallback( + (value: string | null, option: ComboboxItem) => { + onChange(value, option as CustomOption); + }, + [onChange], + ); + + return ( + + + showNotification({ message: 'TODO', color: 'red' })} + > + + + +
+ + ); +}); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/configurations.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/configurations.tsx index 7e691f5d4..6bc634416 100644 --- a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/configurations.tsx +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/configurations.tsx @@ -1,15 +1,15 @@ import { ActionIcon, Center, Divider, MultiSelect, Stack, TextInput } from '@mantine/core'; import { IconDeviceFloppy } from '@tabler/icons-react'; import { observer } from 'mobx-react-lite'; -import { useEffect, useState } from 'react'; -import { QueryRenderModelInstance } from '~/model'; +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CustomSelectorItem } from '~/components/widgets/custom-selector-item'; +import { QueryModelInstance } from '~/dashboard-editor/model'; import { DeleteQuery } from './delete-query'; import { SelectDataSource } from './select-data-source'; -import { CustomSelectorItem } from '~/components/widgets/custom-selector-item'; -import { useTranslation } from 'react-i18next'; interface IQueryConfigurations { - queryModel: QueryRenderModelInstance; + queryModel: QueryModelInstance; } export const QueryConfigurations = observer(({ queryModel }: IQueryConfigurations) => { @@ -18,6 +18,18 @@ export const QueryConfigurations = observer(({ queryModel }: IQueryConfiguration useEffect(() => { setName(queryModel.name); }, [queryModel.name]); + + const options = useMemo(() => { + const groups = queryModel.conditionOptionsWithInvalidRunbys.optionGroups; + return groups.map((optionGroup) => { + const group = t(optionGroup.group); + return { + group, + items: optionGroup.items, + }; + }); + }, [queryModel.conditionOptionsWithInvalidRunbys.optionGroups]); + return (
@@ -46,22 +58,13 @@ export const QueryConfigurations = observer(({ queryModel }: IQueryConfiguration queryModel.setName(name); }} /> - { - queryModel.setKey(key); - queryModel.setType(type); - }} - /> + { - - + + ); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/query-usage.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/query-usage.tsx index 3acea934d..a3f97b385 100644 --- a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/query-usage.tsx +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/query-usage.tsx @@ -3,16 +3,17 @@ import _ from 'lodash'; import { observer } from 'mobx-react-lite'; import { useTranslation } from 'react-i18next'; import { useEditDashboardContext } from '~/contexts'; +import { QueryModelInstance } from '~/dashboard-editor/model'; import { QueryUsageType } from '~/model'; -interface IQueryUsage { - queryID: string; - usage: QueryUsageType[]; -} +type Props = { + queryModel: QueryModelInstance; +}; -export const QueryUsage = observer(({ queryID, usage }: IQueryUsage) => { +export const QueryUsage = observer(({ queryModel }: Props) => { const { t } = useTranslation(); const editor = useEditDashboardContext().editor; + const open = (u: QueryUsageType) => { if (u.type === 'filter') { editor.setPath(['_FILTERS_', u.id]); @@ -28,39 +29,47 @@ export const QueryUsage = observer(({ queryID, usage }: IQueryUsage) => { const openView = (id: string) => { editor.setPath(['_VIEWS_', id]); }; + + const usage = queryModel.usage; return ( - - - - - {t('common.type')} - {t('common.name')} - {t('query.usage.in_views')} +
+ + + {t('common.type')} + {t('common.name')} + {t('query.usage.in_views')} + + + + {usage.map((u) => ( + + {t(u.type_label)} + + open(u)}> + {u.label} + + + + + {u.views.map((v) => ( + openView(v.id)}> + {v.label} + + ))} + {u.views.length === 0 && --} + + - - - {usage.map((u) => ( - - {t(u.type_label)} - - open(u)}> - {u.label} - - - - - {u.views.map((v) => ( - openView(v.id)}> - {v.label} - - ))} - {u.views.length === 0 && --} - - - - ))} - -
-
+ ))} + + ); }); diff --git a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/select-data-source.tsx b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/select-data-source.tsx index b158e1ac7..e7ce487ee 100644 --- a/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/select-data-source.tsx +++ b/dashboard/src/dashboard-editor/ui/settings/content/edit-query/query-editor-form/select-data-source.tsx @@ -6,9 +6,17 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { listDataSources } from '~/api-caller'; import { useEditDashboardContext } from '~/contexts'; +import { QueryModelInstance } from '~/dashboard-editor/model'; import { DataSourceType } from '~/model'; import { DBExplorerModal } from '../../db-explorer-modal'; +const DataSourceTypeNames: Record = { + http: 'HTTP', + mysql: 'MySQL', + postgresql: 'PostgreSQL', + merico_metric_system: '指标系统', +}; + type CustomOption = { label: string; type: DataSourceType } & ComboboxItem; const DataSourceLabel: SelectProps['renderOption'] = ({ option, ...others }) => { const { label, type } = option as CustomOption; @@ -18,7 +26,10 @@ const DataSourceLabel: SelectProps['renderOption'] = ({ option, ...others }) => className="transform-query-option" justify="flex-start" {...others} - sx={{ '&[data-selected="true"]': { '.mantine-Text-root': { color: 'white' }, svg: { stroke: 'white' } } }} + sx={{ + flexGrow: 1, + '&[data-selected="true"]': { '.mantine-Text-root': { color: 'white' }, svg: { stroke: 'white' } }, + }} > @@ -28,17 +39,17 @@ const DataSourceLabel: SelectProps['renderOption'] = ({ option, ...others }) => ); } return ( - + {label} - {type} + {DataSourceTypeNames[type]} ); }; -interface ISelectDataSource { - value: { type: DataSourceType; key: string }; - onChange: (v: { type: DataSourceType; key: string }) => void; -} -export const SelectDataSource = observer(({ value, onChange }: ISelectDataSource) => { +type Props = { + queryModel: QueryModelInstance; + size?: string; +}; +export const SelectDataSource = observer(({ queryModel, size = 'sm' }: Props) => { const { t } = useTranslation(); const model = useEditDashboardContext(); const { data: dataSources = [], loading } = useRequest( @@ -74,17 +85,19 @@ export const SelectDataSource = observer(({ value, onChange }: ISelectDataSource if (key === null) { return; } - onChange({ - key, - type: dataSourceTypeMap[key], - }); + queryModel.setKey(key); + queryModel.setType(dataSourceTypeMap[key]); }; const dataSource = useMemo(() => { - return model.datasources.find(value); - }, [model, value]); + return model.datasources.find({ + type: queryModel.type, + key: queryModel.key, + }); + }, [model, queryModel.type, queryModel.key]); return (