airbnb/caravel

View on GitHub
superset-frontend/src/SqlLab/components/QueryTable/index.tsx

Summary

Maintainability
D
2 days
Test Coverage
/**
 * 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 } from 'react';
import moment from 'moment';
import Card from 'src/components/Card';
import ProgressBar from 'src/components/ProgressBar';
import Label from 'src/components/Label';
import { t, useTheme, QueryResponse } from '@superset-ui/core';
import { useDispatch, useSelector } from 'react-redux';

import {
  queryEditorSetSql,
  cloneQueryToNewTab,
  fetchQueryResults,
  clearQueryResults,
  removeQuery,
} from 'src/SqlLab/actions/sqlLab';
import TableView from 'src/components/TableView';
import Button from 'src/components/Button';
import { fDuration } from 'src/utils/dates';
import Icons from 'src/components/Icons';
import { Tooltip } from 'src/components/Tooltip';
import { SqlLabRootState } from 'src/SqlLab/types';
import ModalTrigger from 'src/components/ModalTrigger';
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
import ResultSet from '../ResultSet';
import HighlightedSql from '../HighlightedSql';
import { StaticPosition, verticalAlign, StyledTooltip } from './styles';

interface QueryTableQuery
  extends Omit<QueryResponse, 'state' | 'sql' | 'progress' | 'results'> {
  state?: Record<string, any>;
  sql?: Record<string, any>;
  progress?: Record<string, any>;
  results?: Record<string, any>;
}

interface QueryTableProps {
  columns?: string[];
  queries?: QueryResponse[];
  onUserClicked?: Function;
  onDbClicked?: Function;
  displayLimit: number;
  latestQueryId?: string | undefined;
}

const openQuery = (id: number) => {
  const url = `/sqllab?queryId=${id}`;
  window.open(url);
};

const QueryTable = ({
  columns = ['started', 'duration', 'rows'],
  queries = [],
  onUserClicked = () => undefined,
  onDbClicked = () => undefined,
  displayLimit,
  latestQueryId,
}: QueryTableProps) => {
  const theme = useTheme();
  const dispatch = useDispatch();

  const QUERY_HISTORY_TABLE_HEADERS_LOCALIZED = {
    state: t('State'),
    started: t('Started'),
    duration: t('Duration'),
    progress: t('Progress'),
    rows: t('Rows'),
    sql: t('SQL'),
    results: t('Results'),
    actions: t('Actions'),
  };

  const setHeaders = (column: string) => {
    if (column === 'sql') {
      return column.toUpperCase();
    }
    return column.charAt(0).toUpperCase().concat(column.slice(1));
  };

  const columnsOfTable = useMemo(
    () =>
      columns.map(column => ({
        accessor: column,
        Header:
          QUERY_HISTORY_TABLE_HEADERS_LOCALIZED[column] || setHeaders(column),
        disableSortBy: true,
      })),
    [columns],
  );

  const user = useSelector<SqlLabRootState, User>(state => state.user);

  const data = useMemo(() => {
    const restoreSql = (query: QueryResponse) => {
      dispatch(
        queryEditorSetSql({ id: query.sqlEditorId }, query.sql, query.id),
      );
    };

    const openQueryInNewTab = (query: QueryResponse) => {
      dispatch(cloneQueryToNewTab(query, true));
    };

    const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
      dispatch(fetchQueryResults(query, displayLimit));
    };

    const statusAttributes = {
      success: {
        config: {
          icon: <Icons.Check iconColor={theme.colors.success.base} />,
          label: t('Success'),
        },
      },
      failed: {
        config: {
          icon: <Icons.XSmall iconColor={theme.colors.error.base} />,
          label: t('Failed'),
        },
      },
      stopped: {
        config: {
          icon: <Icons.XSmall iconColor={theme.colors.error.base} />,
          label: t('Failed'),
        },
      },
      running: {
        config: {
          icon: <Icons.Running iconColor={theme.colors.primary.base} />,
          label: t('Running'),
        },
      },
      fetching: {
        config: {
          icon: <Icons.Queued iconColor={theme.colors.primary.base} />,
          label: t('Fetching'),
        },
      },
      timed_out: {
        config: {
          icon: <Icons.Offline iconColor={theme.colors.grayscale.light1} />,
          label: t('Offline'),
        },
      },
      scheduled: {
        config: {
          icon: <Icons.Queued iconColor={theme.colors.grayscale.base} />,
          label: t('Scheduled'),
        },
      },
      pending: {
        config: {
          icon: <Icons.Queued iconColor={theme.colors.grayscale.base} />,
          label: t('Scheduled'),
        },
      },
      error: {
        config: {
          icon: <Icons.Error iconColor={theme.colors.error.base} />,
          label: t('Unknown Status'),
        },
      },
    };

    return queries
      .map(query => {
        const { state, sql, progress, ...rest } = query;
        const q = rest as QueryTableQuery;

        const status = statusAttributes[state] || statusAttributes.error;

        if (q.endDttm) {
          q.duration = fDuration(q.startDttm, q.endDttm);
        }
        const time = moment(q.startDttm).format().split('T');
        q.time = (
          <div>
            <span>
              {time[0]} <br /> {time[1]}
            </span>
          </div>
        );
        q.user = (
          <Button
            buttonSize="small"
            buttonStyle="link"
            onClick={() => onUserClicked(q.userId)}
          >
            {q.user}
          </Button>
        );
        q.db = (
          <Button
            buttonSize="small"
            buttonStyle="link"
            onClick={() => onDbClicked(q.dbId)}
          >
            {q.db}
          </Button>
        );
        q.started = moment(q.startDttm).format('L HH:mm:ss');
        q.querylink = (
          <Button
            buttonSize="small"
            buttonStyle="link"
            onClick={() => openQuery(q.queryId)}
          >
            <i className="fa fa-external-link m-r-3" />
            {t('Edit')}
          </Button>
        );
        q.sql = (
          <Card css={[StaticPosition]}>
            <HighlightedSql
              sql={sql}
              rawSql={q.executedSql}
              shrink
              maxWidth={60}
            />
          </Card>
        );
        if (q.resultsKey) {
          q.results = (
            <ModalTrigger
              className="ResultsModal"
              triggerNode={
                <Label type="info" className="pointer">
                  {t('View')}
                </Label>
              }
              modalTitle={t('Data preview')}
              beforeOpen={() => openAsyncResults(query, displayLimit)}
              onExit={() => dispatch(clearQueryResults(query))}
              modalBody={
                <ResultSet
                  showSql
                  queryId={query.id}
                  height={400}
                  displayLimit={displayLimit}
                  defaultQueryLimit={1000}
                />
              }
              responsive
            />
          );
        } else {
          q.results = <></>;
        }

        q.progress =
          state === 'success' ? (
            <ProgressBar
              percent={parseInt(progress.toFixed(0), 10)}
              striped
              showInfo={false}
            />
          ) : (
            <ProgressBar percent={parseInt(progress.toFixed(0), 10)} striped />
          );
        q.state = (
          <Tooltip title={status.config.label} placement="bottom">
            <span>{status.config.icon}</span>
          </Tooltip>
        );
        q.actions = (
          <div>
            <StyledTooltip
              onClick={() => restoreSql(query)}
              tooltip={t(
                'Overwrite text in the editor with a query on this table',
              )}
              placement="top"
            >
              <Icons.Edit iconSize="xl" />
            </StyledTooltip>
            <StyledTooltip
              onClick={() => openQueryInNewTab(query)}
              tooltip={t('Run query in a new tab')}
              placement="top"
            >
              <Icons.PlusCircleOutlined iconSize="xl" css={verticalAlign} />
            </StyledTooltip>
            {q.id !== latestQueryId && (
              <StyledTooltip
                tooltip={t('Remove query from log')}
                onClick={() => dispatch(removeQuery(query))}
              >
                <Icons.Trash iconSize="xl" />
              </StyledTooltip>
            )}
          </div>
        );
        return q;
      })
      .reverse();
  }, [queries, onUserClicked, onDbClicked, user, displayLimit]);

  return (
    <div className="QueryTable">
      <TableView
        columns={columnsOfTable}
        data={data}
        className="table-condensed"
        pageSize={50}
      />
    </div>
  );
};

export default QueryTable;