mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-01 21:23:10 +00:00

* Minor: Enhance Block Editor Adjust drag-and-drop handles and improve bar menu layout * Refactor: Replace RichTextEditor with dynamic description fields in user and team forms * Enhance AddNotificationPage and AddObservabilityPage to use RichTextEditor for description input * Enhance AddNotificationPage and AddObservabilityPage to initialize RichTextEditor with alert description
330 lines
11 KiB
TypeScript
330 lines
11 KiB
TypeScript
/*
|
|
* Copyright 2023 Collate.
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
import { Button, Form, FormProps, Space, Tooltip, Typography } from 'antd';
|
|
import { DefaultOptionType } from 'antd/lib/select';
|
|
import { AxiosError } from 'axios';
|
|
import { filter, isEmpty } from 'lodash';
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { AsyncSelect } from '../../components/common/AsyncSelect/AsyncSelect';
|
|
import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels';
|
|
import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component';
|
|
import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface';
|
|
import SchemaEditor from '../../components/Database/SchemaEditor/SchemaEditor';
|
|
import { HTTP_STATUS_CODE } from '../../constants/Auth.constants';
|
|
import {
|
|
getEntityDetailsPath,
|
|
INITIAL_PAGING_VALUE,
|
|
PAGE_SIZE_MEDIUM,
|
|
} from '../../constants/constants';
|
|
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
|
|
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
|
|
import { CSMode } from '../../enums/codemirror.enum';
|
|
import { EntityType } from '../../enums/entity.enum';
|
|
import { SearchIndex } from '../../enums/search.enum';
|
|
import { OwnerType } from '../../enums/user.enum';
|
|
import { CreateQuery } from '../../generated/api/data/createQuery';
|
|
import { Table } from '../../generated/entity/data/table';
|
|
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
|
import { useFqn } from '../../hooks/useFqn';
|
|
import { FieldProp, FieldTypes } from '../../interface/FormUtils.interface';
|
|
import { searchData } from '../../rest/miscAPI';
|
|
import { postQuery } from '../../rest/queryAPI';
|
|
import { getTableDetailsByFQN } from '../../rest/tableAPI';
|
|
import { getPartialNameFromFQN } from '../../utils/CommonUtils';
|
|
import { getCurrentMillis } from '../../utils/date-time/DateTimeUtils';
|
|
import {
|
|
getEntityBreadcrumbs,
|
|
getEntityLabel,
|
|
getEntityName,
|
|
} from '../../utils/EntityUtils';
|
|
import { getField } from '../../utils/formUtils';
|
|
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
|
|
|
const AddQueryPage = () => {
|
|
const { t } = useTranslation();
|
|
const { currentUser } = useApplicationStore();
|
|
const { fqn: datasetFQN } = useFqn();
|
|
const { permissions } = usePermissionProvider();
|
|
const [form] = Form.useForm();
|
|
const [titleBreadcrumb, setTitleBreadcrumb] = useState<
|
|
TitleBreadcrumbProps['titleLinks']
|
|
>([]);
|
|
const [description, setDescription] = useState<string>('');
|
|
const [table, setTable] = useState<Table>();
|
|
const [initialOptions, setInitialOptions] = useState<DefaultOptionType[]>();
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
const fetchEntityDetails = async () => {
|
|
try {
|
|
const tableRes = await getTableDetailsByFQN(datasetFQN);
|
|
setTable(tableRes);
|
|
setTitleBreadcrumb([
|
|
...getEntityBreadcrumbs(tableRes, EntityType.TABLE),
|
|
{
|
|
name: getEntityName(tableRes),
|
|
url: getEntityDetailsPath(
|
|
EntityType.TABLE,
|
|
datasetFQN,
|
|
'table_queries'
|
|
),
|
|
},
|
|
{
|
|
name: t('label.add-entity', {
|
|
entity: t('label.query'),
|
|
}),
|
|
url: '',
|
|
activeTitle: true,
|
|
},
|
|
]);
|
|
} catch (error) {
|
|
showErrorToast(error as AxiosError);
|
|
}
|
|
};
|
|
|
|
const fetchTableEntity = async (
|
|
searchValue = ''
|
|
): Promise<DefaultOptionType[]> => {
|
|
try {
|
|
const { data } = await searchData(
|
|
searchValue,
|
|
INITIAL_PAGING_VALUE,
|
|
PAGE_SIZE_MEDIUM,
|
|
'',
|
|
'',
|
|
'',
|
|
SearchIndex.TABLE
|
|
);
|
|
const options = data.hits.hits.map((value) => ({
|
|
label: getEntityLabel(value._source),
|
|
value: value._source.id,
|
|
}));
|
|
|
|
return table
|
|
? filter(options, ({ value }) => value !== table.id)
|
|
: options;
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (datasetFQN) {
|
|
fetchEntityDetails();
|
|
}
|
|
}, [datasetFQN]);
|
|
|
|
const getInitialOptions = async () => {
|
|
try {
|
|
const option = await fetchTableEntity();
|
|
setInitialOptions(option);
|
|
} catch (error) {
|
|
setInitialOptions([]);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (table) {
|
|
getInitialOptions();
|
|
}
|
|
}, [table]);
|
|
|
|
const handleCancelClick = () => {
|
|
history.back();
|
|
};
|
|
|
|
const handleSubmit: FormProps['onFinish'] = async (values): Promise<void> => {
|
|
setIsSaving(true);
|
|
const updatedValues: CreateQuery = {
|
|
...values,
|
|
description: isEmpty(description) ? undefined : description,
|
|
owners: [
|
|
{
|
|
id: currentUser?.id ?? '',
|
|
type: OwnerType.USER,
|
|
},
|
|
],
|
|
queryUsedIn: [
|
|
{
|
|
id: table?.id ?? '',
|
|
type: EntityType.TABLE,
|
|
},
|
|
...(values.queryUsedIn || []).map((id: string) => ({
|
|
id,
|
|
type: EntityType.TABLE,
|
|
})),
|
|
],
|
|
queryDate: getCurrentMillis(),
|
|
service: getPartialNameFromFQN(datasetFQN, ['service']),
|
|
};
|
|
|
|
try {
|
|
await postQuery(updatedValues);
|
|
showSuccessToast(
|
|
t('server.create-entity-success', { entity: t('label.query') })
|
|
);
|
|
handleCancelClick();
|
|
} catch (error) {
|
|
if (
|
|
(error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT
|
|
) {
|
|
showErrorToast(
|
|
t('server.entity-already-exist-message-without-name', {
|
|
entity: t('label.query'),
|
|
entityPlural: t('label.query-lowercase-plural'),
|
|
})
|
|
);
|
|
} else {
|
|
showErrorToast(
|
|
t('server.create-entity-error', {
|
|
entity: t('label.query-plural'),
|
|
})
|
|
);
|
|
}
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const descriptionField: FieldProp = useMemo(
|
|
() => ({
|
|
name: 'description',
|
|
required: false,
|
|
label: `${t('label.description')}:`,
|
|
id: 'root/description',
|
|
type: FieldTypes.DESCRIPTION,
|
|
props: {
|
|
'data-testid': 'description',
|
|
initialValue: '',
|
|
style: {
|
|
margin: 0,
|
|
},
|
|
placeHolder: t('message.write-your-description'),
|
|
onTextChange: (value: string) => setDescription(value),
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<ResizablePanels
|
|
className="content-height-with-resizable-panel"
|
|
firstPanel={{
|
|
className: 'content-resizable-panel-container',
|
|
children: (
|
|
<div className="max-width-md w-9/10 service-form-container">
|
|
<TitleBreadcrumb titleLinks={titleBreadcrumb} />
|
|
<div className="m-t-md">
|
|
<Typography.Paragraph
|
|
className="text-base"
|
|
data-testid="form-title">
|
|
{t('label.add-new-entity', { entity: t('label.query') })}
|
|
</Typography.Paragraph>
|
|
<Form
|
|
data-testid="query-form"
|
|
form={form}
|
|
id="query-form"
|
|
initialValues={{
|
|
queryUsedIn: table ? [table.id] : undefined,
|
|
}}
|
|
layout="vertical"
|
|
onFinish={handleSubmit}>
|
|
<Form.Item
|
|
data-testid="sql-editor-container"
|
|
label={t('label.sql-uppercase-query')}
|
|
name="query"
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: t('label.field-required', {
|
|
field: t('label.sql-uppercase-query'),
|
|
}),
|
|
},
|
|
]}
|
|
trigger="onChange">
|
|
<SchemaEditor
|
|
className="custom-query-editor query-editor-h-200 custom-code-mirror-theme"
|
|
mode={{ name: CSMode.SQL }}
|
|
showCopyButton={false}
|
|
/>
|
|
</Form.Item>
|
|
{getField(descriptionField)}
|
|
<Form.Item
|
|
label={`${t('label.query-used-in')}:`}
|
|
name="queryUsedIn">
|
|
<AsyncSelect
|
|
api={fetchTableEntity}
|
|
data-testid="query-used-in"
|
|
mode="multiple"
|
|
options={initialOptions}
|
|
placeholder={t('label.please-select-entity', {
|
|
entity: t('label.query-used-in'),
|
|
})}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Space className="w-full justify-end" size={16}>
|
|
<Button
|
|
data-testid="cancel-btn"
|
|
type="default"
|
|
onClick={handleCancelClick}>
|
|
{t('label.cancel')}
|
|
</Button>
|
|
<Tooltip
|
|
placement="top"
|
|
title={
|
|
!permissions.query?.Create && NO_PERMISSION_FOR_ACTION
|
|
}>
|
|
<Button
|
|
data-testid="save-btn"
|
|
disabled={!permissions.query?.Create}
|
|
htmlType="submit"
|
|
loading={isSaving}
|
|
type="primary">
|
|
{t('label.save')}
|
|
</Button>
|
|
</Tooltip>
|
|
</Space>
|
|
</Form.Item>
|
|
</Form>
|
|
</div>
|
|
</div>
|
|
),
|
|
minWidth: 700,
|
|
flex: 0.7,
|
|
}}
|
|
pageTitle={t('label.add-entity', { entity: t('label.query') })}
|
|
secondPanel={{
|
|
children: (
|
|
<>
|
|
<Typography.Paragraph className="text-base font-medium">
|
|
{t('label.add-entity', {
|
|
entity: t('label.query'),
|
|
})}
|
|
</Typography.Paragraph>
|
|
<Typography.Text>
|
|
{t('message.add-query-helper-message')}
|
|
</Typography.Text>
|
|
</>
|
|
),
|
|
className: 'p-md p-t-xl content-resizable-panel-container',
|
|
minWidth: 400,
|
|
flex: 0.3,
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default AddQueryPage;
|