mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-10 06:48:09 +00:00
feat(ui): update pipeline status UI (#8253)
* feat(ui): update pipeline status UI * fix typos * minor update * Merge branch 'main' into support-7127 * design updates * minor fixes * address review comments * fix tests * fix cypress failing due to wrong localization keys
This commit is contained in:
parent
70e2fda452
commit
c83690d07e
@ -11,23 +11,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
deleteCreatedService,
|
||||
editOwnerforCreatedService,
|
||||
goToAddNewServicePage,
|
||||
interceptURL,
|
||||
login,
|
||||
testServiceCreationAndIngestion,
|
||||
updateDescriptionForIngestedTables,
|
||||
verifyResponseStatusCode,
|
||||
visitEntityDetailsPage
|
||||
} from '../../common/common';
|
||||
import {
|
||||
DBT,
|
||||
HTTP_CONFIG_SOURCE,
|
||||
LOGIN,
|
||||
SERVICE_TYPE
|
||||
} from '../../constants/constants';
|
||||
import { deleteCreatedService, editOwnerforCreatedService, goToAddNewServicePage, interceptURL, login, testServiceCreationAndIngestion, updateDescriptionForIngestedTables, verifyResponseStatusCode, visitEntityDetailsPage } from '../../common/common';
|
||||
import { DBT, HTTP_CONFIG_SOURCE, LOGIN, SERVICE_TYPE } from '../../constants/constants';
|
||||
import { REDSHIFT } from '../../constants/service.constants';
|
||||
|
||||
describe('RedShift Ingestion', () => {
|
||||
|
||||
@ -11,17 +11,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getCurrentLocaleDate,
|
||||
getFutureLocaleDateFromCurrentDate
|
||||
} from '../../../src/utils/TimeUtils';
|
||||
import {
|
||||
descriptionBox,
|
||||
interceptURL,
|
||||
login,
|
||||
verifyResponseStatusCode,
|
||||
visitEntityDetailsPage
|
||||
} from '../../common/common';
|
||||
import { getCurrentLocaleDate, getFutureLocaleDateFromCurrentDate } from '../../../src/utils/TimeUtils';
|
||||
import { descriptionBox, interceptURL, login, verifyResponseStatusCode, visitEntityDetailsPage } from '../../common/common';
|
||||
import { DELETE_ENTITY, DELETE_TERM, LOGIN } from '../../constants/constants';
|
||||
|
||||
describe('Entity Details Page', () => {
|
||||
|
||||
@ -11,19 +11,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
addNewTagToEntity,
|
||||
descriptionBox,
|
||||
interceptURL,
|
||||
login,
|
||||
verifyResponseStatusCode
|
||||
} from '../../common/common';
|
||||
import {
|
||||
LOGIN,
|
||||
NEW_TAG,
|
||||
NEW_TAG_CATEGORY,
|
||||
SEARCH_ENTITY_TABLE
|
||||
} from '../../constants/constants';
|
||||
import { addNewTagToEntity, descriptionBox, interceptURL, login, verifyResponseStatusCode } from '../../common/common';
|
||||
import { LOGIN, NEW_TAG, NEW_TAG_CATEGORY, SEARCH_ENTITY_TABLE } from '../../constants/constants';
|
||||
|
||||
describe('Tags page should work', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@ -13,27 +13,8 @@
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import {
|
||||
interceptURL,
|
||||
login,
|
||||
searchEntity,
|
||||
verifyResponseStatusCode,
|
||||
visitEntityDetailsPage,
|
||||
visitEntityTab
|
||||
} from '../../common/common';
|
||||
import {
|
||||
FOLLOWING_TITLE,
|
||||
LOGIN,
|
||||
MYDATA_SUMMARY_OPTIONS,
|
||||
MY_DATA_TITLE,
|
||||
NO_SEARCHED_TERMS,
|
||||
RECENT_SEARCH_TITLE,
|
||||
RECENT_VIEW_TITLE,
|
||||
SEARCH_ENTITY_DASHBOARD,
|
||||
SEARCH_ENTITY_PIPELINE,
|
||||
SEARCH_ENTITY_TABLE,
|
||||
SEARCH_ENTITY_TOPIC
|
||||
} from '../../constants/constants';
|
||||
import { interceptURL, login, searchEntity, verifyResponseStatusCode, visitEntityDetailsPage, visitEntityTab } from '../../common/common';
|
||||
import { FOLLOWING_TITLE, LOGIN, MYDATA_SUMMARY_OPTIONS, MY_DATA_TITLE, NO_SEARCHED_TERMS, RECENT_SEARCH_TITLE, RECENT_VIEW_TITLE, SEARCH_ENTITY_DASHBOARD, SEARCH_ENTITY_PIPELINE, SEARCH_ENTITY_TABLE, SEARCH_ENTITY_TOPIC } from '../../constants/constants';
|
||||
|
||||
const tables = Object.values(SEARCH_ENTITY_TABLE);
|
||||
const topics = Object.values(SEARCH_ENTITY_TOPIC);
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_14437_23247)">
|
||||
<path d="M13.9375 1.25H12.5V0.5C12.5 0.223875 12.2762 0 12 0C11.7238 0 11.5 0.223875 11.5 0.5V1.25H4.5V0.5C4.5 0.223875 4.27616 0 4 0C3.72384 0 3.5 0.223875 3.5 0.5V1.25H2.0625C0.925219 1.25 0 2.17522 0 3.3125V13.9375C0 15.0748 0.925219 16 2.0625 16H13.9375C15.0748 16 16 15.0748 16 13.9375V3.3125C16 2.17522 15.0748 1.25 13.9375 1.25ZM2.0625 2.25H3.5V2.75C3.5 3.02612 3.72384 3.25 4 3.25C4.27616 3.25 4.5 3.02612 4.5 2.75V2.25H11.5V2.75C11.5 3.02612 11.7238 3.25 12 3.25C12.2762 3.25 12.5 3.02612 12.5 2.75V2.25H13.9375C14.5234 2.25 15 2.72662 15 3.3125V4.5H1V3.3125C1 2.72662 1.47662 2.25 2.0625 2.25ZM13.9375 15H2.0625C1.47662 15 1 14.5234 1 13.9375V5.5H15V13.9375C15 14.5234 14.5234 15 13.9375 15Z" fill="#7147E8"/>
|
||||
<path d="M6.37528 9C6.72031 9 7 8.77615 7 8.5C7 8.22385 6.72028 8 6.37528 8H3.69601C3.35098 8 3.07129 8.22385 3.07129 8.5C3.07129 8.77615 3.35098 9 3.69601 9H6.37528Z" fill="#7147E8"/>
|
||||
<path d="M12.3533 9C12.6984 9 12.9781 8.77615 12.9781 8.5C12.9781 8.22385 12.6983 8 12.3533 8H9.62472C9.27969 8 9 8.22385 9 8.5C9 8.77615 9.27969 9 9.62472 9H12.3533Z" fill="#7147E8"/>
|
||||
<path d="M6.37528 12C6.72031 12 7 11.7761 7 11.5C7 11.2239 6.72028 11 6.37528 11H3.69601C3.35098 11 3.07129 11.2239 3.07129 11.5C3.07129 11.7761 3.35098 12 3.69601 12H6.37528Z" fill="#7147E8"/>
|
||||
<path d="M12.3533 12C12.6984 12 12.9781 11.7761 12.9781 11.5C12.9781 11.2239 12.6983 11 12.3533 11H9.62472C9.27969 11 9 11.2239 9 11.5C9 11.7761 9.27969 12 9.62472 12H12.3533Z" fill="#7147E8"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_14437_23247">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 2.82053H8.11266C8.34209 3.86487 9.27472 4.6489 10.387 4.6489C11.4992 4.6489 12.4318 3.8649 12.6613 2.82053H15.5C15.7761 2.82053 16 2.59665 16 2.32053C16 2.0444 15.7761 1.82053 15.5 1.82053H12.661C12.4312 0.776718 11.4972 -0.0078125 10.387 -0.0078125C9.27609 -0.0078125 8.34262 0.776593 8.11284 1.82053H0.5C0.223875 1.82053 0 2.0444 0 2.32053C0 2.59665 0.223875 2.82053 0.5 2.82053ZM9.05866 2.32184C9.05866 2.32006 9.05869 2.31825 9.05869 2.31647C9.06087 1.58625 9.65672 0.992218 10.387 0.992218C11.1162 0.992218 11.7121 1.58544 11.7152 2.31531L11.7153 2.32265C11.7142 3.05412 11.1187 3.64893 10.387 3.64893C9.65553 3.64893 9.06028 3.05472 9.05863 2.32369L9.05866 2.32184ZM15.5 13.1793H12.661C12.4311 12.1355 11.4972 11.351 10.387 11.351C9.27609 11.351 8.34262 12.1354 8.11284 13.1793H0.5C0.223875 13.1793 0 13.4031 0 13.6793C0 13.9555 0.223875 14.1793 0.5 14.1793H8.11266C8.34209 15.2236 9.27472 16.0077 10.387 16.0077C11.4992 16.0077 12.4318 15.2236 12.6613 14.1793H15.5C15.7761 14.1793 16 13.9555 16 13.6793C16 13.4031 15.7761 13.1793 15.5 13.1793ZM10.387 15.0077C9.65553 15.0077 9.06028 14.4135 9.05863 13.6824L9.05866 13.6806C9.05866 13.6788 9.05869 13.677 9.05869 13.6752C9.06087 12.945 9.65672 12.351 10.387 12.351C11.1162 12.351 11.7121 12.9442 11.7152 13.674L11.7153 13.6814C11.7143 14.4129 11.1188 15.0077 10.387 15.0077ZM15.5 7.49993H7.88734C7.65791 6.45559 6.72528 5.67159 5.61303 5.67159C4.50078 5.67159 3.56816 6.45559 3.33872 7.49993H0.5C0.223875 7.49993 0 7.72381 0 7.99993C0 8.27609 0.223875 8.49993 0.5 8.49993H3.33897C3.56888 9.54371 4.50275 10.3283 5.61303 10.3283C6.72391 10.3283 7.65738 9.54384 7.88716 8.49993H15.5C15.7761 8.49993 16 8.27609 16 7.99993C16 7.72381 15.7761 7.49993 15.5 7.49993ZM6.94134 7.99862C6.94134 8.00043 6.94131 8.00221 6.94131 8.00399C6.93912 8.73421 6.34328 9.32824 5.61303 9.32824C4.88381 9.32824 4.28794 8.73502 4.28478 8.00518L4.28469 7.99787C4.28578 7.26631 4.88125 6.67159 5.61303 6.67159C6.34447 6.67159 6.93972 7.26577 6.94137 7.99684L6.94134 7.99862Z" fill="#7147E8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright 2022 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 { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuProps,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { RangePickerProps } from 'antd/lib/date-picker';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { isNaN, map } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as Calendar } from '../../assets/svg/calendar.svg';
|
||||
import { ReactComponent as FilterIcon } from '../../assets/svg/filter.svg';
|
||||
import { getPipelineStatus } from '../../axiosAPIs/pipelineAPI';
|
||||
import {
|
||||
EXECUTION_FILTER_RANGE,
|
||||
MenuOptions,
|
||||
} from '../../constants/execution.constants';
|
||||
import { PipelineStatus } from '../../generated/entity/data/pipeline';
|
||||
import {
|
||||
getCurrentDateTimeStamp,
|
||||
getPastDatesTimeStampFromCurrentDate,
|
||||
getTimeStampByDate,
|
||||
} from '../../utils/TimeUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import './Execution.style.less';
|
||||
import ListView from './ListView/ListViewTab.component';
|
||||
import TreeViewTab from './TreeView/TreeViewTab.component';
|
||||
|
||||
interface ExecutionProps {
|
||||
pipelineFQN: string;
|
||||
}
|
||||
|
||||
const ExecutionsTab = ({ pipelineFQN }: ExecutionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const listViewLabel = t('label.list');
|
||||
const treeViewLabel = t('label.tree');
|
||||
|
||||
const [view, setView] = useState(listViewLabel);
|
||||
const [executions, setExecutions] = useState<Array<PipelineStatus>>();
|
||||
const [datesSelected, setDatesSelected] = useState<boolean>(false);
|
||||
const [startTime, setStartTime] = useState(
|
||||
getPastDatesTimeStampFromCurrentDate(
|
||||
EXECUTION_FILTER_RANGE.last365days.days
|
||||
)
|
||||
);
|
||||
const [endTime, setEndTime] = useState(getCurrentDateTimeStamp());
|
||||
const [isClickedCalendar, setIsClickedCalendar] = useState(false);
|
||||
const [status, setStatus] = useState(MenuOptions.all);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const fetchPipelineStatus = async (startRange: number, endRange: number) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await getPipelineStatus(pipelineFQN, {
|
||||
startTs: startRange,
|
||||
endTs: endRange,
|
||||
});
|
||||
setExecutions(response.data);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t('message.fetch-pipeline-status-error')
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModeChange = (e: RadioChangeEvent) => {
|
||||
setView(e.target.value);
|
||||
};
|
||||
|
||||
const handleMenuClick: MenuProps['onClick'] = (event) => {
|
||||
if (event?.key) {
|
||||
setStatus(MenuOptions[event.key as keyof typeof MenuOptions]);
|
||||
}
|
||||
};
|
||||
|
||||
const menu = useMemo(
|
||||
() => (
|
||||
<Menu
|
||||
items={map(MenuOptions, (value, key) => ({
|
||||
key: key,
|
||||
label: value,
|
||||
}))}
|
||||
onClick={handleMenuClick}
|
||||
/>
|
||||
),
|
||||
[handleMenuClick]
|
||||
);
|
||||
|
||||
const onDateChange: RangePickerProps['onChange'] = (_, dateStrings) => {
|
||||
if (dateStrings) {
|
||||
const startTime = getTimeStampByDate(dateStrings[0]);
|
||||
|
||||
const endTime = getTimeStampByDate(dateStrings[1]);
|
||||
|
||||
if (!isNaN(startTime) && !isNaN(endTime)) {
|
||||
setStartTime(startTime);
|
||||
setEndTime(endTime);
|
||||
}
|
||||
if (isNaN(startTime)) {
|
||||
setIsClickedCalendar(false);
|
||||
setStartTime(
|
||||
getPastDatesTimeStampFromCurrentDate(
|
||||
EXECUTION_FILTER_RANGE.last365days.days
|
||||
)
|
||||
);
|
||||
setEndTime(getCurrentDateTimeStamp());
|
||||
|
||||
setDatesSelected(false);
|
||||
}
|
||||
|
||||
setDatesSelected(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPipelineStatus(startTime, endTime);
|
||||
}, [pipelineFQN, datesSelected, startTime, endTime]);
|
||||
|
||||
return (
|
||||
<Row className="h-full p-md" gutter={16}>
|
||||
<Col flex="auto">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Space className="justify-between w-full">
|
||||
<Radio.Group value={view} onChange={handleModeChange}>
|
||||
<Radio.Button value={listViewLabel}>
|
||||
{listViewLabel}
|
||||
</Radio.Button>
|
||||
<Radio.Button value={treeViewLabel}>
|
||||
{treeViewLabel}
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Space>
|
||||
<Dropdown overlay={menu} placement="bottom">
|
||||
<Button ghost type="primary">
|
||||
<Space>
|
||||
<FilterIcon />
|
||||
{status === MenuOptions.all ? t('label.status') : status}
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{view === listViewLabel ? (
|
||||
<>
|
||||
<Button
|
||||
ghost
|
||||
className={classNames('range-picker-button delay-100', {
|
||||
'range-picker-button-width delay-100':
|
||||
!datesSelected && !isClickedCalendar,
|
||||
})}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setIsClickedCalendar(true);
|
||||
}}>
|
||||
<Space>
|
||||
<Calendar />
|
||||
{!datesSelected && (
|
||||
<label>{t('label.date-filter')}</label>
|
||||
)}
|
||||
<DatePicker.RangePicker
|
||||
allowClear
|
||||
showNow
|
||||
bordered={false}
|
||||
className="executions-date-picker"
|
||||
clearIcon={<CloseCircleOutlined />}
|
||||
open={isClickedCalendar}
|
||||
placeholder={['', '']}
|
||||
suffixIcon={null}
|
||||
onChange={onDateChange}
|
||||
onOpenChange={(isOpen) => {
|
||||
setIsClickedCalendar(isOpen);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{view === listViewLabel ? (
|
||||
<ListView
|
||||
executions={executions}
|
||||
loading={isLoading}
|
||||
status={status}
|
||||
/>
|
||||
) : (
|
||||
<TreeViewTab
|
||||
endTime={endTime}
|
||||
executions={executions}
|
||||
startTime={startTime}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExecutionsTab;
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
.executions-date-picker {
|
||||
.ant-picker-range-separator {
|
||||
display: none;
|
||||
}
|
||||
.ant-picker-clear {
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.range-picker-button-width {
|
||||
@apply delay-100;
|
||||
max-width: 130px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.range-picker-button {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Table } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PipelineStatus,
|
||||
StatusType,
|
||||
} from '../../../generated/entity/data/pipeline';
|
||||
import {
|
||||
getTableViewData,
|
||||
StatusIndicator,
|
||||
} from '../../../utils/executionUtils';
|
||||
import Loader from '../../Loader/Loader';
|
||||
|
||||
interface ListViewProps {
|
||||
executions: Array<PipelineStatus> | undefined;
|
||||
status: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const ListView = ({ executions, status, loading }: ListViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tableData = useMemo(
|
||||
() => getTableViewData(executions, status),
|
||||
[executions, status]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('label.name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: t('label.status'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: StatusType) => <StatusIndicator status={status} />,
|
||||
},
|
||||
{
|
||||
title: t('label.date-and-time'),
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
bordered
|
||||
className="h-full"
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
loading={{ spinning: loading, indicator: <Loader /> }}
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListView;
|
||||
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Card, Col, Empty, Row, Space, Typography } from 'antd';
|
||||
import { isEmpty, uniqueId } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import { PipelineStatus } from '../../../generated/entity/data/pipeline';
|
||||
import { getTreeViewData } from '../../../utils/executionUtils';
|
||||
import { getStatusBadgeIcon } from '../../../utils/PipelineDetailsUtils';
|
||||
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||
import { formatDateTimeFromSeconds } from '../../../utils/TimeUtils';
|
||||
import './tree-view-tab.less';
|
||||
|
||||
interface TreeViewProps {
|
||||
executions: Array<PipelineStatus> | undefined;
|
||||
status: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
const TreeViewTab = ({
|
||||
executions,
|
||||
status,
|
||||
startTime,
|
||||
endTime,
|
||||
}: TreeViewProps) => {
|
||||
const viewData = useMemo(
|
||||
() => getTreeViewData(executions as PipelineStatus[], status),
|
||||
[executions, status]
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Row align="middle" className="m-b-lg m-t-md" justify="center">
|
||||
<SVGIcons
|
||||
alt="result"
|
||||
className="tw-w-4 transform-180 m-r-7 cursor-pointer"
|
||||
icon={Icons.ARROW_RIGHT}
|
||||
/>
|
||||
<Typography.Title className="p-b-0" level={5}>
|
||||
{formatDateTimeFromSeconds(startTime)} to{' '}
|
||||
{formatDateTimeFromSeconds(endTime)}
|
||||
</Typography.Title>
|
||||
|
||||
<SVGIcons
|
||||
alt="result"
|
||||
className="tw-w-4 m-l-7 cursor-pointer"
|
||||
icon={Icons.ARROW_RIGHT}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
{isEmpty(viewData) && (
|
||||
<Empty
|
||||
className="my-4"
|
||||
description={t('label.no-execution-runs-found')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{Object.entries(viewData).map(([key, value]) => {
|
||||
return (
|
||||
<Row gutter={16} key={uniqueId()}>
|
||||
<Col span={5}>
|
||||
<Space>
|
||||
<div className="tree-view-dot" />
|
||||
<Typography.Text type="secondary">{key}</Typography.Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col span={19}>
|
||||
<Space>
|
||||
{value.map((status) => (
|
||||
<Tooltip
|
||||
html={
|
||||
<Space direction="vertical">
|
||||
<div>{status.timestamp}</div>
|
||||
<div>{status.executionStatus}</div>
|
||||
</Space>
|
||||
}
|
||||
key={uniqueId()}
|
||||
position="bottom">
|
||||
<SVGIcons
|
||||
alt="result"
|
||||
className="tw-w-6"
|
||||
icon={getStatusBadgeIcon(status.executionStatus)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TreeViewTab;
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
.status-border-top {
|
||||
border-top: 1px solid rgba(220, 227, 236, 0.3);
|
||||
}
|
||||
|
||||
.status-border-right {
|
||||
border-right: 1px solid rgba(220, 227, 236, 0.3);
|
||||
}
|
||||
|
||||
.tree-view-dot {
|
||||
border-radius: 6px;
|
||||
border: 6px solid #cccccc;
|
||||
}
|
||||
@ -54,12 +54,12 @@ import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo';
|
||||
import TabsPane from '../common/TabsPane/TabsPane';
|
||||
import PageContainer from '../containers/PageContainer';
|
||||
import EntityLineageComponent from '../EntityLineage/EntityLineage.component';
|
||||
import ExecutionsTab from '../Execution/Execution.component';
|
||||
import Loader from '../Loader/Loader';
|
||||
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||
import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal';
|
||||
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
|
||||
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
|
||||
import PipelineStatusList from '../PipelineStatusList/PipelineStatusList.component';
|
||||
import TasksDAGView from '../TasksDAGView/TasksDAGView';
|
||||
import { PipeLineDetailsProp } from './PipelineDetails.interface';
|
||||
|
||||
@ -123,14 +123,8 @@ const PipelineDetails = ({
|
||||
const [selectedField, setSelectedField] = useState<string>('');
|
||||
|
||||
const [elementRef, isInView] = useInfiniteScroll(observerOptions);
|
||||
const [selectedExecution, setSelectedExecution] = useState<PipelineStatus>(
|
||||
() => {
|
||||
if (pipelineStatus) {
|
||||
return pipelineStatus;
|
||||
} else {
|
||||
return {} as PipelineStatus;
|
||||
}
|
||||
}
|
||||
const [selectedExecution] = useState<PipelineStatus | undefined>(
|
||||
pipelineStatus
|
||||
);
|
||||
const [threadType, setThreadType] = useState<ThreadType>(
|
||||
ThreadType.Conversation
|
||||
@ -199,6 +193,17 @@ const PipelineDetails = ({
|
||||
position: 2,
|
||||
count: feedCount,
|
||||
},
|
||||
{
|
||||
name: 'Executions',
|
||||
icon: {
|
||||
alt: 'executions',
|
||||
name: 'executions',
|
||||
title: 'Executions',
|
||||
selectedName: 'activity-feed-color',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
name: 'Lineage',
|
||||
icon: {
|
||||
@ -208,12 +213,12 @@ const PipelineDetails = ({
|
||||
selectedName: 'icon-lineagecolor',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 3,
|
||||
position: 4,
|
||||
},
|
||||
{
|
||||
name: 'Custom Properties',
|
||||
isProtected: false,
|
||||
position: 4,
|
||||
position: 5,
|
||||
},
|
||||
];
|
||||
|
||||
@ -464,7 +469,7 @@ const PipelineDetails = ({
|
||||
/>
|
||||
|
||||
<div className="tw-flex-grow tw-flex tw-flex-col tw--mx-6 tw-px-7 tw-py-4">
|
||||
<div className="tw-flex-grow tw-flex tw-flex-col tw-bg-white tw-p-4 tw-shadow tw-rounded-md tw-w-full">
|
||||
<div className="tw-flex-grow tw-flex tw-flex-col tw-bg-white tw-shadow tw-rounded-md tw-w-full">
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<div className="tw-grid tw-grid-cols-4 tw-gap-4 tw-w-full">
|
||||
@ -514,17 +519,6 @@ const PipelineDetails = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<hr className="tw-my-3" />
|
||||
<div className="tw-px-6">
|
||||
<PipelineStatusList
|
||||
pipelineFQN={pipelineFQN}
|
||||
pipelineStatus={pipelineStatus}
|
||||
selectedExec={selectedExecution}
|
||||
onSelectExecution={(exec) => {
|
||||
setSelectedExecution(exec);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 2 && (
|
||||
@ -546,7 +540,8 @@ const PipelineDetails = ({
|
||||
<div />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 3 && (
|
||||
{activeTab === 3 && <ExecutionsTab pipelineFQN={pipelineFQN} />}
|
||||
{activeTab === 4 && (
|
||||
<div className="h-full">
|
||||
<EntityLineageComponent
|
||||
addLineageHandler={addLineageHandler}
|
||||
@ -567,7 +562,7 @@ const PipelineDetails = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 4 && (
|
||||
{activeTab === 5 && (
|
||||
<CustomPropertyTable
|
||||
entityDetails={
|
||||
pipelineDetails as CustomPropertyProps['entityDetails']
|
||||
|
||||
@ -199,6 +199,10 @@ jest.mock('../../utils/CommonUtils', () => ({
|
||||
getOwnerValue: jest.fn().mockReturnValue('Owner'),
|
||||
}));
|
||||
|
||||
jest.mock('', () => ({
|
||||
ExecutionsTab: jest.fn().mockImplementation(() => <p>Executions</p>),
|
||||
}));
|
||||
|
||||
describe('Test PipelineDetails component', () => {
|
||||
it('Checks if the PipelineDetails component has all the proper components rendered', async () => {
|
||||
const { container } = render(
|
||||
@ -230,13 +234,8 @@ describe('Test PipelineDetails component', () => {
|
||||
}
|
||||
);
|
||||
const taskDetail = await findByTestId(container, 'tasks-dag');
|
||||
const pipelineStatus = await findByTestId(
|
||||
container,
|
||||
'pipeline-status-list'
|
||||
);
|
||||
|
||||
expect(taskDetail).toBeInTheDocument();
|
||||
expect(pipelineStatus).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render no tasks data placeholder is tasks list is empty', async () => {
|
||||
@ -262,13 +261,25 @@ describe('Test PipelineDetails component', () => {
|
||||
expect(activityFeedList).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Check if active tab is lineage', async () => {
|
||||
it('should render execution tab if active tab is 3', async () => {
|
||||
const { container } = render(
|
||||
<PipelineDetails {...PipelineDetailsProps} activeTab={3} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
const executions = await findByText(container, 'Executions');
|
||||
|
||||
expect(executions).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Check if active tab is lineage', async () => {
|
||||
const { container } = render(
|
||||
<PipelineDetails {...PipelineDetailsProps} activeTab={4} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
const lineage = await findByTestId(container, 'lineage');
|
||||
|
||||
expect(lineage).toBeInTheDocument();
|
||||
@ -276,7 +287,7 @@ describe('Test PipelineDetails component', () => {
|
||||
|
||||
it('Check if active tab is custom properties', async () => {
|
||||
const { container } = render(
|
||||
<PipelineDetails {...PipelineDetailsProps} activeTab={4} />,
|
||||
<PipelineDetails {...PipelineDetailsProps} activeTab={5} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
@ -291,7 +302,7 @@ describe('Test PipelineDetails component', () => {
|
||||
|
||||
it('Should create an observer if IntersectionObserver is available', async () => {
|
||||
const { container } = render(
|
||||
<PipelineDetails {...PipelineDetailsProps} activeTab={4} />,
|
||||
<PipelineDetails {...PipelineDetailsProps} activeTab={5} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2022 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 { StatusType } from '../generated/entity/data/pipeline';
|
||||
|
||||
export const MenuOptions = {
|
||||
all: 'All',
|
||||
[StatusType.Successful]: 'Success',
|
||||
[StatusType.Failed]: 'Failure',
|
||||
[StatusType.Pending]: 'Pending',
|
||||
Aborted: 'Aborted',
|
||||
};
|
||||
|
||||
export const EXECUTION_FILTER_RANGE = {
|
||||
last3days: { days: 3, title: 'Last 3 days' },
|
||||
last7days: { days: 7, title: 'Last 7 days' },
|
||||
last14days: { days: 14, title: 'Last 14 days' },
|
||||
last30days: { days: 30, title: 'Last 30 days' },
|
||||
last60days: { days: 60, title: 'Last 60 days' },
|
||||
last365days: { days: 365, title: 'Last 365 days' },
|
||||
};
|
||||
@ -83,6 +83,7 @@ export enum TabSpecificField {
|
||||
DASHBOARD = 'dashboard',
|
||||
TABLE_CONSTRAINTS = 'tableConstraints',
|
||||
EXTENSION = 'extension',
|
||||
EXECUTIONS = 'executions',
|
||||
}
|
||||
|
||||
export enum FqnPart {
|
||||
|
||||
@ -4,13 +4,16 @@
|
||||
"type": "Type",
|
||||
"description": "Description",
|
||||
"docs": "Docs",
|
||||
"date-filter": "Date Filter",
|
||||
"api-uppercase": "API",
|
||||
"date-and-time": "Date & Time",
|
||||
"slack": "Slack",
|
||||
"logout": "Logout",
|
||||
"version": "Version",
|
||||
"explore": "Explore",
|
||||
"glossary": "Glossary",
|
||||
"tags": "Tags",
|
||||
"status": "Status",
|
||||
"settings": "Settings",
|
||||
"search-global": "Search for Tables, Topics, Dashboards, Pipelines and ML Models",
|
||||
"summary": "Summary",
|
||||
@ -193,7 +196,6 @@
|
||||
"pause": "Pause",
|
||||
"metadata-ingestion": "Metadata Ingestion",
|
||||
"unpause": "UnPause",
|
||||
"end-date": "End Date",
|
||||
"execution-date": "Execution Date",
|
||||
"start-date": "Start Date",
|
||||
"view-dag": "View Dag",
|
||||
@ -201,6 +203,8 @@
|
||||
"show-deleted": "Show deleted",
|
||||
"add-bot": "Add Bot",
|
||||
"search-for-bots": "Search for bots...",
|
||||
"list": "List",
|
||||
"tree": "Tree",
|
||||
"condition-is-invalid": "Condition is invalid",
|
||||
"rule-name": "Rule Name",
|
||||
"write-your-description": "Write your description",
|
||||
@ -208,6 +212,7 @@
|
||||
"select-rule-effect": "Select Rule Effect",
|
||||
"field-required": "{{field}} is required",
|
||||
"field-required-plural": "{{field}} are required",
|
||||
"no-execution-runs-found": "No execution runs found for the pipeline.",
|
||||
"last-no-of-days": "Last {{day}} Days",
|
||||
"tier-number": "Tier{{tier}}",
|
||||
"profiler-amp-data-quality": "Profiler & Data Quality",
|
||||
@ -239,22 +244,23 @@
|
||||
"entity-restored-error": "Error while restoring {{entity}}",
|
||||
"no-ingestion-available": "No ingestion data available",
|
||||
"no-ingestion-description": "To view Ingestion Data, run the MetaData Ingestion. Please refer to this doc to schedule the",
|
||||
"fetch-pipeline-status-error": "Error while fetching pipeline status."
|
||||
},
|
||||
"server": {
|
||||
"no-followed-entities": "You have not followed anything yet.",
|
||||
"no-owned-entities": "You have not owned anything yet.",
|
||||
"entity-fetch-error": "Error while fetching {{entity}}",
|
||||
"feed-post-error": "Error while posting the message!",
|
||||
"unexpected-error": "An unexpected error occurred.",
|
||||
"entity-creation-error": "Error while creating {{entity}}",
|
||||
"entity-updation-error": "Error while updating {{entity}}",
|
||||
"entity-updating-error": "Error while updating {{entity}}",
|
||||
"join-team-success": "Team joined successfully!",
|
||||
"leave-team-success": "Left the team successfully!",
|
||||
"join-team-error": "Error while joining the team!",
|
||||
"leave-team-error": "Error while leaving the team!",
|
||||
"field-insight": "Display the percentage of datasets with {{field}} by type.",
|
||||
"total-entity-insight": "Display the total of datasets by type.",
|
||||
"no-query-available": "No query available"
|
||||
},
|
||||
"server": {
|
||||
"no-query-available": "No query available",
|
||||
"unexpected-response": "Unexpected response from server!"
|
||||
},
|
||||
"url": {}
|
||||
|
||||
@ -297,7 +297,7 @@ const TeamsPage = () => {
|
||||
.catch((error: AxiosError) => {
|
||||
showErrorToast(
|
||||
error,
|
||||
t('message.entity-updation-error', {
|
||||
t('server.entity-updating-error', {
|
||||
entity: 'Team',
|
||||
})
|
||||
);
|
||||
@ -322,41 +322,36 @@ const TeamsPage = () => {
|
||||
};
|
||||
|
||||
const handleJoinTeamClick = (id: string, data: Operation[]) => {
|
||||
// setIsPageLoading(true);
|
||||
updateUserDetail(id, data)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
AppState.updateUserDetails(res);
|
||||
fetchTeamByFqn(selectedTeam.name);
|
||||
showSuccessToast(t('message.join-team-success'), 2000);
|
||||
showSuccessToast(t('server.join-team-success'), 2000);
|
||||
} else {
|
||||
throw t('message.join-team-error');
|
||||
throw t('server.join-team-error');
|
||||
}
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(err, t('message.join-team-error'));
|
||||
// setIsRightPanelLoading(false);
|
||||
showErrorToast(err, t('server.join-team-error'));
|
||||
});
|
||||
};
|
||||
|
||||
const handleLeaveTeamClick = (id: string, data: Operation[]) => {
|
||||
// setIsRightPanelLoading(true);
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
updateUserDetail(id, data)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
AppState.updateUserDetails(res);
|
||||
fetchTeamByFqn(selectedTeam.name);
|
||||
showSuccessToast(t('message.leave-team-success'), 2000);
|
||||
showSuccessToast(t('server.leave-team-success'), 2000);
|
||||
resolve();
|
||||
} else {
|
||||
throw t('message.leave-team-error');
|
||||
throw t('server.leave-team-error');
|
||||
}
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(err, t('message.leave-team-error'));
|
||||
// setIsRightPanelLoading(false);
|
||||
showErrorToast(err, t('server.leave-team-error'));
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -383,7 +378,7 @@ const TeamsPage = () => {
|
||||
.catch((error: AxiosError) => {
|
||||
showErrorToast(
|
||||
error,
|
||||
t('message.entity-updation-error', {
|
||||
t('server.entity-updating-error', {
|
||||
entity: 'Team',
|
||||
})
|
||||
);
|
||||
@ -421,7 +416,7 @@ const TeamsPage = () => {
|
||||
.catch((error: AxiosError) => {
|
||||
showErrorToast(
|
||||
error,
|
||||
t('message.entity-updation-error', {
|
||||
t('server.entity-updating-error', {
|
||||
entity: 'Team',
|
||||
})
|
||||
);
|
||||
|
||||
@ -74,6 +74,9 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
// Width
|
||||
.w-4 {
|
||||
width: 16px;
|
||||
}
|
||||
.w-8 {
|
||||
width: 32px;
|
||||
}
|
||||
@ -269,6 +272,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.transform-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.no-underline {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -166,6 +166,13 @@
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.m-l-7 {
|
||||
margin-left: 7.5rem;
|
||||
}
|
||||
|
||||
.m-r-7 {
|
||||
margin-right: 7.5rem;
|
||||
}
|
||||
.mt-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@ -32,6 +32,11 @@ export const pipelineDetailsTabs = [
|
||||
path: 'activity_feed',
|
||||
field: TabSpecificField.ACTIVITY_FEED,
|
||||
},
|
||||
{
|
||||
name: 'Executions',
|
||||
path: 'executions',
|
||||
field: TabSpecificField.EXECUTIONS,
|
||||
},
|
||||
{
|
||||
name: 'Lineage',
|
||||
path: 'lineage',
|
||||
@ -51,13 +56,18 @@ export const getCurrentPipelineTab = (tab: string) => {
|
||||
|
||||
break;
|
||||
|
||||
case 'lineage':
|
||||
case 'executions':
|
||||
currentTab = 3;
|
||||
|
||||
break;
|
||||
case 'custom_properties':
|
||||
|
||||
case 'lineage':
|
||||
currentTab = 4;
|
||||
|
||||
break;
|
||||
case 'custom_properties':
|
||||
currentTab = 5;
|
||||
|
||||
break;
|
||||
|
||||
case 'details':
|
||||
@ -109,7 +119,7 @@ export const STATUS_OPTIONS = [
|
||||
{ value: StatusType.Pending, label: StatusType.Pending },
|
||||
];
|
||||
|
||||
export const getStatusBadgeIcon = (status: StatusType) => {
|
||||
export const getStatusBadgeIcon = (status?: StatusType) => {
|
||||
switch (status) {
|
||||
case StatusType.Successful:
|
||||
return Icons.SUCCESS_BADGE;
|
||||
|
||||
@ -31,6 +31,7 @@ import IconAnnouncements from '../assets/svg/announcements.svg';
|
||||
import IconAPI from '../assets/svg/api.svg';
|
||||
import IconArrowDownPrimary from '../assets/svg/arrow-down-primary.svg';
|
||||
import IconArrowRightPrimary from '../assets/svg/arrow-right-primary.svg';
|
||||
import IconArrowRight from '../assets/svg/arrow-right.svg';
|
||||
import IconBotProfile from '../assets/svg/bot-profile.svg';
|
||||
import IconSuccess from '../assets/svg/check.svg';
|
||||
import IconCheckboxPrimary from '../assets/svg/checkbox-primary.svg';
|
||||
@ -317,6 +318,7 @@ export const Icons = {
|
||||
CIRCLE_CHECKBOX: 'icon-circle-checkbox',
|
||||
ARROW_RIGHT_PRIMARY: 'icon-arrow-right-primary',
|
||||
ARROW_DOWN_PRIMARY: 'icon-arrow-down-primary',
|
||||
ARROW_RIGHT: 'icon-arrow-right',
|
||||
ANNOUNCEMENT: 'icon-announcement',
|
||||
ANNOUNCEMENT_BLACK: 'icon-announcement-black',
|
||||
ANNOUNCEMENT_PURPLE: 'icon-announcement-purple',
|
||||
@ -858,6 +860,10 @@ const SVGIcons: FunctionComponent<Props> = ({
|
||||
case Icons.ARROW_DOWN_PRIMARY:
|
||||
IconComponent = IconArrowDownPrimary;
|
||||
|
||||
break;
|
||||
case Icons.ARROW_RIGHT:
|
||||
IconComponent = IconArrowRight;
|
||||
|
||||
break;
|
||||
case Icons.ARROW_RIGHT_PRIMARY:
|
||||
IconComponent = IconArrowRightPrimary;
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { toNumber } from 'lodash';
|
||||
import { isNil, toNumber } from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const msPerSecond = 1000;
|
||||
@ -181,6 +181,19 @@ export const getDateTimeByTimeStamp = (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* It takes a timestamp and returns a formatted date string
|
||||
* @param {number} timeStamp - The timestamp you want to convert to a date.
|
||||
* @param {string} [format] - The format of the date you want to return.
|
||||
* @returns A string
|
||||
*/
|
||||
export const getDateByTimeStamp = (
|
||||
timeStamp: number,
|
||||
format?: string
|
||||
): string => {
|
||||
return DateTime.fromMillis(timeStamp).toFormat(format || 'dd MMM yyyy');
|
||||
};
|
||||
|
||||
/**
|
||||
* It takes a timestamp and returns a relative date time string
|
||||
* @param {number} timestamp - number - The timestamp to convert to a relative date time.
|
||||
@ -216,7 +229,7 @@ export const getTimeZone = (): string => {
|
||||
})
|
||||
.slice(4);
|
||||
|
||||
// Line below finds out the abbrevation for time zone
|
||||
// Line below finds out the abbreviation for time zone
|
||||
// e.g. India Standard Time --> IST
|
||||
const abbreviation = timeZoneToString.match(/\b[A-Z]+/g)?.join('') || '';
|
||||
|
||||
@ -322,3 +335,62 @@ export const getFormattedDateFromMilliSeconds = (
|
||||
*/
|
||||
export const getDateTimeFromMilliSeconds = (timeStamp: number) =>
|
||||
DateTime.fromMillis(timeStamp).toLocaleString(DateTime.DATETIME_MED);
|
||||
|
||||
/**
|
||||
* It takes a timestamp and returns a string in the format of "dd MMM yyyy, hh:mm"
|
||||
* @param {number} timeStamp - number - The timestamp you want to convert to a date.
|
||||
* @returns A string ex: 23 May 2022, 23:59
|
||||
*/
|
||||
export const getDateTimeByTimeStampWithCommaSeparated = (
|
||||
timeStamp: number
|
||||
): string => {
|
||||
return `${DateTime.fromMillis(timeStamp).toFormat('dd MMM yyyy, hh:mm')}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a date string, return the time stamp of that date.
|
||||
* @param {string} date - The date you want to convert to a timestamp.
|
||||
*/
|
||||
export const getTimeStampByDate = (date: string) => Date.parse(date);
|
||||
|
||||
/**
|
||||
* @param date EPOCH Millis
|
||||
* @returns Formatted date for valid input. Format: MMM DD, YYYY, HH:MM AM/PM TimeZone
|
||||
*/
|
||||
export const formatDateTimeWithTimeZone = (date: number) => {
|
||||
if (isNil(date)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateTime = DateTime.fromMillis(date);
|
||||
|
||||
return dateTime.toLocaleString(DateTime.DATETIME_FULL);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param date EPOCH Millis
|
||||
* @returns Formatted date for valid input. Format: MMM DD, YYYY, HH:MM AM/PM
|
||||
*/
|
||||
export const formatDateTime = (date: number) => {
|
||||
if (isNil(date)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateTime = DateTime.fromMillis(date);
|
||||
|
||||
return dateTime.toLocaleString(DateTime.DATETIME_MED);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param date EPOCH seconds
|
||||
* @returns Formatted date for valid input. Format: MMM DD, YYYY, HH:MM AM/PM
|
||||
*/
|
||||
export const formatDateTimeFromSeconds = (date: number) => {
|
||||
if (isNil(date)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateTime = DateTime.fromSeconds(date);
|
||||
|
||||
return dateTime.toLocaleString(DateTime.DATETIME_MED);
|
||||
};
|
||||
|
||||
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Space } from 'antd';
|
||||
import { groupBy, isUndefined, toLower } from 'lodash';
|
||||
import React from 'react';
|
||||
import { MenuOptions } from '../constants/execution.constants';
|
||||
import { PipelineStatus, StatusType } from '../generated/entity/data/pipeline';
|
||||
import { getStatusBadgeIcon } from './PipelineDetailsUtils';
|
||||
import SVGIcons from './SvgUtils';
|
||||
import { formatDateTimeFromSeconds } from './TimeUtils';
|
||||
|
||||
interface StatusIndicatorInterface {
|
||||
status: StatusType;
|
||||
}
|
||||
|
||||
export interface ViewDataInterface {
|
||||
name: string;
|
||||
status?: StatusType;
|
||||
timestamp?: string;
|
||||
executionStatus?: StatusType;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export const StatusIndicator = ({ status }: StatusIndicatorInterface) => (
|
||||
<Space>
|
||||
<SVGIcons
|
||||
alt="result"
|
||||
className="tw-w-4"
|
||||
icon={getStatusBadgeIcon(status)}
|
||||
/>
|
||||
<p>
|
||||
{status === StatusType.Successful
|
||||
? MenuOptions[StatusType.Successful]
|
||||
: ''}
|
||||
{status === StatusType.Failed ? MenuOptions[StatusType.Failed] : ''}
|
||||
{status === StatusType.Pending ? MenuOptions[StatusType.Pending] : ''}
|
||||
</p>
|
||||
</Space>
|
||||
);
|
||||
|
||||
/**
|
||||
* It takes in an array of PipelineStatus objects and a string, and returns an array of
|
||||
* ViewDataInterface objects
|
||||
* @param {PipelineStatus[] | undefined} executions - PipelineStatus[] | undefined
|
||||
* @param {string | undefined} status - The status of the pipeline.
|
||||
*/
|
||||
export const getTableViewData = (
|
||||
executions: PipelineStatus[] | undefined,
|
||||
status: string | undefined
|
||||
): Array<ViewDataInterface> | undefined => {
|
||||
if (isUndefined(executions)) return;
|
||||
|
||||
const viewData: Array<ViewDataInterface> = [];
|
||||
executions?.map((execution) => {
|
||||
execution.taskStatus?.map((execute) => {
|
||||
viewData.push({
|
||||
name: execute.name,
|
||||
status: execute.executionStatus,
|
||||
timestamp: formatDateTimeFromSeconds(execution.timestamp as number),
|
||||
executionStatus: execute.executionStatus,
|
||||
type: '--',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return viewData.filter((data) =>
|
||||
status !== MenuOptions.all
|
||||
? toLower(data.status)?.includes(toLower(status))
|
||||
: data
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* It takes an array of objects and groups them by a property
|
||||
* @param {PipelineStatus[]} executions - PipelineStatus[] - This is the array of pipeline status
|
||||
* objects that we get from the API.
|
||||
* @param {string | undefined} status - The status of the pipeline.
|
||||
*/
|
||||
export const getTreeViewData = (
|
||||
executions: PipelineStatus[],
|
||||
status: string | undefined
|
||||
) => {
|
||||
const taskStatusArr = getTableViewData(executions, status);
|
||||
|
||||
return groupBy(taskStatusArr, 'name');
|
||||
};
|
||||
|
||||
export const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case StatusType.Successful:
|
||||
case StatusType.Pending:
|
||||
case StatusType.Failed:
|
||||
return MenuOptions[status];
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user