feat: Query History CRUD list view (#11574)

This commit is contained in:
ʈᵃᵢ 2020-11-12 11:55:13 -10:00 committed by GitHub
parent 12cb27f5cb
commit 432e5ab460
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 683 additions and 87 deletions

View File

@ -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.
-->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="4" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@ -0,0 +1,23 @@
<!--
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.
-->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="8" width="8" height="2" rx="1" transform="rotate(90 10 8)" fill="#B2B2B2"/>
<rect x="13" y="8" width="8" height="2" rx="1" transform="rotate(90 13 8)" fill="#666666"/>
<rect x="16" y="8" width="8" height="2" rx="1" transform="rotate(90 16 8)" fill="#323232"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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.
-->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 8C11.7341 8 11.4734 8.02608 11.2204 8.07611L11.3175 8.56661L11.5115 9.5476L11.6086 10.0381C11.7344 10.0132 11.8651 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C11.8651 14 11.7344 13.9868 11.6086 13.9619L11.5115 14.4524L11.3175 15.4334L11.2204 15.9239C11.4734 15.9739 11.7341 16 12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8ZM9.77683 8.67434C9.34106 8.96608 8.96608 9.34106 8.67434 9.77683L9.08982 10.055L9.92079 10.6113L10.3363 10.8895C10.4826 10.6709 10.6709 10.4826 10.8895 10.3363L10.6113 9.92079L10.055 9.08982L9.77683 8.67434ZM8.07611 11.2204C8.02608 11.4734 8 11.7341 8 12C8 12.2659 8.02608 12.5266 8.07611 12.7796L8.56661 12.6825L9.5476 12.4885L10.0381 12.3914C10.0132 12.2656 10 12.1349 10 12C10 11.8651 10.0132 11.7344 10.0381 11.6086L9.5476 11.5115L8.56661 11.3175L8.07611 11.2204ZM8.67434 14.2232C8.96608 14.6589 9.34106 15.0339 9.77683 15.3257L10.055 14.9102L10.6113 14.0792L10.8895 13.6637C10.6709 13.5174 10.4826 13.3291 10.3363 13.1105L9.92079 13.3887L9.08982 13.945L8.67434 14.2232Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -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="
}
}

View File

@ -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)"`,
);

View File

@ -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 = (
<QuerySearch
actions={this.props.actions}

View File

@ -18,30 +18,30 @@
*/
import React, { SVGProps } from 'react';
import { ReactComponent as AlertSolidIcon } from 'images/icons/alert_solid.svg';
import { ReactComponent as AlertIcon } from 'images/icons/alert.svg';
import { ReactComponent as AlertSolidIcon } from 'images/icons/alert_solid.svg';
import { ReactComponent as BinocularsIcon } from 'images/icons/binoculars.svg';
import { ReactComponent as BoltSmallRunIcon } from 'images/icons/bolt_small_run.svg';
import { ReactComponent as BoltSmallIcon } from 'images/icons/bolt_small.svg';
import { ReactComponent as BoltIcon } from 'images/icons/bolt.svg';
import { ReactComponent as BoltSmallIcon } from 'images/icons/bolt_small.svg';
import { ReactComponent as BoltSmallRunIcon } from 'images/icons/bolt_small_run.svg';
import { ReactComponent as CalendarIcon } from 'images/icons/calendar.svg';
import { ReactComponent as CancelIcon } from 'images/icons/cancel.svg';
import { ReactComponent as CancelSolidIcon } from 'images/icons/cancel_solid.svg';
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
import { ReactComponent as CancelIcon } from 'images/icons/cancel.svg';
import { ReactComponent as CardViewIcon } from 'images/icons/card_view.svg';
import { ReactComponent as CardsLockedIcon } from 'images/icons/cards_locked.svg';
import { ReactComponent as CardsIcon } from 'images/icons/cards.svg';
import { ReactComponent as CardsLockedIcon } from 'images/icons/cards_locked.svg';
import { ReactComponent as CardViewIcon } from 'images/icons/card_view.svg';
import { ReactComponent as CaretDownIcon } from 'images/icons/caret_down.svg';
import { ReactComponent as CaretLeftIcon } from 'images/icons/caret_left.svg';
import { ReactComponent as CaretRightIcon } from 'images/icons/caret_right.svg';
import { ReactComponent as CaretUpIcon } from 'images/icons/caret_up.svg';
import { ReactComponent as CertifiedIcon } from 'images/icons/certified.svg';
import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg';
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg';
import { ReactComponent as CircleCheckSolidIcon } from 'images/icons/circle_check_solid.svg';
import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
import { ReactComponent as CircleCheckIcon } from 'images/icons/circle_check.svg';
import { ReactComponent as CircleCheckSolidIcon } from 'images/icons/circle_check_solid.svg';
import { ReactComponent as CircleIcon } from 'images/icons/circle.svg';
import { ReactComponent as ClockIcon } from 'images/icons/clock.svg';
import { ReactComponent as CloseIcon } from 'images/icons/close.svg';
@ -60,14 +60,14 @@ import { ReactComponent as DownloadIcon } from 'images/icons/download.svg';
import { ReactComponent as EditAltIcon } from 'images/icons/edit_alt.svg';
import { ReactComponent as EditIcon } from 'images/icons/edit.svg';
import { ReactComponent as EmailIcon } from 'images/icons/email.svg';
import { ReactComponent as ErrorSolidSmallIcon } from 'images/icons/error_solid_small.svg';
import { ReactComponent as ErrorSolidIcon } from 'images/icons/error_solid.svg';
import { ReactComponent as ErrorIcon } from 'images/icons/error.svg';
import { ReactComponent as ErrorSolidIcon } from 'images/icons/error_solid.svg';
import { ReactComponent as ErrorSolidSmallIcon } from 'images/icons/error_solid_small.svg';
import { ReactComponent as ExpandIcon } from 'images/icons/expand.svg';
import { ReactComponent as EyeSlashIcon } from 'images/icons/eye_slash.svg';
import { ReactComponent as EyeIcon } from 'images/icons/eye.svg';
import { ReactComponent as FavoriteSmallSelectedIcon } from 'images/icons/favorite_small_selected.svg';
import { ReactComponent as EyeSlashIcon } from 'images/icons/eye_slash.svg';
import { ReactComponent as FavoriteSelectedIcon } from 'images/icons/favorite-selected.svg';
import { ReactComponent as FavoriteSmallSelectedIcon } from 'images/icons/favorite_small_selected.svg';
import { ReactComponent as FavoriteUnselectedIcon } from 'images/icons/favorite-unselected.svg';
import { ReactComponent as FieldABCIcon } from 'images/icons/field_abc.svg';
import { ReactComponent as FieldBooleanIcon } from 'images/icons/field_boolean.svg';
@ -84,22 +84,22 @@ import { ReactComponent as GearIcon } from 'images/icons/gear.svg';
import { ReactComponent as GridIcon } from 'images/icons/grid.svg';
import { ReactComponent as ImageIcon } from 'images/icons/image.svg';
import { ReactComponent as ImportIcon } from 'images/icons/import.svg';
import { ReactComponent as InfoSolidSmallIcon } from 'images/icons/info_solid_small.svg';
import { ReactComponent as InfoSolidIcon } from 'images/icons/info-solid.svg';
import { ReactComponent as InfoIcon } from 'images/icons/info.svg';
import { ReactComponent as InfoSolidIcon } from 'images/icons/info-solid.svg';
import { ReactComponent as InfoSolidSmallIcon } from 'images/icons/info_solid_small.svg';
import { ReactComponent as JoinIcon } from 'images/icons/join.svg';
import { ReactComponent as KeyboardIcon } from 'images/icons/keyboard.svg';
import { ReactComponent as LayersIcon } from 'images/icons/layers.svg';
import { ReactComponent as LightbulbIcon } from 'images/icons/lightbulb.svg';
import { ReactComponent as ListViewIcon } from 'images/icons/list_view.svg';
import { ReactComponent as ListIcon } from 'images/icons/list.svg';
import { ReactComponent as ListViewIcon } from 'images/icons/list_view.svg';
import { ReactComponent as LocationIcon } from 'images/icons/location.svg';
import { ReactComponent as LockLockedIcon } from 'images/icons/lock_locked.svg';
import { ReactComponent as LockUnlockedIcon } from 'images/icons/lock_unlocked.svg';
import { ReactComponent as MapIcon } from 'images/icons/map.svg';
import { ReactComponent as MessageIcon } from 'images/icons/message.svg';
import { ReactComponent as MinusSolidIcon } from 'images/icons/minus_solid.svg';
import { ReactComponent as MinusIcon } from 'images/icons/minus.svg';
import { ReactComponent as MinusSolidIcon } from 'images/icons/minus_solid.svg';
import { ReactComponent as MoreHorizIcon } from 'images/icons/more_horiz.svg';
import { ReactComponent as MoveIcon } from 'images/icons/move.svg';
import { ReactComponent as NavChartsIcon } from 'images/icons/nav_charts.svg';
@ -109,13 +109,16 @@ import { ReactComponent as NavExploreIcon } from 'images/icons/nav_explore.svg';
import { ReactComponent as NavHomeIcon } from 'images/icons/nav_home.svg';
import { ReactComponent as NavLabIcon } from 'images/icons/nav_lab.svg';
import { ReactComponent as NoteIcon } from 'images/icons/note.svg';
import { ReactComponent as OfflineIcon } from 'images/icons/offline.svg';
import { ReactComponent as PaperclipIcon } from 'images/icons/paperclip.svg';
import { ReactComponent as PlaceholderIcon } from 'images/icons/placeholder.svg';
import { ReactComponent as PlusIcon } from 'images/icons/plus.svg';
import { ReactComponent as PlusLargeIcon } from 'images/icons/plus_large.svg';
import { ReactComponent as PlusSmallIcon } from 'images/icons/plus_small.svg';
import { ReactComponent as PlusSolidIcon } from 'images/icons/plus_solid.svg';
import { ReactComponent as PlusIcon } from 'images/icons/plus.svg';
import { ReactComponent as QueuedIcon } from 'images/icons/queued.svg';
import { ReactComponent as RefreshIcon } from 'images/icons/refresh.svg';
import { ReactComponent as RunningIcon } from 'images/icons/running.svg';
import { ReactComponent as SearchIcon } from 'images/icons/search.svg';
import { ReactComponent as ServerIcon } from 'images/icons/server.svg';
import { ReactComponent as ShareIcon } from 'images/icons/share.svg';
@ -131,8 +134,8 @@ import { ReactComponent as TriangleDownIcon } from 'images/icons/triangle_down.s
import { ReactComponent as TriangleUpIcon } from 'images/icons/triangle_up.svg';
import { ReactComponent as UpLevelIcon } from 'images/icons/up-level.svg';
import { ReactComponent as UserIcon } from 'images/icons/user.svg';
import { ReactComponent as WarningSolidIcon } from 'images/icons/warning_solid.svg';
import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
import { ReactComponent as WarningSolidIcon } from 'images/icons/warning_solid.svg';
import { ReactComponent as XLargeIcon } from 'images/icons/x-large.svg';
import { ReactComponent as XSmallIcon } from 'images/icons/x-small.svg';
@ -185,8 +188,8 @@ export type IconName =
| 'expand'
| 'eye-slash'
| 'eye'
| 'favorite-small-selected'
| 'favorite-selected'
| 'favorite-small-selected'
| 'favorite-unselected'
| 'field-abc'
| 'field-boolean'
@ -195,8 +198,8 @@ export type IconName =
| 'field-num'
| 'field-struct'
| 'file'
| 'filter'
| 'filter-small'
| 'filter'
| 'folder'
| 'full'
| 'gear'
@ -228,13 +231,16 @@ export type IconName =
| 'nav-home'
| 'nav-lab'
| 'note'
| 'offline'
| 'paperclip'
| 'placeholder'
| 'plus-large'
| 'plus-small'
| 'plus-solid'
| 'plus'
| 'queued'
| 'refresh'
| 'running'
| 'search'
| 'server'
| 'share'
@ -260,55 +266,32 @@ export const iconsRegistry: Record<
React.ComponentType<SVGProps<SVGSVGElement>>
> = {
'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<SVGSVGElement> {

View File

@ -61,6 +61,10 @@ const ListViewStyles = styled.div`
margin-bottom: 0;
}
.body {
overflow-x: auto;
}
.ant-empty {
.ant-empty-image {
height: auto;

View File

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

View File

@ -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 = {

View File

@ -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 = () => (
<AnnotationList user={user} />
</ErrorBoundary>
</Route>
<Route path="/superset/sqllab/history/">
<ErrorBoundary>
<QueryList user={user} />
</ErrorBoundary>
</Route>
</Switch>
<ToastPresenter />
</QueryParamProvider>

View File

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

View File

@ -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(<QueryList {...mockedProps} />, {
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();
});
});

View File

@ -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)<ListViewProps<QueryObject>>`
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<QueryObject>(
'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 (
<Tooltip title={statusConfig.label} placement="bottom">
<StatusIcon
name={statusConfig.name as IconName}
status={statusConfig.status}
/>
</Tooltip>
);
},
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]} <br />
{formattedStartTimeData[1]}
</>
);
return end_time ? (
<Tooltip
title={t(
'Duration: %s',
moment(moment.utc(end_time - start_time)).format(TIME_WITH_MS),
)}
placement="bottom"
>
<span>{formattedStartTime}</span>
</Tooltip>
) : (
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 (
<StyledTableLabel>
<span>{main}</span>
<Popover
placement="right"
title={t('TABLES')}
trigger="click"
content={
<>
{names.map((name: string) => (
<StyledPopoverItem>{name}</StyledPopoverItem>
))}
</>
}
>
<span className="count">(+{names.length})</span>
</Popover>
</StyledTableLabel>
);
}
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 (
<StyledSyntaxHighlighter language="sql" style={github}>
{shortenSQL(sql)}
</StyledSyntaxHighlighter>
);
},
},
{
Header: t('Actions'),
id: 'actions',
disableSortBy: true,
Cell: ({
row: {
original: { id },
},
}: any) => {
return (
<Tooltip title={t('Open query in SQL Lab')} placement="bottom">
<a href={`/superset/sqllab?queryId=${id}`}>
<Icon name="full" />
</a>
</Tooltip>
);
},
},
],
[],
);
const filters: Filters = useMemo(() => [], []);
return (
<>
<SubMenu {...menuData} />
<TopAlignedListView
className="query-history-list-view"
columns={columns}
count={queryCount}
data={queries}
fetchData={fetchData}
filters={filters}
initialSort={initialSort}
loading={loading}
pageSize={PAGE_SIZE}
/>
</>
);
}
export default withToasts(QueryList);

View File

@ -79,7 +79,7 @@ function SavedQueryList({
refreshData,
} = useListViewResource<SavedQueryObject>(
'saved_query',
t('saved_queries'),
t('Saved Queries'),
addDangerToast,
);
const [

View File

@ -59,11 +59,11 @@ export function useListViewResource<D extends object = any>(
}
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<D extends object = any>(
createErrorHandler(errMsg =>
handleErrorMsg(
t(
'An error occurred while fetching %ss info: %s',
'An error occurred while fetching %s info: %s',
resourceLabel,
errMsg,
),

View File

@ -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.

View File

@ -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",

View File

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

View File

@ -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")

View File

@ -257,6 +257,7 @@ class TestQueryApi(SupersetTestCase):
"changed_on",
"database",
"end_time",
"id",
"rows",
"schema",
"sql",