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:
Karan Hotchandani 2024-03-20 15:05:59 +05:30 committed by GitHub
parent d6c7465b19
commit bd208371a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 165 additions and 14 deletions

View File

@ -38,6 +38,7 @@ import { useClipboard } from '../../../hooks/useClipBoard';
import { useFqn } from '../../../hooks/useFqn';
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
import { parseSearchParams } from '../../../utils/Query/QueryUtils';
import queryClassBase from '../../../utils/QueryClassBase';
import { getQueryPath } from '../../../utils/RouterUtils';
import SchemaEditor from '../SchemaEditor/SchemaEditor';
import QueryCardExtraOption from './QueryCardExtraOption/QueryCardExtraOption.component';
@ -59,6 +60,7 @@ const QueryCard: FC<QueryCardProp> = ({
afterDeleteAction,
}: QueryCardProp) => {
const { t } = useTranslation();
const QueryExtras = queryClassBase.getQueryExtras();
const { fqn: datasetFQN } = useFqn();
const location = useLocation();
const history = useHistory();
@ -169,7 +171,7 @@ const QueryCard: FC<QueryCardProp> = ({
return (
<Row gutter={[0, 8]}>
<Col span={24}>
<Col span={isExpanded && QueryExtras ? 12 : 24}>
<Card
bordered={false}
className={classNames(
@ -247,8 +249,8 @@ const QueryCard: FC<QueryCardProp> = ({
onChange={handleQueryChange}
/>
</div>
<Row align="middle" className="p-y-xs border-top">
<Col className="p-y-0.5 p-l-md" span={16}>
<Row align="middle" className="p-y-md border-top">
<Col className="p-l-md" span={16}>
<QueryUsedByOtherTable
isEditMode={isEditMode}
query={query}
@ -284,6 +286,7 @@ const QueryCard: FC<QueryCardProp> = ({
</Row>
</Card>
</Col>
{isExpanded && QueryExtras && <QueryExtras />}
</Row>
);
};

View File

@ -12,7 +12,7 @@
*/
import { Button, Dropdown, MenuProps, Space, Tag, Tooltip } from 'antd';
import { isUndefined, split } from 'lodash';
import React, { useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.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 { AxiosError } from 'axios';
import Qs from 'qs';
import { useHistory } from 'react-router-dom';
import { useApplicationStore } from '../../../../hooks/useApplicationStore';
import { useFqn } from '../../../../hooks/useFqn';
import { deleteQuery } from '../../../../rest/queryAPI';
import queryClassBase from '../../../../utils/QueryClassBase';
import { getQueryPath } from '../../../../utils/RouterUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
import ConfirmationModal from '../../../Modals/ConfirmationModal/ConfirmationModal';
import './query-card-extra-option.style.less';
@ -39,6 +44,9 @@ const QueryCardExtraOption = ({
afterDeleteAction,
}: QueryCardExtraOptionProps) => {
const { EditAll, EditQueries, Delete } = permission;
const { fqn: datasetFQN } = useFqn();
const history = useHistory();
const QueryHeaderButton = queryClassBase.getQueryHeaderActionsButtons();
const { currentUser } = useApplicationStore();
const { t } = useTranslation();
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 items: MenuProps['items'] = [
{
@ -130,9 +145,14 @@ const QueryCardExtraOption = ({
className="query-card-extra-option"
data-testid="extra-option-container"
size={8}>
{QueryHeaderButton && (
<QueryHeaderButton onClickHandler={onExpandClick} />
)}
<Tag className="query-lines" data-testid="query-line">
{queryLine}
</Tag>
<Tooltip title={t('label.up-vote')}>
<Button
className="vote-button"

View File

@ -46,6 +46,10 @@ jest.mock('../../../../rest/queryAPI', () => ({
deleteQuery: jest.fn(),
}));
jest.mock('../../../../hooks/useFqn', () => ({
useFqn: jest.fn().mockImplementation(() => ({ fqn: 'testFqn' })),
}));
describe('QueryCardExtraOption component test', () => {
it('Component should render', async () => {
render(<QueryCardExtraOption {...mockProps} />);

View File

@ -18,6 +18,9 @@ class ApplicationSchemaClassBase {
public getJSONUISchema() {
return {};
}
public importAppLogo(appName: string) {
return import(`../../../../assets/svg/${appName}.svg`);
}
}
const applicationSchemaClassBase = new ApplicationSchemaClassBase();

View File

@ -11,7 +11,8 @@
* limitations under the License.
*/
import { Avatar } from 'antd';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import applicationSchemaClassBase from '../AppDetails/ApplicationSchemaClassBase';
const AppLogo = ({
logo,
@ -22,18 +23,21 @@ const AppLogo = ({
}) => {
const [appLogo, setAppLogo] = useState<JSX.Element | null>(null);
useEffect(() => {
const fetchLogo = useCallback(async () => {
if (!logo) {
import(`../../../../assets/svg/${appName}.svg`).then((data) => {
const Icon = data.ReactComponent as React.ComponentType<
JSX.IntrinsicElements['svg']
>;
setAppLogo(<Icon height={55} width={55} />);
});
const data = await applicationSchemaClassBase.importAppLogo(appName);
const Icon = data.ReactComponent as React.ComponentType<
JSX.IntrinsicElements['svg']
>;
setAppLogo(<Icon />);
} else {
setAppLogo(logo);
}
}, [appName, logo]);
}, [logo, appName]);
useEffect(() => {
fetchLogo();
}, [appName]);
return <Avatar className="flex-center bg-grey-1" icon={appLogo} size={100} />;
};

View File

@ -19,6 +19,7 @@ import {
getImageWithResolutionAndFallback,
ImageQuality,
} from '../../utils/ProfilerUtils';
import userClassBase from '../../utils/UserClassBase';
import { useApplicationStore } from '../useApplicationStore';
let userProfilePicsLoading: string[] = [];
@ -80,7 +81,11 @@ export const useUserProfile = ({
});
userProfilePicsLoading = userProfilePicsLoading.filter((p) => p !== name);
setProfilePic(profile);
if (user.isBot) {
setProfilePic(userClassBase.getBotLogo(user.name) ?? '');
} else {
setProfilePic(profile);
}
} catch (error) {
// Error
userProfilePicsLoading = userProfilePicsLoading.filter((p) => p !== name);

View File

@ -47,6 +47,7 @@
@blue-1: #ebf6fe;
@blue-2: #3ca2f4;
@blue-3: #0950c5;
@blue-4: #f1f9ff;
@black: #000000;
@aborted-color: #efae2f;
@info-color: #2196f3;

View File

@ -10,8 +10,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { FC } from 'react';
import DataProductsPage from '../components/DataProducts/DataProductsPage/DataProductsPage.component';
import {
getEditWebhookPath,
@ -284,6 +286,10 @@ class EntityUtilClassBase {
}
}
public getEntityFloatingButton(_: EntityType): FC | null {
return null;
}
public getManageExtraOptions(
_entityType?: EntityType,
_fqn?: string

View File

@ -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();
});
});

View File

@ -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 };

View File

@ -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();
});
});

View File

@ -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 };