blob: a72cc760dae1fa9beb36c7d165958a1533eb6d92 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 { useMemo, useState, useCallback, ReactElement, useEffect } from 'react';
import { Link, useHistory } from 'react-router-dom';
import {
css,
QueryState,
styled,
SupersetClient,
t,
useTheme,
} from '@superset-ui/core';
import {
createFetchRelated,
createFetchDistinct,
createErrorHandler,
shortenSQL,
} from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
import { useListViewResource } from 'src/views/CRUD/hooks';
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
import { Popover, Label, Tooltip } from '@superset-ui/core/components';
import { commonMenuData } from 'src/features/home/commonMenuData';
import {
ListView,
ListViewFilterOperator as FilterOperator,
type ListViewProps,
type ListViewFilters,
} from 'src/components';
import CodeSyntaxHighlighter, {
preloadLanguages,
} from '@superset-ui/core/components/CodeSyntaxHighlighter';
import { DATETIME_WITH_TIME_ZONE, TIME_WITH_MS } from 'src/constants';
import { QueryObject, QueryObjectColumns } from 'src/views/CRUD/types';
import { Icons } from '@superset-ui/core/components/Icons';
import QueryPreviewModal from 'src/features/queries/QueryPreviewModal';
import { addSuccessToast } from 'src/components/MessageToasts/actions';
import getOwnerName from 'src/utils/getOwnerName';
import { extendedDayjs } from '@superset-ui/core/utils/dates';
const PAGE_SIZE = 25;
const SQL_PREVIEW_MAX_LINES = 4;
const TopAlignedListView = styled(ListView)<ListViewProps<QueryObject>>`
table .ant-table-cell {
vertical-align: top;
}
`;
const StyledCodeSyntaxHighlighter = styled(CodeSyntaxHighlighter)`
height: ${({ theme }) => theme.sizeUnit * 26}px;
overflow: hidden !important; /* needed to override inline styles */
text-overflow: ellipsis;
white-space: nowrap;
/* Ensure the syntax highlighter content respects the container constraints */
& > div {
height: 100%;
overflow: hidden;
}
pre {
height: 100% !important;
overflow: hidden !important;
margin: 0 !important;
}
`;
interface QueryListProps {
addDangerToast: (msg: string, config?: any) => any;
addSuccessToast: (msg: string, config?: any) => any;
}
const StyledTableLabel = styled.div`
.count {
margin-left: 5px;
color: ${({ theme }) => theme.colorPrimary};
text-decoration: underline;
cursor: pointer;
}
`;
const StyledPopoverItem = styled.div`
color: ${({ theme }) => theme.colorText};
`;
const TimerLabel = styled(Label)`
text-align: left;
font-family: ${({ theme }) => theme.fontFamilyCode};
`;
function QueryList({ addDangerToast }: QueryListProps) {
const {
state: { loading, resourceCount: queryCount, resourceCollection: queries },
fetchData,
} = useListViewResource<QueryObject>(
'query',
t('Query history'),
addDangerToast,
false,
);
const [queryCurrentlyPreviewing, setQueryCurrentlyPreviewing] =
useState<QueryObject>();
const theme = useTheme();
const history = useHistory();
// Preload SQL language since this component will definitely display SQL
useEffect(() => {
preloadLanguages(['sql']);
}, []);
const handleQueryPreview = useCallback(
(id: number) => {
SupersetClient.get({
endpoint: `/api/v1/query/${id}`,
}).then(
({ json = {} }) => {
setQueryCurrentlyPreviewing({ ...json.result });
},
createErrorHandler(errMsg =>
addDangerToast(
t('There was an issue previewing the selected query. %s', errMsg),
),
),
);
},
[addDangerToast],
);
const menuData: SubMenuProps = {
activeChild: 'Query history',
...commonMenuData,
};
const initialSort = [{ id: QueryObjectColumns.StartTime, desc: true }];
const columns = useMemo(
() => [
{
Cell: ({
row: {
original: { status },
},
}: {
row: {
original: {
status: QueryState;
};
};
}) => {
const statusConfig: {
name: ReactElement | null;
label: string;
} = {
name: null,
label: '',
};
if (status === QueryState.Success) {
statusConfig.name = (
<Icons.CheckOutlined
iconSize="m"
iconColor={theme.colorSuccess}
css={css`
vertical-align: -webkit-baseline-middle;
`}
/>
);
statusConfig.label = t('Success');
} else if (
status === QueryState.Failed ||
status === QueryState.Stopped
) {
statusConfig.name = (
<Icons.CloseOutlined
iconSize="m"
iconColor={
status === QueryState.Failed
? theme.colorError
: theme.colorIcon
}
/>
);
statusConfig.label = t('Failed');
} else if (status === QueryState.Running) {
statusConfig.name = (
<Icons.LoadingOutlined
iconSize="m"
iconColor={theme.colorPrimary}
/>
);
statusConfig.label = t('Running');
} else if (status === QueryState.TimedOut) {
statusConfig.name = (
<Icons.CircleSolid iconSize="m" iconColor={theme.colorIcon} />
);
statusConfig.label = t('Offline');
} else if (
status === QueryState.Scheduled ||
status === QueryState.Pending
) {
statusConfig.name = <Icons.Queued iconSize="m" />;
statusConfig.label = t('Scheduled');
}
return (
<Tooltip title={statusConfig.label} placement="bottom">
<span>{statusConfig.name}</span>
</Tooltip>
);
},
accessor: QueryObjectColumns.Status,
size: 'xs',
disableSortBy: true,
id: QueryObjectColumns.Status,
},
{
accessor: QueryObjectColumns.StartTime,
Header: t('Time'),
size: 'lg',
Cell: ({
row: {
original: { start_time },
},
}: any) => {
const start = extendedDayjs.utc(start_time).local();
const formattedStartTimeData = start
.format(DATETIME_WITH_TIME_ZONE)
.split(' ');
const formattedStartTime = (
<>
{formattedStartTimeData[0]} <br />
{formattedStartTimeData[1]}
</>
);
return formattedStartTime;
},
id: QueryObjectColumns.StartTime,
},
{
Header: t('Duration'),
size: 'lg',
Cell: ({
row: {
original: { status, start_time, start_running_time, end_time },
},
}: any) => {
const timerType = status === QueryState.Failed ? 'danger' : status;
// Use start_running_time if available for more accurate duration
const startTime = start_running_time || start_time;
const timerTime =
end_time && startTime
? extendedDayjs(extendedDayjs.utc(end_time - startTime)).format(
TIME_WITH_MS,
)
: '00:00:00.000';
return (
<TimerLabel type={timerType} role="timer">
{timerTime}
</TimerLabel>
);
},
id: 'duration',
},
{
accessor: QueryObjectColumns.TabName,
Header: t('Tab name'),
size: 'xl',
id: QueryObjectColumns.TabName,
},
{
accessor: QueryObjectColumns.DatabaseName,
Header: t('Database'),
size: 'lg',
id: QueryObjectColumns.DatabaseName,
},
{
accessor: QueryObjectColumns.Database,
hidden: true,
id: QueryObjectColumns.Database,
},
{
accessor: QueryObjectColumns.Schema,
Header: t('Schema'),
size: 'lg',
id: QueryObjectColumns.Schema,
},
{
Cell: ({
row: {
original: { sql_tables: tables = [] },
},
}: any) => {
const names = tables.map((table: any) => table.table);
const main = names.length > 0 ? names.shift() : '';
if (names.length) {
return (
<StyledTableLabel>
<span>{main}</span>
<Popover
placement="right"
title={t('TABLES')}
trigger="click"
content={
<>
{names.map((name: string) => (
<StyledPopoverItem key={name}>{name}</StyledPopoverItem>
))}
</>
}
>
<span className="count">(+{names.length})</span>
</Popover>
</StyledTableLabel>
);
}
return main;
},
accessor: QueryObjectColumns.SqlTables,
Header: t('Tables'),
size: 'lg',
disableSortBy: true,
id: QueryObjectColumns.SqlTables,
},
{
accessor: QueryObjectColumns.UserFirstName,
Header: t('User'),
size: 'xl',
Cell: ({
row: {
original: { user },
},
}: any) => getOwnerName(user),
id: QueryObjectColumns.UserFirstName,
},
{
accessor: QueryObjectColumns.User,
hidden: true,
id: QueryObjectColumns.User,
},
{
accessor: QueryObjectColumns.Rows,
Header: t('Rows'),
size: 'sm',
id: QueryObjectColumns.Rows,
},
{
accessor: QueryObjectColumns.Sql,
Header: t('SQL'),
Cell: ({ row: { original, id } }: any) => (
<div
tabIndex={0}
role="button"
data-test={`open-sql-preview-${id}`}
onClick={() => setQueryCurrentlyPreviewing(original)}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setQueryCurrentlyPreviewing(original);
}
}}
style={{ cursor: 'pointer' }}
>
<StyledCodeSyntaxHighlighter
language="sql"
customStyle={{
cursor: 'pointer',
userSelect: 'none',
}}
>
{shortenSQL(original.sql, SQL_PREVIEW_MAX_LINES)}
</StyledCodeSyntaxHighlighter>
</div>
),
size: 'xxl',
id: QueryObjectColumns.Sql,
},
{
Header: t('Actions'),
id: 'actions',
disableSortBy: true,
size: 'sm',
Cell: ({
row: {
original: { id },
},
}: any) => (
<Tooltip title={t('Open query in SQL Lab')} placement="bottom">
<Link to={`/sqllab?queryId=${id}`}>
<Icons.Full iconSize="l" />
</Link>
</Tooltip>
),
},
],
[theme], // Add theme to dependencies since it's used in the columns
);
const filters: ListViewFilters = useMemo(
() => [
{
Header: t('Database'),
key: 'database',
id: 'database',
input: 'select',
operator: FilterOperator.RelationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'query',
'database',
createErrorHandler(errMsg =>
addDangerToast(
t('An error occurred while fetching database values: %s', errMsg),
),
),
),
paginate: true,
},
{
Header: t('State'),
key: 'state',
id: 'status',
input: 'select',
operator: FilterOperator.Equals,
unfilteredLabel: 'All',
fetchSelects: createFetchDistinct(
'query',
'status',
createErrorHandler(errMsg =>
addDangerToast(
t('An error occurred while fetching schema values: %s', errMsg),
),
),
),
paginate: true,
},
{
Header: t('User'),
key: 'user',
id: 'user',
input: 'select',
operator: FilterOperator.RelationOneMany,
unfilteredLabel: 'All',
fetchSelects: createFetchRelated(
'query',
'user',
createErrorHandler(errMsg =>
addDangerToast(
t('An error occurred while fetching user values: %s', errMsg),
),
),
),
paginate: true,
},
{
Header: t('Time range'),
key: 'start_time',
id: 'start_time',
input: 'datetime_range',
operator: FilterOperator.Between,
},
{
Header: t('Search by query text'),
key: 'sql',
id: 'sql',
input: 'search',
operator: FilterOperator.Contains,
},
],
[addDangerToast],
);
return (
<>
<SubMenu {...menuData} />
{queryCurrentlyPreviewing && (
<QueryPreviewModal
onHide={() => setQueryCurrentlyPreviewing(undefined)}
query={queryCurrentlyPreviewing}
queries={queries}
fetchData={handleQueryPreview}
openInSqlLab={(id: number) => history.push(`/sqllab?queryId=${id}`)}
show
/>
)}
<TopAlignedListView
className="query-history-list-view"
columns={columns}
count={queryCount}
data={queries}
fetchData={fetchData}
filters={filters}
initialSort={initialSort}
loading={loading}
pageSize={PAGE_SIZE}
highlightRowId={queryCurrentlyPreviewing?.id}
refreshData={() => {}}
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
/>
</>
);
}
export default withToasts(QueryList);