mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-29 11:26:05 +00:00
Query extend (#15595)
* initial commit * add profile pic for bot * logo configurable for collate app * add query extras * fix tests * add tests
This commit is contained in:
parent
d6c7465b19
commit
bd208371a9
@ -38,6 +38,7 @@ import { useClipboard } from '../../../hooks/useClipBoard';
|
|||||||
import { useFqn } from '../../../hooks/useFqn';
|
import { useFqn } from '../../../hooks/useFqn';
|
||||||
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
|
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
|
||||||
import { parseSearchParams } from '../../../utils/Query/QueryUtils';
|
import { parseSearchParams } from '../../../utils/Query/QueryUtils';
|
||||||
|
import queryClassBase from '../../../utils/QueryClassBase';
|
||||||
import { getQueryPath } from '../../../utils/RouterUtils';
|
import { getQueryPath } from '../../../utils/RouterUtils';
|
||||||
import SchemaEditor from '../SchemaEditor/SchemaEditor';
|
import SchemaEditor from '../SchemaEditor/SchemaEditor';
|
||||||
import QueryCardExtraOption from './QueryCardExtraOption/QueryCardExtraOption.component';
|
import QueryCardExtraOption from './QueryCardExtraOption/QueryCardExtraOption.component';
|
||||||
@ -59,6 +60,7 @@ const QueryCard: FC<QueryCardProp> = ({
|
|||||||
afterDeleteAction,
|
afterDeleteAction,
|
||||||
}: QueryCardProp) => {
|
}: QueryCardProp) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const QueryExtras = queryClassBase.getQueryExtras();
|
||||||
const { fqn: datasetFQN } = useFqn();
|
const { fqn: datasetFQN } = useFqn();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -169,7 +171,7 @@ const QueryCard: FC<QueryCardProp> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[0, 8]}>
|
<Row gutter={[0, 8]}>
|
||||||
<Col span={24}>
|
<Col span={isExpanded && QueryExtras ? 12 : 24}>
|
||||||
<Card
|
<Card
|
||||||
bordered={false}
|
bordered={false}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -247,8 +249,8 @@ const QueryCard: FC<QueryCardProp> = ({
|
|||||||
onChange={handleQueryChange}
|
onChange={handleQueryChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Row align="middle" className="p-y-xs border-top">
|
<Row align="middle" className="p-y-md border-top">
|
||||||
<Col className="p-y-0.5 p-l-md" span={16}>
|
<Col className="p-l-md" span={16}>
|
||||||
<QueryUsedByOtherTable
|
<QueryUsedByOtherTable
|
||||||
isEditMode={isEditMode}
|
isEditMode={isEditMode}
|
||||||
query={query}
|
query={query}
|
||||||
@ -284,6 +286,7 @@ const QueryCard: FC<QueryCardProp> = ({
|
|||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
{isExpanded && QueryExtras && <QueryExtras />}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { Button, Dropdown, MenuProps, Space, Tag, Tooltip } from 'antd';
|
import { Button, Dropdown, MenuProps, Space, Tag, Tooltip } from 'antd';
|
||||||
import { isUndefined, split } from 'lodash';
|
import { isUndefined, split } from 'lodash';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
|
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
|
||||||
import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg';
|
import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg';
|
||||||
@ -25,8 +25,13 @@ import { QueryVoteType } from '../TableQueries.interface';
|
|||||||
import { QueryCardExtraOptionProps } from './QueryCardExtraOption.interface';
|
import { QueryCardExtraOptionProps } from './QueryCardExtraOption.interface';
|
||||||
|
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import Qs from 'qs';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useApplicationStore } from '../../../../hooks/useApplicationStore';
|
import { useApplicationStore } from '../../../../hooks/useApplicationStore';
|
||||||
|
import { useFqn } from '../../../../hooks/useFqn';
|
||||||
import { deleteQuery } from '../../../../rest/queryAPI';
|
import { deleteQuery } from '../../../../rest/queryAPI';
|
||||||
|
import queryClassBase from '../../../../utils/QueryClassBase';
|
||||||
|
import { getQueryPath } from '../../../../utils/RouterUtils';
|
||||||
import { showErrorToast } from '../../../../utils/ToastUtils';
|
import { showErrorToast } from '../../../../utils/ToastUtils';
|
||||||
import ConfirmationModal from '../../../Modals/ConfirmationModal/ConfirmationModal';
|
import ConfirmationModal from '../../../Modals/ConfirmationModal/ConfirmationModal';
|
||||||
import './query-card-extra-option.style.less';
|
import './query-card-extra-option.style.less';
|
||||||
@ -39,6 +44,9 @@ const QueryCardExtraOption = ({
|
|||||||
afterDeleteAction,
|
afterDeleteAction,
|
||||||
}: QueryCardExtraOptionProps) => {
|
}: QueryCardExtraOptionProps) => {
|
||||||
const { EditAll, EditQueries, Delete } = permission;
|
const { EditAll, EditQueries, Delete } = permission;
|
||||||
|
const { fqn: datasetFQN } = useFqn();
|
||||||
|
const history = useHistory();
|
||||||
|
const QueryHeaderButton = queryClassBase.getQueryHeaderActionsButtons();
|
||||||
const { currentUser } = useApplicationStore();
|
const { currentUser } = useApplicationStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
@ -57,6 +65,13 @@ const QueryCardExtraOption = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onExpandClick = useCallback(() => {
|
||||||
|
history.push({
|
||||||
|
search: Qs.stringify({ query: query.id }),
|
||||||
|
pathname: getQueryPath(datasetFQN, query.id ?? ''),
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
const dropdownItems = useMemo(() => {
|
const dropdownItems = useMemo(() => {
|
||||||
const items: MenuProps['items'] = [
|
const items: MenuProps['items'] = [
|
||||||
{
|
{
|
||||||
@ -130,9 +145,14 @@ const QueryCardExtraOption = ({
|
|||||||
className="query-card-extra-option"
|
className="query-card-extra-option"
|
||||||
data-testid="extra-option-container"
|
data-testid="extra-option-container"
|
||||||
size={8}>
|
size={8}>
|
||||||
|
{QueryHeaderButton && (
|
||||||
|
<QueryHeaderButton onClickHandler={onExpandClick} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Tag className="query-lines" data-testid="query-line">
|
<Tag className="query-lines" data-testid="query-line">
|
||||||
{queryLine}
|
{queryLine}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|
||||||
<Tooltip title={t('label.up-vote')}>
|
<Tooltip title={t('label.up-vote')}>
|
||||||
<Button
|
<Button
|
||||||
className="vote-button"
|
className="vote-button"
|
||||||
|
@ -46,6 +46,10 @@ jest.mock('../../../../rest/queryAPI', () => ({
|
|||||||
deleteQuery: jest.fn(),
|
deleteQuery: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../hooks/useFqn', () => ({
|
||||||
|
useFqn: jest.fn().mockImplementation(() => ({ fqn: 'testFqn' })),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('QueryCardExtraOption component test', () => {
|
describe('QueryCardExtraOption component test', () => {
|
||||||
it('Component should render', async () => {
|
it('Component should render', async () => {
|
||||||
render(<QueryCardExtraOption {...mockProps} />);
|
render(<QueryCardExtraOption {...mockProps} />);
|
||||||
|
@ -18,6 +18,9 @@ class ApplicationSchemaClassBase {
|
|||||||
public getJSONUISchema() {
|
public getJSONUISchema() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
public importAppLogo(appName: string) {
|
||||||
|
return import(`../../../../assets/svg/${appName}.svg`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationSchemaClassBase = new ApplicationSchemaClassBase();
|
const applicationSchemaClassBase = new ApplicationSchemaClassBase();
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Avatar } from 'antd';
|
import { Avatar } from 'antd';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import applicationSchemaClassBase from '../AppDetails/ApplicationSchemaClassBase';
|
||||||
|
|
||||||
const AppLogo = ({
|
const AppLogo = ({
|
||||||
logo,
|
logo,
|
||||||
@ -22,18 +23,21 @@ const AppLogo = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [appLogo, setAppLogo] = useState<JSX.Element | null>(null);
|
const [appLogo, setAppLogo] = useState<JSX.Element | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchLogo = useCallback(async () => {
|
||||||
if (!logo) {
|
if (!logo) {
|
||||||
import(`../../../../assets/svg/${appName}.svg`).then((data) => {
|
const data = await applicationSchemaClassBase.importAppLogo(appName);
|
||||||
const Icon = data.ReactComponent as React.ComponentType<
|
const Icon = data.ReactComponent as React.ComponentType<
|
||||||
JSX.IntrinsicElements['svg']
|
JSX.IntrinsicElements['svg']
|
||||||
>;
|
>;
|
||||||
setAppLogo(<Icon height={55} width={55} />);
|
setAppLogo(<Icon />);
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setAppLogo(logo);
|
setAppLogo(logo);
|
||||||
}
|
}
|
||||||
}, [appName, logo]);
|
}, [logo, appName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogo();
|
||||||
|
}, [appName]);
|
||||||
|
|
||||||
return <Avatar className="flex-center bg-grey-1" icon={appLogo} size={100} />;
|
return <Avatar className="flex-center bg-grey-1" icon={appLogo} size={100} />;
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
getImageWithResolutionAndFallback,
|
getImageWithResolutionAndFallback,
|
||||||
ImageQuality,
|
ImageQuality,
|
||||||
} from '../../utils/ProfilerUtils';
|
} from '../../utils/ProfilerUtils';
|
||||||
|
import userClassBase from '../../utils/UserClassBase';
|
||||||
import { useApplicationStore } from '../useApplicationStore';
|
import { useApplicationStore } from '../useApplicationStore';
|
||||||
|
|
||||||
let userProfilePicsLoading: string[] = [];
|
let userProfilePicsLoading: string[] = [];
|
||||||
@ -80,7 +81,11 @@ export const useUserProfile = ({
|
|||||||
});
|
});
|
||||||
userProfilePicsLoading = userProfilePicsLoading.filter((p) => p !== name);
|
userProfilePicsLoading = userProfilePicsLoading.filter((p) => p !== name);
|
||||||
|
|
||||||
|
if (user.isBot) {
|
||||||
|
setProfilePic(userClassBase.getBotLogo(user.name) ?? '');
|
||||||
|
} else {
|
||||||
setProfilePic(profile);
|
setProfilePic(profile);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error
|
// Error
|
||||||
userProfilePicsLoading = userProfilePicsLoading.filter((p) => p !== name);
|
userProfilePicsLoading = userProfilePicsLoading.filter((p) => p !== name);
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
@blue-1: #ebf6fe;
|
@blue-1: #ebf6fe;
|
||||||
@blue-2: #3ca2f4;
|
@blue-2: #3ca2f4;
|
||||||
@blue-3: #0950c5;
|
@blue-3: #0950c5;
|
||||||
|
@blue-4: #f1f9ff;
|
||||||
@black: #000000;
|
@black: #000000;
|
||||||
@aborted-color: #efae2f;
|
@aborted-color: #efae2f;
|
||||||
@info-color: #2196f3;
|
@info-color: #2196f3;
|
||||||
|
@ -10,8 +10,10 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { ItemType } from 'antd/lib/menu/hooks/useItems';
|
import { ItemType } from 'antd/lib/menu/hooks/useItems';
|
||||||
|
import { FC } from 'react';
|
||||||
import DataProductsPage from '../components/DataProducts/DataProductsPage/DataProductsPage.component';
|
import DataProductsPage from '../components/DataProducts/DataProductsPage/DataProductsPage.component';
|
||||||
import {
|
import {
|
||||||
getEditWebhookPath,
|
getEditWebhookPath,
|
||||||
@ -284,6 +286,10 @@ class EntityUtilClassBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getEntityFloatingButton(_: EntityType): FC | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public getManageExtraOptions(
|
public getManageExtraOptions(
|
||||||
_entityType?: EntityType,
|
_entityType?: EntityType,
|
||||||
_fqn?: string
|
_fqn?: string
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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 queryClassBase from './QueryClassBase';
|
||||||
|
|
||||||
|
describe('QueryClassBase', () => {
|
||||||
|
it('should return null from getQueryExtras', () => {
|
||||||
|
const result = queryClassBase.getQueryExtras();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null from getQueryHeaderActionsButtons', () => {
|
||||||
|
const result = queryClassBase.getQueryHeaderActionsButtons();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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 { FC } from 'react';
|
||||||
|
|
||||||
|
class QueryClassBase {
|
||||||
|
public getQueryExtras(): FC | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getQueryHeaderActionsButtons(): FC<{
|
||||||
|
onClickHandler: () => void;
|
||||||
|
}> | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClassBase = new QueryClassBase();
|
||||||
|
|
||||||
|
export default queryClassBase;
|
||||||
|
export { QueryClassBase };
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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 userClassBase from './UserClassBase';
|
||||||
|
|
||||||
|
describe('UserClassBase', () => {
|
||||||
|
it('should return empty string from getBotLogo when botName is empty', () => {
|
||||||
|
let result = userClassBase.getBotLogo('');
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
|
||||||
|
result = userClassBase.getBotLogo('unknown');
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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.
|
||||||
|
*/
|
||||||
|
class UserClassBase {
|
||||||
|
protected botLogos: Record<string, string> = {};
|
||||||
|
public getBotLogo(botName: string) {
|
||||||
|
return this.botLogos[botName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userClassBase = new UserClassBase();
|
||||||
|
|
||||||
|
export default userClassBase;
|
||||||
|
export { UserClassBase };
|
Loading…
x
Reference in New Issue
Block a user