From 432e5ab4607fe4743562101336c63dcfd18b0867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CA=88=E1=B5=83=E1=B5=A2?= Date: Thu, 12 Nov 2020 11:55:13 -1000 Subject: [PATCH] feat: Query History CRUD list view (#11574) --- superset-frontend/images/icons/offline.svg | 21 ++ superset-frontend/images/icons/queued.svg | 23 ++ superset-frontend/images/icons/running.svg | 21 ++ superset-frontend/package-lock.json | 2 +- .../CRUD/annotation/AnnotationList_spec.jsx | 6 +- .../src/SqlLab/components/App.jsx | 9 +- .../src/components/Icon/index.tsx | 159 ++++---- .../src/components/ListView/ListView.tsx | 4 + superset-frontend/src/constants.ts | 21 ++ superset-frontend/src/featureFlags.ts | 1 + superset-frontend/src/views/App.tsx | 6 + .../src/views/CRUD/data/common.ts | 7 + .../views/CRUD/data/query/QueryList.test.tsx | 100 +++++ .../src/views/CRUD/data/query/QueryList.tsx | 349 ++++++++++++++++++ .../CRUD/data/savedquery/SavedQueryList.tsx | 2 +- superset-frontend/src/views/CRUD/hooks.ts | 10 +- superset/config.py | 1 + superset/queries/api.py | 3 +- superset/views/base.py | 13 + superset/views/core.py | 11 + tests/queries/api_tests.py | 1 + 21 files changed, 683 insertions(+), 87 deletions(-) create mode 100644 superset-frontend/images/icons/offline.svg create mode 100644 superset-frontend/images/icons/queued.svg create mode 100644 superset-frontend/images/icons/running.svg create mode 100644 superset-frontend/src/constants.ts create mode 100644 superset-frontend/src/views/CRUD/data/query/QueryList.test.tsx create mode 100644 superset-frontend/src/views/CRUD/data/query/QueryList.tsx diff --git a/superset-frontend/images/icons/offline.svg b/superset-frontend/images/icons/offline.svg new file mode 100644 index 000000000..b11359678 --- /dev/null +++ b/superset-frontend/images/icons/offline.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/images/icons/queued.svg b/superset-frontend/images/icons/queued.svg new file mode 100644 index 000000000..0b6ad0987 --- /dev/null +++ b/superset-frontend/images/icons/queued.svg @@ -0,0 +1,23 @@ + + + + + + diff --git a/superset-frontend/images/icons/running.svg b/superset-frontend/images/icons/running.svg new file mode 100644 index 000000000..a1dc59a45 --- /dev/null +++ b/superset-frontend/images/icons/running.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 4e3128a8e..81241570b 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -27270,7 +27270,7 @@ "dependencies": { "core-js": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "resolved": "http://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" } } diff --git a/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx index 1fc7089e0..e096c7c6f 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx @@ -103,15 +103,15 @@ describe('AnnotationList', () => { it('fetches annotation layer', () => { const callsQ = fetchMock.calls(/annotation_layer\/1/); - expect(callsQ).toHaveLength(3); - expect(callsQ[2][0]).toMatchInlineSnapshot( + expect(callsQ).toHaveLength(2); + expect(callsQ[1][0]).toMatchInlineSnapshot( `"http://localhost/api/v1/annotation_layer/1"`, ); }); it('fetches annotations', () => { const callsQ = fetchMock.calls(/annotation_layer\/1\/annotation/); - expect(callsQ).toHaveLength(2); + expect(callsQ).toHaveLength(1); expect(callsQ[0][0]).toMatchInlineSnapshot( `"http://localhost/api/v1/annotation_layer/1/annotation/?q=(order_column:short_descr,order_direction:desc,page:0,page_size:25)"`, ); diff --git a/superset-frontend/src/SqlLab/components/App.jsx b/superset-frontend/src/SqlLab/components/App.jsx index 481a60233..127b21f2c 100644 --- a/superset-frontend/src/SqlLab/components/App.jsx +++ b/superset-frontend/src/SqlLab/components/App.jsx @@ -21,6 +21,7 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { t, supersetTheme, ThemeProvider } from '@superset-ui/core'; +import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import throttle from 'lodash/throttle'; import TabbedSqlEditors from './TabbedSqlEditors'; import QueryAutoRefresh from './QueryAutoRefresh'; @@ -86,7 +87,13 @@ class App extends React.PureComponent { render() { let content; - if (this.state.hash) { + if (this.state.hash && this.state.hash === '#search') { + if ( + isFeatureEnabled(FeatureFlag.ENABLE_REACT_CRUD_VIEWS) && + isFeatureEnabled(FeatureFlag.SIP_34_QUERY_SEARCH_UI) + ) { + return window.location.replace('/superset/sqllab/history/'); + } content = ( > > = { 'alert-solid': AlertSolidIcon, - alert: AlertIcon, - binoculars: BinocularsIcon, 'bolt-small-run': BoltSmallRunIcon, 'bolt-small': BoltSmallIcon, - bolt: BoltIcon, - calendar: CalendarIcon, 'cancel-solid': CancelSolidIcon, 'cancel-x': CancelXIcon, - cancel: CancelIcon, 'card-view': CardViewIcon, 'cards-locked': CardsLockedIcon, - cards: CardsIcon, 'caret-down': CaretDownIcon, 'caret-left': CaretLeftIcon, 'caret-right': CaretRightIcon, 'caret-up': CaretUpIcon, - certified: CertifiedIcon, - check: CheckIcon, 'checkbox-half': CheckboxHalfIcon, 'checkbox-off': CheckboxOffIcon, 'checkbox-on': CheckboxOnIcon, 'circle-check-solid': CircleCheckSolidIcon, 'circle-check': CircleCheckIcon, - circle: CircleIcon, - clock: ClockIcon, - close: CloseIcon, - code: CodeIcon, - cog: CogIcon, - collapse: CollapseIcon, 'color-palette': ColorPaletteIcon, - components: ComponentsIcon, - copy: CopyIcon, 'cursor-target': CursorTargeIcon, - database: DatabaseIcon, 'dataset-physical': DatasetPhysicalIcon, 'dataset-virtual-greyscale': DatasetVirtualGreyscaleIcon, 'dataset-virtual': DatasetVirtualIcon, - download: DownloadIcon, 'edit-alt': EditAltIcon, - edit: EditIcon, - email: EmailIcon, 'error-solid-small': ErrorSolidSmallIcon, 'error-solid': ErrorSolidIcon, - error: ErrorIcon, - expand: ExpandIcon, 'eye-slash': EyeSlashIcon, - eye: EyeIcon, - 'favorite-small-selected': FavoriteSmallSelectedIcon, 'favorite-selected': FavoriteSelectedIcon, + 'favorite-small-selected': FavoriteSmallSelectedIcon, 'favorite-unselected': FavoriteUnselectedIcon, 'field-abc': FieldABCIcon, 'field-boolean': FieldBooleanIcon, @@ -316,66 +299,92 @@ export const iconsRegistry: Record< 'field-derived': FieldDerivedIcon, 'field-num': FieldNumIcon, 'field-struct': FieldStructIcon, - file: FileIcon, - filter: FilterIcon, 'filter-small': FilterSmallIcon, - folder: FolderIcon, - full: FullIcon, - gear: GearIcon, - grid: GridIcon, - image: ImageIcon, - import: ImportIcon, 'info-solid-small': InfoSolidSmallIcon, 'info-solid': InfoSolidIcon, - info: InfoIcon, - join: JoinIcon, - keyboard: KeyboardIcon, - layers: LayersIcon, - lightbulb: LightbulbIcon, 'list-view': ListViewIcon, - list: ListIcon, - location: LocationIcon, 'lock-locked': LockLockedIcon, 'lock-unlocked': LockUnlockedIcon, - map: MapIcon, - message: MessageIcon, 'minus-solid': MinusSolidIcon, - minus: MinusIcon, 'more-horiz': MoreHorizIcon, - move: MoveIcon, 'nav-charts': NavChartsIcon, 'nav-dashboard': NavDashboardIcon, 'nav-data': NavDataIcon, 'nav-explore': NavExploreIcon, 'nav-home': NavHomeIcon, 'nav-lab': NavLabIcon, - note: NoteIcon, - paperclip: PaperclipIcon, - placeholder: PlaceholderIcon, 'plus-large': PlusLargeIcon, 'plus-small': PlusSmallIcon, 'plus-solid': PlusSolidIcon, + 'sort-asc': SortAscIcon, + 'sort-desc': SortDescIcon, + 'triangle-change': TriangleChangeIcon, + 'triangle-down': TriangleDownIcon, + 'triangle-up': TriangleUpIcon, + 'up-level': UpLevelIcon, + 'warning-solid': WarningSolidIcon, + 'x-large': XLargeIcon, + 'x-small': XSmallIcon, + alert: AlertIcon, + binoculars: BinocularsIcon, + bolt: BoltIcon, + calendar: CalendarIcon, + cancel: CancelIcon, + cards: CardsIcon, + certified: CertifiedIcon, + check: CheckIcon, + circle: CircleIcon, + clock: ClockIcon, + close: CloseIcon, + code: CodeIcon, + cog: CogIcon, + collapse: CollapseIcon, + components: ComponentsIcon, + copy: CopyIcon, + database: DatabaseIcon, + download: DownloadIcon, + edit: EditIcon, + email: EmailIcon, + error: ErrorIcon, + expand: ExpandIcon, + eye: EyeIcon, + file: FileIcon, + filter: FilterIcon, + folder: FolderIcon, + full: FullIcon, + gear: GearIcon, + grid: GridIcon, + image: ImageIcon, + import: ImportIcon, + info: InfoIcon, + join: JoinIcon, + keyboard: KeyboardIcon, + layers: LayersIcon, + lightbulb: LightbulbIcon, + list: ListIcon, + location: LocationIcon, + map: MapIcon, + message: MessageIcon, + minus: MinusIcon, + move: MoveIcon, + note: NoteIcon, + offline: OfflineIcon, + paperclip: PaperclipIcon, + placeholder: PlaceholderIcon, plus: PlusIcon, + queued: QueuedIcon, refresh: RefreshIcon, + running: RunningIcon, search: SearchIcon, server: ServerIcon, share: ShareIcon, - 'sort-asc': SortAscIcon, - 'sort-desc': SortDescIcon, sort: SortIcon, sql: SQLIcon, table: TableIcon, tag: TagIcon, trash: TrashIcon, - 'triangle-change': TriangleChangeIcon, - 'triangle-down': TriangleDownIcon, - 'triangle-up': TriangleUpIcon, - 'up-level': UpLevelIcon, user: UserIcon, - 'warning-solid': WarningSolidIcon, warning: WarningIcon, - 'x-large': XLargeIcon, - 'x-small': XSmallIcon, }; interface IconProps extends SVGProps { diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index f496680f7..635a08c2b 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -61,6 +61,10 @@ const ListViewStyles = styled.div` margin-bottom: 0; } + .body { + overflow-x: auto; + } + .ant-empty { .ant-empty-image { height: auto; diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts new file mode 100644 index 000000000..a1ce0ae4f --- /dev/null +++ b/superset-frontend/src/constants.ts @@ -0,0 +1,21 @@ +/** + * 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. + */ +export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ'; + +export const TIME_WITH_MS = 'HH:mm:ss.SSS'; diff --git a/superset-frontend/src/featureFlags.ts b/superset-frontend/src/featureFlags.ts index b64a5ed64..1bac02dcb 100644 --- a/superset-frontend/src/featureFlags.ts +++ b/superset-frontend/src/featureFlags.ts @@ -32,6 +32,7 @@ export enum FeatureFlag { DISPLAY_MARKDOWN_HTML = 'DISPLAY_MARKDOWN_HTML', ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML', VERSIONED_EXPORT = 'VERSIONED_EXPORT', + SIP_34_QUERY_SEARCH_UI = 'SIP_34_QUERY_SEARCH_UI', } export type FeatureFlagMap = { diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx index 103db080a..b03496eff 100644 --- a/superset-frontend/src/views/App.tsx +++ b/superset-frontend/src/views/App.tsx @@ -36,6 +36,7 @@ import SavedQueryList from 'src/views/CRUD/data/savedquery/SavedQueryList'; import CssTemplatesList from 'src/views/CRUD/csstemplates/CssTemplatesList'; import AnnotationLayersList from 'src/views/CRUD/annotationlayers/AnnotationLayersList'; import AnnotationList from 'src/views/CRUD/annotation/AnnotationList'; +import QueryList from 'src/views/CRUD/data/query/QueryList'; import messageToastReducer from '../messageToasts/reducers'; import { initEnhancer } from '../reduxUtils'; @@ -115,6 +116,11 @@ const App = () => ( + + + + + diff --git a/superset-frontend/src/views/CRUD/data/common.ts b/superset-frontend/src/views/CRUD/data/common.ts index 6fecd21d5..4f89b5dad 100644 --- a/superset-frontend/src/views/CRUD/data/common.ts +++ b/superset-frontend/src/views/CRUD/data/common.ts @@ -17,6 +17,7 @@ * under the License. */ import { t } from '@superset-ui/core'; +import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; export const commonMenuData = { name: t('Data'), @@ -39,5 +40,11 @@ export const commonMenuData = { url: '/savedqueryview/list/', usesRouter: true, }, + { + name: 'Query History', + label: t('Query History'), + url: '/superset/sqllab/history/', + usesRouter: isFeatureEnabled(FeatureFlag.SIP_34_QUERY_SEARCH_UI), + }, ], }; diff --git a/superset-frontend/src/views/CRUD/data/query/QueryList.test.tsx b/superset-frontend/src/views/CRUD/data/query/QueryList.test.tsx new file mode 100644 index 000000000..774c93b58 --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/query/QueryList.test.tsx @@ -0,0 +1,100 @@ +/** + * 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 React from 'react'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; +import fetchMock from 'fetch-mock'; + +import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; +import { styledMount as mount } from 'spec/helpers/theming'; + +import QueryList, { QueryObject } from 'src/views/CRUD/data/query/QueryList'; +import ListView from 'src/components/ListView'; +import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; + +// store needed for withToasts +const mockStore = configureStore([thunk]); +const store = mockStore({}); + +const queriesEndpoint = 'glob:*/api/v1/query/?*'; + +const mockQueries: QueryObject[] = [...new Array(3)].map((_, i) => ({ + changed_on: new Date().toISOString(), + id: i, + slice_name: `cool chart ${i}`, + database: { + database_name: 'main db', + }, + schema: 'public', + sql: `SELECT ${i} FROM table`, + sql_tables: [ + { schema: 'foo', table: 'table' }, + { schema: 'bar', table: 'table_2' }, + ], + status: 'success', + tab_name: 'Main Tab', + user: { + first_name: 'cool', + last_name: 'dude', + id: 2, + username: 'cooldude', + }, + start_time: new Date().valueOf(), + end_time: new Date().valueOf(), + rows: 200, + tmp_table_name: '', + tracking_url: '', +})); + +fetchMock.get(queriesEndpoint, { + result: mockQueries, + chart_count: 3, +}); + +describe('QueryList', () => { + const mockedProps = {}; + const wrapper = mount(, { + context: { store }, + }); + + beforeAll(async () => { + await waitForComponentToPaint(wrapper); + }); + + it('renders', () => { + expect(wrapper.find(QueryList)).toExist(); + }); + + it('renders a ListView', () => { + expect(wrapper.find(ListView)).toExist(); + }); + + it('fetches data', () => { + wrapper.update(); + const callsD = fetchMock.calls(/query\/\?q/); + expect(callsD).toHaveLength(1); + expect(callsD[0][0]).toMatchInlineSnapshot( + `"http://localhost/api/v1/query/?q=(order_column:changed_on,order_direction:desc,page:0,page_size:25)"`, + ); + }); + + it('renders a SyntaxHighlight', () => { + expect(wrapper.find(SyntaxHighlighter)).toExist(); + }); +}); diff --git a/superset-frontend/src/views/CRUD/data/query/QueryList.tsx b/superset-frontend/src/views/CRUD/data/query/QueryList.tsx new file mode 100644 index 000000000..6e8807e59 --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/query/QueryList.tsx @@ -0,0 +1,349 @@ +/** + * 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 React, { useMemo } from 'react'; +import { t, styled } from '@superset-ui/core'; +import moment from 'moment'; + +import withToasts from 'src/messageToasts/enhancers/withToasts'; +import { useListViewResource } from 'src/views/CRUD/hooks'; +import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu'; +import { Popover } from 'src/common/components'; +import { commonMenuData } from 'src/views/CRUD/data/common'; +import ListView, { Filters, ListViewProps } from 'src/components/ListView'; +import Icon, { IconName } from 'src/components/Icon'; +import Tooltip from 'src/common/components/Tooltip'; +import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; +import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; +import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; +import { DATETIME_WITH_TIME_ZONE, TIME_WITH_MS } from 'src/constants'; + +SyntaxHighlighter.registerLanguage('sql', sql); + +const TopAlignedListView = styled(ListView)>` + table .table-cell { + vertical-align: top; + } +`; + +const StyledSyntaxHighlighter = styled(SyntaxHighlighter)` + height: ${({ theme }) => theme.gridUnit * 26}px; + overflow-x: hidden !important; /* needed to override inline styles */ + text-overflow: ellipsis; + white-space: nowrap; +`; +const PAGE_SIZE = 25; +const SQL_PREVIEW_MAX_LINES = 4; +function shortenSQL(sql: string) { + let lines: string[] = sql.split('\n'); + if (lines.length >= SQL_PREVIEW_MAX_LINES) { + lines = lines.slice(0, SQL_PREVIEW_MAX_LINES); + lines.push('...'); + } + return lines.join('\n'); +} +interface QueryListProps { + addDangerToast: (msg: string, config?: any) => any; + addSuccessToast: (msg: string, config?: any) => any; +} + +export interface QueryObject { + id: number; + changed_on: string; + database: { + database_name: string; + }; + schema: string; + sql: string; + sql_tables?: { catalog?: string; schema: string; table: string }[]; + status: + | 'success' + | 'failed' + | 'stopped' + | 'running' + | 'timed_out' + | 'scheduled' + | 'pending'; + tab_name: string; + user: { + first_name: string; + id: number; + last_name: string; + username: string; + }; + start_time: number; + end_time: number; + rows: number; + tmp_table_name: string; + tracking_url: string; +} + +const StyledTableLabel = styled.div` + .count { + margin-left: 5px; + color: ${({ theme }) => theme.colors.primary.base}; + text-decoration: underline; + cursor: pointer; + } +`; + +const StyledPopoverItem = styled.div` + color: ${({ theme }) => theme.colors.grayscale.dark2}; +`; + +const StatusIcon = styled(Icon)<{ status: string }>` + color: ${({ status, theme }) => { + if (status === 'success') return theme.colors.success.base; + if (status === 'failed') return theme.colors.error.base; + if (status === 'running') return theme.colors.primary.base; + if (status === 'offline') return theme.colors.grayscale.light1; + + return theme.colors.grayscale.base; + }}; +`; + +function QueryList({ addDangerToast, addSuccessToast }: QueryListProps) { + const { + state: { loading, resourceCount: queryCount, resourceCollection: queries }, + fetchData, + } = useListViewResource( + 'query', + t('Query History'), + addDangerToast, + false, + ); + + const menuData: SubMenuProps = { + activeChild: 'Query History', + ...commonMenuData, + }; + + const initialSort = [{ id: 'changed_on', desc: true }]; + const columns = useMemo( + () => [ + { + Cell: ({ + row: { + original: { status }, + }, + }: any) => { + const statusConfig = { + name: '', + label: '', + status: '', + }; + if (status === 'success') { + statusConfig.name = 'check'; + statusConfig.label = t('Success'); + statusConfig.status = 'success'; + } + if (status === 'failed' || status === 'stopped') { + statusConfig.name = 'x-small'; + statusConfig.label = t('Failed'); + statusConfig.status = 'failed'; + } + if (status === 'running') { + statusConfig.name = 'running'; + statusConfig.label = t('Running'); + statusConfig.status = 'running'; + } + if (status === 'timed_out') { + statusConfig.name = 'offline'; + statusConfig.label = t('Offline'); + statusConfig.status = 'offline'; + } + if (status === 'scheduled' || status === 'pending') { + statusConfig.name = 'queued'; + statusConfig.label = t('Scheduled'); + statusConfig.status = 'queued'; + } + return ( + + + + ); + }, + accessor: 'status', + size: 'xs', + disableSortBy: true, + }, + { + accessor: 'start_time', + Header: t('Time'), + size: 'lg', + Cell: ({ + row: { + original: { start_time, end_time }, + }, + }: any) => { + const startMoment = moment.utc(start_time).local(); + const formattedStartTimeData = startMoment + .format(DATETIME_WITH_TIME_ZONE) + .split(' '); + + const formattedStartTime = ( + <> + {formattedStartTimeData[0]}
+ {formattedStartTimeData[1]} + + ); + + return end_time ? ( + + {formattedStartTime} + + ) : ( + formattedStartTime + ); + }, + }, + { + accessor: 'tab_name', + Header: t('Tab Name'), + size: 'lg', + }, + { + accessor: 'database.database_name', + Header: t('Database'), + size: 'lg', + }, + { + accessor: 'schema', + Header: t('Schema'), + size: 'lg', + }, + { + 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 ( + + {main} + + {names.map((name: string) => ( + {name} + ))} + + } + > + (+{names.length}) + + + ); + } + + return main; + }, + accessor: 'sql_tables', + Header: t('Tables'), + size: 'lg', + disableSortBy: true, + }, + { + accessor: 'user.first_name', + Header: t('User'), + size: 'lg', + Cell: ({ + row: { + original: { user }, + }, + }: any) => `${user.first_name} ${user.last_name}`, + }, + { + accessor: 'rows', + Header: t('Rows'), + size: 'md', + }, + { + accessor: 'sql', + Header: t('SQL'), + Cell: ({ + row: { + original: { sql }, + }, + }: any) => { + return ( + + {shortenSQL(sql)} + + ); + }, + }, + { + Header: t('Actions'), + id: 'actions', + disableSortBy: true, + Cell: ({ + row: { + original: { id }, + }, + }: any) => { + return ( + + + + + + ); + }, + }, + ], + [], + ); + + const filters: Filters = useMemo(() => [], []); + + return ( + <> + + + + ); +} + +export default withToasts(QueryList); diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx index 88ed0154a..96f0f5f9e 100644 --- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx +++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx @@ -79,7 +79,7 @@ function SavedQueryList({ refreshData, } = useListViewResource( 'saved_query', - t('saved_queries'), + t('Saved Queries'), addDangerToast, ); const [ diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts index b6963d403..48daa7670 100644 --- a/superset-frontend/src/views/CRUD/hooks.ts +++ b/superset-frontend/src/views/CRUD/hooks.ts @@ -59,11 +59,11 @@ export function useListViewResource( } useEffect(() => { - const infoParam = infoEnable - ? `_info?q=${rison.encode({ keys: ['permissions'] })}` - : ''; + if (!infoEnable) return; SupersetClient.get({ - endpoint: `/api/v1/${resource}/${infoParam}`, + endpoint: `/api/v1/${resource}/_info?q=${rison.encode({ + keys: ['permissions'], + })}`, }).then( ({ json: infoJson = {} }) => { updateState({ @@ -73,7 +73,7 @@ export function useListViewResource( createErrorHandler(errMsg => handleErrorMsg( t( - 'An error occurred while fetching %ss info: %s', + 'An error occurred while fetching %s info: %s', resourceLabel, errMsg, ), diff --git a/superset/config.py b/superset/config.py index d54e39ee7..04cb70fd1 100644 --- a/superset/config.py +++ b/superset/config.py @@ -334,6 +334,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = { "ROW_LEVEL_SECURITY": False, # Enables Alerts and reports new implementation "ALERT_REPORTS": False, + "SIP_34_QUERY_SEARCH_UI": False, } # Set the default view to card/grid view if thumbnail support is enabled. diff --git a/superset/queries/api.py b/superset/queries/api.py index 710c52bb9..e5feaa9bb 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -38,6 +38,7 @@ class QueryRestApi(BaseSupersetModelRestApi): class_permission_name = "QueryView" list_columns = [ + "id", "changed_on", "database.database_name", "rows", @@ -52,7 +53,6 @@ class QueryRestApi(BaseSupersetModelRestApi): "user.username", "start_time", "end_time", - "rows", "tmp_table_name", "tracking_url", ] @@ -93,6 +93,7 @@ class QueryRestApi(BaseSupersetModelRestApi): "database.database_name", "rows", "schema", + "start_time", "sql", "tab_name", "user.first_name", diff --git a/superset/views/base.py b/superset/views/base.py index a5ead2c69..a3bcafa77 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -254,6 +254,19 @@ class BaseSupersetView(BaseView): mimetype="application/json", ) + def render_app_template(self) -> FlaskResponse: + payload = { + "user": bootstrap_user_data(g.user), + "common": common_bootstrap_payload(), + } + return self.render_template( + "superset/crud_views.html", + entry="crudViews", + bootstrap_data=json.dumps( + payload, default=utils.pessimistic_json_iso_dttm_ser + ), + ) + def menu_data() -> Dict[str, Any]: menu = appbuilder.menu.get_data() diff --git a/superset/views/core.py b/superset/views/core.py index 9ff28fc1e..0ac0fb19e 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -2738,6 +2738,17 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods "superset/basic.html", entry="sqllab", bootstrap_data=bootstrap_data ) + @has_access + @expose("/sqllab/history/", methods=["GET"]) + def sqllab_search(self) -> FlaskResponse: + if not ( + is_feature_enabled("ENABLE_REACT_CRUD_VIEWS") + and is_feature_enabled("SIP_34_QUERY_SEARCH_UI") + ): + return redirect("/superset/sqllab#search", code=307) + + return super().render_app_template() + @api @has_access_api @expose("/schemas_access_for_csv_upload") diff --git a/tests/queries/api_tests.py b/tests/queries/api_tests.py index 744aae1ae..4aed9ecbd 100644 --- a/tests/queries/api_tests.py +++ b/tests/queries/api_tests.py @@ -257,6 +257,7 @@ class TestQueryApi(SupersetTestCase): "changed_on", "database", "end_time", + "id", "rows", "schema", "sql",