From 592337e6539226b32ea194fdefd68a22962d5832 Mon Sep 17 00:00:00 2001 From: cccs-Dustin <96579982+cccs-Dustin@users.noreply.github.com> Date: Wed, 15 Mar 2023 14:08:33 -0400 Subject: [PATCH] Feature/cldn 1968 (Display JSON data inline) (#268) * Add buttons to expand and minimize JSON data as well as ability to expand and/or collapse all rows in a certain column * [CLDN-1968] Added expand button for full row * [CLDN-1968] Resize JSON columns * [CLDN-1968] Added new array which tracks JSON cell state * Revert "[CLDN-1968] Added new array which tracks JSON cell state" This reverts commit dabc3daee96cfbe39d9615837076e54736e518b6. * [CLDN-1968] Added ability for row level expand all button to track if cells are expanded or not * [CLDN-1968] Ran pre-commit hook * [CLDN-1968] Improved UI * [CLDN-1968] Update image tag for testing * [CLDN-1968] Revert image tag for testing * [CLDN-1968] Added multiple UI/UX changes based on QA feedback * [CLDN-1968] Added more UI/UX changes based on QA feedback * [CLDN-1968] Temp change to image * Revert "[CLDN-1968] Temp change to image" This reverts commit 57490bd71dbf14499f08e21e691933d6bbaf914d. * Update superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/CccsGrid.tsx Co-authored-by: cccs-rc <62034438+cccs-rc@users.noreply.github.com> * [CLDN-1968] Remove 'TODO's as they are no longer needed * [CLDN-1968] Changed a variable name, and condensed a few lines * [CLDN-1968] Modified a setState so that only one is needed instead of 2 --------- Co-authored-by: cccs-rc <62034438+cccs-rc@users.noreply.github.com> --- .../plugin-chart-cccs-grid/src/Buttons.css | 31 ++ .../plugin-chart-cccs-grid/src/CccsGrid.tsx | 312 +++++++++----- .../src/ExpandAllValueRenderer.tsx | 118 ++++++ .../src/JsonValueRenderer.tsx | 131 ++++-- .../JumpActionConfig.tsx | 389 +++++++++--------- .../JumpActionConfigControll/index.tsx | 296 ++++++------- .../src/plugin/controlPanel.tsx | 55 ++- .../src/plugin/transformProps.ts | 33 +- .../plugin-chart-cccs-grid/src/types.ts | 2 +- .../src/IFrameVisualization.tsx | 32 +- .../src/plugin/buildQuery.ts | 69 ++-- .../src/plugin/controlPanel.ts | 25 +- .../src/plugin/transformProps.ts | 25 +- tests/unit_tests/explore/utils_test.py | 1 + 14 files changed, 934 insertions(+), 585 deletions(-) create mode 100644 superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/Buttons.css create mode 100644 superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/ExpandAllValueRenderer.tsx diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/Buttons.css b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/Buttons.css new file mode 100644 index 000000000000..3d4c353a0cd3 --- /dev/null +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/Buttons.css @@ -0,0 +1,31 @@ +.Button { + background-color: transparent; + border: solid rgb(137, 137, 137); + border-width: 0 2px 2px 0; + display: inline-block; + padding-right: 3px; + padding-left: 3px; + padding-top: 3px; + padding-bottom: 3px; + vertical-align: middle; + margin-right: 10px; + margin-left: 5px; +} + +.Expand { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); +} + +.Collapse { + transform: rotate(45deg); + -webkit-transform: rotate(45deg); +} + +.Row-Expand { + background-color: transparent; + text-decoration: underline; + vertical-align: middle; + border: none; + color: rgb(31, 167, 201); +} diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/CccsGrid.tsx b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/CccsGrid.tsx index 67167ad1860f..29ebf2ef1499 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/CccsGrid.tsx +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/CccsGrid.tsx @@ -42,6 +42,7 @@ import Ipv4ValueRenderer from './Ipv4ValueRenderer'; import Ipv6ValueRenderer from './Ipv6ValueRenderer'; import DomainValueRenderer from './DomainValueRenderer'; import JsonValueRenderer from './JsonValueRenderer'; +import ExpandAllValueRenderer from './ExpandAllValueRenderer'; import CustomTooltip from './CustomTooltip'; /// / jcc @@ -90,9 +91,11 @@ export default function CccsGrid({ const crossFilterValue = useSelector( state => state.dataMask[formData.slice_id]?.filterState?.value, ); - - const [selectedDataByColumnName, setSelectedDataColumnName] = useState<{[key: string]: string[] }>(initialFilters); - const [selectedDataByAdvancedType, setselectedDataByAdvancedType] = useState<{[key: string]: string[]}>(initialFilters); + + const [selectedDataByColumnName, setSelectedDataColumnName] = + useState<{ [key: string]: string[] }>(initialFilters); + const [selectedDataByAdvancedType, setselectedDataByAdvancedType] = + useState<{ [key: string]: string[] }>(initialFilters); const [principalColumnFilters, setPrincipalColumnFilters] = useState({}); const [searchValue, setSearchValue] = useState(''); @@ -134,120 +137,125 @@ export default function CccsGrid({ [emitFilter, setDataMask, selectedDataByColumnName, principalColumnFilters], ); // only take relevant page size options - const generateNativeFilterUrlString = (nativefilterID: string, urlSelectedData: any[], column: string = "" ) =>{ - const stringSelectedData = urlSelectedData.map( e => { - return `${e.toString()}` - }) + const generateNativeFilterUrlString = ( + nativefilterID: string, + urlSelectedData: any[], + column: string = '', + ) => { + const stringSelectedData = urlSelectedData.map(e => { + return `${e.toString()}`; + }); const navtiveFilter = { extraFormData: { - filters: [ - {col: column, - op: "IN", - val: stringSelectedData} - ] + filters: [{ col: column, op: 'IN', val: stringSelectedData }], }, filterState: { label: stringSelectedData, validateStatus: false, - value: stringSelectedData + value: stringSelectedData, }, id: nativefilterID, - ownState: {} - } - return navtiveFilter - } - const getJumpToDashboardContextMenuItems = (selectedData: {[key: string]: string[] }, disableOveride: boolean): (string | MenuItemDef)[] => { - - let sub_menu: any = [] + ownState: {}, + }; + return navtiveFilter; + }; + const getJumpToDashboardContextMenuItems = ( + selectedData: { [key: string]: string[] }, + disableOveride: boolean, + ): (string | MenuItemDef)[] => { + let sub_menu: any = []; for (let key in jumpActionConfigs) { - let advancedDataTypeNativeFilters = jumpActionConfigs[key]; let nativeFilterUrls: any = {}; - let jumpActionName: string = "" + let jumpActionName: string = ''; advancedDataTypeNativeFilters.forEach((element: any) => { - jumpActionName = element.name + jumpActionName = element.name; let advancedDataType = element['advancedDataType']; - let nativefilters: any[] = element["nativefilters"]; + let nativefilters: any[] = element['nativefilters']; let selectedDataForUrl = selectedData[advancedDataType]; - + if (selectedDataForUrl && nativefilters) { - nativefilters.forEach( filter => { - nativeFilterUrls[filter["value"]] = generateNativeFilterUrlString(filter["value"], selectedDataForUrl, filter["column"]) + nativefilters.forEach(filter => { + nativeFilterUrls[filter['value']] = generateNativeFilterUrlString( + filter['value'], + selectedDataForUrl, + filter['column'], + ); }); } - }); - - if (Object.keys(nativeFilterUrls).length !== 0){ - + + if (Object.keys(nativeFilterUrls).length !== 0) { let action = () => { - let baseUrl = location.protocol + '//' + location.host; - let url = `${baseUrl}/superset/dashboard/${key}/?native_filters=${rison.encode(nativeFilterUrls)}` + let baseUrl = location.protocol + '//' + location.host; + let url = `${baseUrl}/superset/dashboard/${key}/?native_filters=${rison.encode( + nativeFilterUrls, + )}`; window.open(url, '_blank'); - } - + }; + let DashboardMenuItem = { name: jumpActionName, - action - } - - sub_menu.push(DashboardMenuItem) + action, + }; + + sub_menu.push(DashboardMenuItem); } - } - - const menu = {name: "Jump to dashboard", subMenu: sub_menu, disabled: disableOveride || sub_menu.length < 1, icon: '' } - return [ menu ] - } + + const menu = { + name: 'Jump to dashboard', + subMenu: sub_menu, + disabled: disableOveride || sub_menu.length < 1, + icon: '', + }; + return [menu]; + }; const getContextMenuItems = useCallback( (params: GetContextMenuItemsParams): (string | MenuItemDef)[] => { - let result: (string | MenuItemDef)[] = []; - result = ['copy', 'copyWithHeaders', 'paste',]; - - if(emitFilter) { - result = result.concat( - [ - 'separator', - { - name: 'Emit Filter(s)', - disabled: params.value === null, - action: () => handleChange(selectedDataByColumnName), - // eslint-disable-next-line theme-colors/no-literal-colors - icon: '', - }, - { - name: 'Emit Principal Column Filter(s)', - disabled: - ensureIsArray(principalColumns).length === 0 || - Object.keys(principalColumnFilters).some(column => - principalColumnFilters[column].some((val: any) => val === null), - ) || - params.node === null, - action: () => handleChange(principalColumnFilters), - // eslint-disable-next-line theme-colors/no-literal-colors - icon: '', - }, - { - name: 'Clear Emitted Filter(s)', - disabled: crossFilterValue === undefined, - action: () => dispatch(clearDataMask(formData.slice_id)), - icon: '', - }, - ] - ) - } - result = result.concat( - getJumpToDashboardContextMenuItems(selectedDataByAdvancedType, (params.value === null)) - ) - result = result.concat( - [ + result = ['copy', 'copyWithHeaders', 'paste']; + + if (emitFilter) { + result = result.concat([ 'separator', - 'export' - ] - ) - + { + name: 'Emit Filter(s)', + disabled: + params.value === null || 'undefined' in selectedDataByColumnName, + action: () => handleChange(selectedDataByColumnName), + // eslint-disable-next-line theme-colors/no-literal-colors + icon: '', + }, + { + name: 'Emit Principal Column Filter(s)', + disabled: + ensureIsArray(principalColumns).length === 0 || + Object.keys(principalColumnFilters).some(column => + principalColumnFilters[column].some((val: any) => val === null), + ) || + params.node === null, + action: () => handleChange(principalColumnFilters), + // eslint-disable-next-line theme-colors/no-literal-colors + icon: '', + }, + { + name: 'Clear Emitted Filter(s)', + disabled: crossFilterValue === undefined, + action: () => dispatch(clearDataMask(formData.slice_id)), + icon: '', + }, + ]); + } + result = result.concat( + getJumpToDashboardContextMenuItems( + selectedDataByAdvancedType, + params.value === null, + ), + ); + result = result.concat(['separator', 'export']); + return result; }, [ @@ -262,12 +270,67 @@ export default function CccsGrid({ ], ); + const getMainMenuItems = useCallback(params => { + // If the column currently selected has either JSON data, or the + // expand all button for the row, add extra options + if ( + params.column.colDef.cellRenderer === 'jsonValueRenderer' || + params.column.colDef.cellRenderer === 'expandAllValueRenderer' + ) { + // Get the default menu items so we can add to them + const jsonMenuItems = params.defaultItems.slice(0); + + // Get all of the cell renderers and the current column ID + const instances = params.api.getCellRendererInstances(); + const columnID = params.column.colId; + + // Only keep cells which belong to the current column + const newInstances = instances.filter( + (instance: any) => instance.params.column.colId === columnID, + ); + + // Add an expand all button which will send an update to each cell renderer + jsonMenuItems.push({ + name: + params.column.colDef.cellRenderer === 'jsonValueRenderer' + ? 'Expand Column' + : 'Expand All', + action: () => { + newInstances.map((instance: any) => + instance.componentInstance.updateState(true), + ); + }, + }); + + // Add a collapse all button which will send an update to each cell renderer + jsonMenuItems.push({ + name: + params.column.colDef.cellRenderer === 'jsonValueRenderer' + ? 'Collapse Column' + : 'Collapse All', + action: () => { + newInstances.map((instance: any) => + instance.componentInstance.updateState(false), + ); + }, + }); + + // Return the default menu with added options (expand/collapse) + return jsonMenuItems; + } + + // If the column currently selected has neither JSON data, nor the + // expand all button for the row, use the regular menu + return params.defaultItems; + }, []); + const frameworkComponents = { countryValueRenderer: CountryValueRenderer, ipv4ValueRenderer: Ipv4ValueRenderer, ipv6ValueRenderer: Ipv6ValueRenderer, domainValueRenderer: DomainValueRenderer, jsonValueRenderer: JsonValueRenderer, + expandAllValueRenderer: ExpandAllValueRenderer, customTooltip: CustomTooltip, }; @@ -284,6 +347,21 @@ export default function CccsGrid({ } }; + const onFirstDataRendered = (params: any) => { + // Get all of the columns in the grid + const instances = params.columnApi.getAllColumns(); + + // Filter on columns that have JSON data + const newInstances = instances.filter( + (instance: any) => instance.colDef?.cellRenderer === 'jsonValueRenderer', + ); + + // Set the columns which have JSON data to be 350 pixels wide + newInstances.map((instance: any) => + params.columnApi.setColumnWidth(instance.colId, 350), + ); + }; + const onSelectionChanged = (params: any) => { const gridApi = params.api; const selectedRows = gridApi.getSelectedRows(); @@ -298,22 +376,24 @@ export default function CccsGrid({ const gridApi = params.api; const cellRanges = gridApi.getCellRanges(); - - const updatedSelectedData: {[key: string]: string[] } = {}; - const newSelectedbyAdvancedType: {[key: string]: string[] } = {}; - const updatedPrincipalColumnFilters = {}; + const updatedSelectedData: { [key: string]: string[] } = {}; + const newSelectedbyAdvancedType: { [key: string]: string[] } = {}; + const updatedPrincipalColumnFilters = {}; cellRanges.forEach((range: any) => { range.columns.forEach((column: any) => { const col = getEmitTarget(column.colDef?.field); - let advancedDataType = datasetColumns.find( (column) => { return column.column_name == col })?.advanced_data_type || "" - + let advancedDataType = + datasetColumns.find(column => { + return column.column_name == col; + })?.advanced_data_type || ''; updatedSelectedData[col] = updatedSelectedData[col] || []; - - newSelectedbyAdvancedType[advancedDataType] = newSelectedbyAdvancedType[advancedDataType] || [] - + + newSelectedbyAdvancedType[advancedDataType] = + newSelectedbyAdvancedType[advancedDataType] || []; + const startRow = Math.min( range.startRow.rowIndex, range.endRow.rowIndex, @@ -321,27 +401,35 @@ export default function CccsGrid({ const endRow = Math.max(range.startRow.rowIndex, range.endRow.rowIndex); for (let rowIndex = startRow; rowIndex <= endRow; rowIndex++) { + const rowNode = gridApi.getModel().getRow(rowIndex); + const value = gridApi.getValue(column, rowNode); + + const valueRendererName = column.colDef.cellRenderer; + let valueRendererObjt = null; + let renderedValue = null; - const rowNode = gridApi.getModel().getRow(rowIndex) - const value = gridApi.getValue( - column, - rowNode, - ); - - const valueRendererName = column.colDef.cellRenderer - let valueRendererObjt = null - let renderedValue = null - if (valueRendererName) { - const valueRenderer = valueRendererName ? frameworkComponents[valueRendererName] : undefined - valueRendererObjt = new valueRenderer({value, valueFormatted: null}) + const valueRenderer = valueRendererName + ? frameworkComponents[valueRendererName] + : undefined; + valueRendererObjt = new valueRenderer({ + value, + valueFormatted: null, + }); } - renderedValue = valueRendererObjt ? valueRendererObjt.render() : value + renderedValue = valueRendererObjt + ? valueRendererObjt.render() + : value; if (!updatedSelectedData[col].includes(value)) { updatedSelectedData[col].push(value); } - if (!newSelectedbyAdvancedType[advancedDataType].includes(renderedValue) && renderedValue) { + if ( + !newSelectedbyAdvancedType[advancedDataType].includes( + renderedValue, + ) && + renderedValue + ) { newSelectedbyAdvancedType[advancedDataType].push(renderedValue); } } @@ -479,7 +567,9 @@ export default function CccsGrid({ gridOptions={gridOptions} onGridColumnsChanged={autoSizeFirst100Columns} getContextMenuItems={getContextMenuItems} + getMainMenuItems={getMainMenuItems} onGridReady={onGridReady} + onFirstDataRendered={onFirstDataRendered} onRangeSelectionChanged={onRangeSelectionChanged} onSelectionChanged={onSelectionChanged} rowData={rowData} diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/ExpandAllValueRenderer.tsx b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/ExpandAllValueRenderer.tsx new file mode 100644 index 000000000000..faf7265ea24f --- /dev/null +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/ExpandAllValueRenderer.tsx @@ -0,0 +1,118 @@ +import { GroupCellRenderer } from '@ag-grid-enterprise/all-modules'; +import React, { Component } from 'react'; +import './Buttons.css'; + +// Show a button to collapse all of the JSON blobs in the row +function collapseJSON(this: any, reverseState: any) { + return ( + + ); +} + +// Show a button to expand all of the JSON blobs in the row +function expandJSON(this: any, reverseState: any) { + return ( + <> + + + ); +} + +export default class ExpandAllValueRenderer extends Component< + {}, + { api: any; expanded: boolean; rowIndex: number } +> { + constructor(props: any) { + super(props); + + this.state = { + api: props.api, + expanded: false, + rowIndex: props.rowIndex, + }; + } + + // Get all of the cells in the AG Grid and only keep the ones that + // are in the same row as the current expand all button, and make + // sure that they have a JSON blob + getJSONCells = () => { + const instances = this.state.api.getCellRendererInstances(); + + // Make sure row grouping is not enabled, but if it is, don't + // try to find all of the JSON blobs in the row + if ( + instances.filter((instance: any) => instance instanceof GroupCellRenderer) + .length === 0 + ) { + const newInstances = instances.filter( + (instance: any) => + instance.params.rowIndex === this.state.rowIndex && + instance.params.column.colDef.cellRenderer === 'jsonValueRenderer', + ); + + return newInstances; + } + return []; + }; + + // Set the current `expanded` field to the opposite of what it currently is + // as well as go through each cell renderer and if it's in the same row & + // it's a cell with a JSON blob, update whether it is expanded or not + reverseState = () => { + this.setState(prevState => ({ + ...prevState, + expanded: !prevState.expanded, + })); + + const newInstances = this.getJSONCells(); + + newInstances.map((instance: any) => + instance.componentInstance.updateState(!this.state.expanded), + ); + }; + + // Set the current `expanded` field to be equal to the boolean being passed in + // as well as go through each cell renderer and if it's in the same row & + // it's a cell with a JSON blob, update whether it is expanded or not + updateState = (newFlag: any) => { + this.setState(prevState => ({ ...prevState, expanded: newFlag })); + + const newInstances = this.getJSONCells(); + + newInstances.map((instance: any) => + instance.componentInstance.updateState(newFlag), + ); + }; + + // Get all of the cells in the AG Grid and only keep the ones + // that are in the same row as the current expand all button and + // make sure that they have a JSON blob (and see whether they are + // expanded or not) + checkState = () => { + const newInstances = this.getJSONCells(); + + const jsonCellExpandedValues = newInstances.map((instance: any) => + instance.componentInstance.getExpandedValue(), + ); + + // If there is at least one cell that can expand, the expand all + // button for the row should show 'Expand' + this.setState(prevState => ({ + ...prevState, + expanded: !jsonCellExpandedValues.includes(false), + })); + }; + + // Show either the expand or collapse button dependent + // on the value of the `expanded` field + render() { + if (this.state.expanded === false) { + return expandJSON(this.reverseState); + } + return collapseJSON(this.reverseState); + } +} diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/JsonValueRenderer.tsx b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/JsonValueRenderer.tsx index 3eb8342010e9..de16fcf7459c 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/JsonValueRenderer.tsx +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/JsonValueRenderer.tsx @@ -1,28 +1,7 @@ +import { GroupCellRenderer } from '@ag-grid-enterprise/all-modules'; import React, { Component } from 'react'; -import ModalTrigger from 'src/components/ModalTrigger'; import JSONTree from 'react-json-tree'; -import Button from 'src/components/Button'; -import CopyToClipboard from 'src/components/CopyToClipboard'; - -const JSON_TREE_THEME = { - scheme: 'monokai', - base00: '#272822', - base01: '#383830', - base02: '#49483e', - base03: '#75715e', - base04: '#a59f85', - base05: '#f8f8f2', - base06: '#f5f4f1', - base07: '#f9f8f5', - base08: '#f92672', - base09: '#fd971f', - base0A: '#f4bf75', - base0B: '#a6e22e', - base0C: '#a1efe4', - base0D: '#66d9ef', - base0E: '#ae81ff', - base0F: '#cc6633', -}; +import './Buttons.css'; function safeJsonObjectParse( data: unknown, @@ -48,34 +27,60 @@ function safeJsonObjectParse( } } -function addJsonModal( - node: React.ReactNode, - jsonObject: Record | unknown[], - jsonString: String, -) { +// JSX which shows the JSON tree inline, and a button to collapse it +function collapseJSON(this: any, reverseState: any, jsonObject: any) { return ( - } - modalFooter={ - - } - modalTitle="Cell content as JSON" - triggerNode={node} - /> + <> +
+ +
+
+ true} + /> +
+ + ); +} + +// JSX which shows the JSON data on one line, and a button to open the JSON tree +function expandJSON(this: any, reverseState: any, cellData: any) { + return ( + <> + + {cellData} + ); } export default class JsonValueRenderer extends Component< {}, - { cellValue: any } + { api: any; cellValue: any; expanded: boolean; rowIndex: number } > { constructor(props: any) { super(props); this.state = { + api: props.api, cellValue: JsonValueRenderer.getValueToDisplay(props), + expanded: false, + rowIndex: props.rowIndex, }; } @@ -86,14 +91,56 @@ export default class JsonValueRenderer extends Component< }; } + // Set the current `expanded` field to the opposite of what it currently is + // and trigger the 'checkState` function in the expand all button for the row + reverseState = () => { + this.setState( + prevState => ({ ...prevState, expanded: !prevState.expanded }), + () => { + const instances = this.state.api.getCellRendererInstances(); + + // Make sure row grouping is not enabled, but if it is, don't + // trigger the 'checkState` function in the expand all button for the row + if ( + instances.filter( + (instance: any) => instance instanceof GroupCellRenderer, + ).length === 0 + ) { + instances + .filter( + (instance: any) => + instance.params.rowIndex === this.state.rowIndex && + instance.params.column.colDef.cellRenderer === + 'expandAllValueRenderer', + ) + .map((instance: any) => instance.componentInstance.checkState()); + } + }, + ); + }; + + // Take the boolean value passed in and set the `expanded` field equal to it + updateState = (newFlag: any) => { + this.setState(prevState => ({ ...prevState, expanded: newFlag })); + }; + + // Return whether 'expanded' is set to true or false + getExpandedValue = () => this.state.expanded; + render() { const cellData = this.state.cellValue; const jsonObject = safeJsonObjectParse(this.state.cellValue); - const cellNode =
{cellData}
; + + // If there is a JSON object, either show it expanded or collapsed based + // on the value which the `expanded` field is set to if (jsonObject) { - return addJsonModal(cellNode, jsonObject, cellData); + if (this.state.expanded === false) { + return expandJSON(this.reverseState, cellData); + } + return collapseJSON(this.reverseState, jsonObject); } - return cellData ?? null; + // If the cellData is set to 'null' or undefined, return null + return cellData !== 'null' && cellData !== undefined ? cellData : null; } static getValueToDisplay(params: { valueFormatted: any; value: any }) { diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/JumpActionConfig.tsx b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/JumpActionConfig.tsx index 30221152c8e2..563c54a96f0b 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/JumpActionConfig.tsx +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/JumpActionConfig.tsx @@ -1,202 +1,207 @@ -import React, { useState, useCallback, useEffect } from "react"; +import React, { useState, useCallback, useEffect } from 'react'; import SelectControl from 'src/explore/components/controls/SelectControl'; import { bootstrapData } from 'src/preamble'; import Button from 'src/components/Button'; import { - t, - SupersetClient, - validateNonEmpty, - withTheme, - SupersetTheme - } from '@superset-ui/core'; - - - + t, + SupersetClient, + validateNonEmpty, + withTheme, + SupersetTheme, +} from '@superset-ui/core'; interface Props { - dashboardID: number; - filters: any[]; - advancedDataType: string; - error: string; - visiblePopoverIndex: number; - close: () => void; - addDrillActionConfig: (drillactionConfig: any) => boolean; - removeDrillActionConfig: () => boolean + dashboardID: number; + filters: any[]; + advancedDataType: string; + error: string; + visiblePopoverIndex: number; + close: () => void; + addDrillActionConfig: (drillactionConfig: any) => boolean; + removeDrillActionConfig: () => boolean; } const useDashboardState = () => { - - const [dashboardList, setDashboardList] = useState([]); - - const [filterList, setFilterList] = useState([]); - - const fetchDashboardList = useCallback(() => { - const endpoint = `/api/v1/dashboard`; - SupersetClient.get({ endpoint }).then( - ({json}) => { - const dashboards = json.result.filter( (e: any) => { - return JSON.parse((e.json_metadata))?.native_filter_configuration - }) - .map( (e: any) => { - return {value: e.id, label: e.dashboard_title } - }) - setDashboardList(dashboards) - } - ).catch(error => {}); - }, []) - - const fetchFilterList = useCallback((dashboardId: number) => { - const endpoint = `/api/v1/dashboard/${dashboardId}`; - if(dashboardId < 0 ) - return - SupersetClient.get({ endpoint }).then( - ({json}) => { - const metadata = JSON.parse((json.result.json_metadata)) - - setFilterList(metadata?.native_filter_configuration.map( (e: any) => { - return {value: e.id, label: e.name, column: e?.targets[0].column?.name || "" } - })) - - } - ).catch(error => {}); - }, []) - - return { - dashboardList, - filterList, - fetchDashboardList, - fetchFilterList, - } -} - -const DrillActionConfig: React.FC = (props : Props) => { - const {dashboardID, filters, advancedDataType, visiblePopoverIndex } = props - - const {dashboardList, filterList, fetchDashboardList, fetchFilterList} = useDashboardState() - - const [selectedDashboardID, setSelectedDashboardID] = useState(dashboardID) - - const [selectedFilters, setSelectedFilters] = useState(filters?.map( (filter: any) => filter.value) || []) - - - const [advancedDataTypeName, setAdvancedDataTypeName] = useState(advancedDataType) - - const [state, setState] = useState({ - isNew: !props.dashboardID, - }) - - useEffect (()=> { - setSelectedFilters(filters?.map( (filter: any) => filter.value) || []) - setSelectedDashboardID(dashboardID) - setAdvancedDataTypeName(advancedDataType) - }, [dashboardID, filters, advancedDataType, visiblePopoverIndex] - - ) - useEffect(() => { - fetchDashboardList() - },[fetchDashboardList] ) - - useEffect(() => { - fetchFilterList(selectedDashboardID) - }, [fetchFilterList, selectedDashboardID]) - - - const isValidForm = () => { - const errors = [ - validateNonEmpty(selectedDashboardID), - validateNonEmpty(selectedFilters), - validateNonEmpty(advancedDataTypeName), - ]; - return !errors.filter(x => x).length; - } - const applyDrillActionConfig = () => { - - if (isValidForm()) { - const element: any = (dashboardList || []).find( - (e: any) => e.value === selectedDashboardID - ) - const advancedDataTypeNameLabel = bootstrapData?.common?.advanced_data_types.find( (e: { id: string; }) => e.id === advancedDataTypeName)?.verbose_name || advancedDataTypeName - const selectedFiltersWithColumn = filterList.filter( (filter: any) => selectedFilters.includes(filter.value) ) - const name = `${ element?.label } | ${ advancedDataTypeNameLabel }` - const newDrillActionConfig = { - dashboardID: selectedDashboardID, - filters: selectedFiltersWithColumn, - dashBoardName: element?.label || "", - advancedDataType: advancedDataTypeName, - name - } - props.addDrillActionConfig(newDrillActionConfig) - setState({...state, isNew: false}) - props.close() - } + const [dashboardList, setDashboardList] = useState([]); + + const [filterList, setFilterList] = useState([]); + + const fetchDashboardList = useCallback(() => { + const endpoint = `/api/v1/dashboard`; + SupersetClient.get({ endpoint }) + .then(({ json }) => { + const dashboards = json.result + .filter((e: any) => { + return JSON.parse(e.json_metadata)?.native_filter_configuration; + }) + .map((e: any) => { + return { value: e.id, label: e.dashboard_title }; + }); + setDashboardList(dashboards); + }) + .catch(error => {}); + }, []); + + const fetchFilterList = useCallback((dashboardId: number) => { + const endpoint = `/api/v1/dashboard/${dashboardId}`; + if (dashboardId < 0) return; + SupersetClient.get({ endpoint }) + .then(({ json }) => { + const metadata = JSON.parse(json.result.json_metadata); + + setFilterList( + metadata?.native_filter_configuration.map((e: any) => { + return { + value: e.id, + label: e.name, + column: e?.targets[0].column?.name || '', + }; + }), + ); + }) + .catch(error => {}); + }, []); + + return { + dashboardList, + filterList, + fetchDashboardList, + fetchFilterList, + }; +}; + +const DrillActionConfig: React.FC = (props: Props) => { + const { dashboardID, filters, advancedDataType, visiblePopoverIndex } = props; + + const { dashboardList, filterList, fetchDashboardList, fetchFilterList } = + useDashboardState(); + + const [selectedDashboardID, setSelectedDashboardID] = + useState(dashboardID); + + const [selectedFilters, setSelectedFilters] = useState( + filters?.map((filter: any) => filter.value) || [], + ); + + const [advancedDataTypeName, setAdvancedDataTypeName] = + useState(advancedDataType); + + const [state, setState] = useState({ + isNew: !props.dashboardID, + }); + + useEffect(() => { + setSelectedFilters(filters?.map((filter: any) => filter.value) || []); + setSelectedDashboardID(dashboardID); + setAdvancedDataTypeName(advancedDataType); + }, [dashboardID, filters, advancedDataType, visiblePopoverIndex]); + useEffect(() => { + fetchDashboardList(); + }, [fetchDashboardList]); + + useEffect(() => { + fetchFilterList(selectedDashboardID); + }, [fetchFilterList, selectedDashboardID]); + + const isValidForm = () => { + const errors = [ + validateNonEmpty(selectedDashboardID), + validateNonEmpty(selectedFilters), + validateNonEmpty(advancedDataTypeName), + ]; + return !errors.filter(x => x).length; + }; + const applyDrillActionConfig = () => { + if (isValidForm()) { + const element: any = (dashboardList || []).find( + (e: any) => e.value === selectedDashboardID, + ); + const advancedDataTypeNameLabel = + bootstrapData?.common?.advanced_data_types.find( + (e: { id: string }) => e.id === advancedDataTypeName, + )?.verbose_name || advancedDataTypeName; + const selectedFiltersWithColumn = filterList.filter((filter: any) => + selectedFilters.includes(filter.value), + ); + const name = `${element?.label} | ${advancedDataTypeNameLabel}`; + const newDrillActionConfig = { + dashboardID: selectedDashboardID, + filters: selectedFiltersWithColumn, + dashBoardName: element?.label || '', + advancedDataType: advancedDataTypeName, + name, + }; + props.addDrillActionConfig(newDrillActionConfig); + setState({ ...state, isNew: false }); + props.close(); } - - const onDashboardChange = (v: any) => { - setSelectedDashboardID(v) - setSelectedFilters([]) - } - return( -
-
- ({ marginBottom: theme.gridUnit * 4 })} - ariaLabel={t('Annotation layer value')} - name="annotation-layer-value" - label={t('DashBoard')} - showHeader - hovered - placeholder="" - options={dashboardList} - value={selectedDashboardID} - onChange={onDashboardChange} - /> - ({ - value: v.id, - label: v.verbose_name, - }))} - value={advancedDataTypeName} - onChange={setAdvancedDataTypeName} - /> - -
-
- -
- - -
+ }; + + const onDashboardChange = (v: any) => { + setSelectedDashboardID(v); + setSelectedFilters([]); + }; + return ( +
+
+ ({ marginBottom: theme.gridUnit * 4 })} + ariaLabel={t('Annotation layer value')} + name="annotation-layer-value" + label={t('DashBoard')} + showHeader + hovered + placeholder="" + options={dashboardList} + value={selectedDashboardID} + onChange={onDashboardChange} + /> + ({ + value: v.id, + label: v.verbose_name, + }), + )} + value={advancedDataTypeName} + onChange={setAdvancedDataTypeName} + /> + +
+
+ +
+
-
- ); - -} -export default withTheme(DrillActionConfig); \ No newline at end of file +
+
+ ); +}; +export default withTheme(DrillActionConfig); diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/index.tsx b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/index.tsx index 018c354bf46f..3cd912603080 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/index.tsx +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/index.tsx @@ -22,27 +22,27 @@ import CustomListItem from 'src/explore/components/controls/CustomListItem'; import { t, withTheme } from '@superset-ui/core'; import AsyncEsmComponent from 'src/components/AsyncEsmComponent'; import { List } from 'src/components'; -import ControlPopover from 'src/explore/components/controls/ControlPopover/ControlPopover' +import ControlPopover from 'src/explore/components/controls/ControlPopover/ControlPopover'; import { connect } from 'react-redux'; import { HeaderContainer, LabelsContainer, -} from 'src/explore/components/controls/OptionControls' +} from 'src/explore/components/controls/OptionControls'; import ControlHeader from 'src/explore/components/ControlHeader'; export interface Props { - colorScheme: string; - annotationError: object; - annotationQuery: object; - vizType: string; - theme: any; - validationErrors: string[]; - name: string; - actions: object; - label: string; - value?: object[]; - onChange: (a: any) => void, -}; + colorScheme: string; + annotationError: object; + annotationQuery: object; + vizType: string; + theme: any; + validationErrors: string[]; + name: string; + actions: object; + label: string; + value?: object[]; + onChange: (a: any) => void; +} const DrillActionConfig = AsyncEsmComponent( () => import('./JumpActionConfig'), @@ -50,143 +50,147 @@ const DrillActionConfig = AsyncEsmComponent( () =>
, ); - - const DrillActionConfigControl: React.FC = ( - { - colorScheme, - annotationError, - annotationQuery, - vizType, - theme, - validationErrors, - name, - actions, - onChange, - value = [], - ...props - } - ) => - { - // Need to bind function - // Need to preload - const [state, setState] = useState({ - handleVisibleChange: {}, - addedDrillActionConfigIndex: -1, - }); - - const [visiblePopoverIndex, setVisiblePopoverindex] = useState(-1) - - const addDrillActionConfig = (originalDrillActionConfig: any, newDrillActionConfig: any) => - { - let drillActionConfigs = value; - if (drillActionConfigs.includes(originalDrillActionConfig)) { - drillActionConfigs = drillActionConfigs.map(anno => - anno === originalDrillActionConfig ? newDrillActionConfig : anno, - ); - } else { - drillActionConfigs = [...drillActionConfigs, newDrillActionConfig]; - setState({...state, addedDrillActionConfigIndex: drillActionConfigs.length - 1 }); - } - onChange(drillActionConfigs); - } - - - const handleVisibleChange = (visible: any, popoverKey: string | number) => { - setVisiblePopoverindex(visible ? popoverKey: -1) - } - - const removeDrillActionConfig = (drillActionConfig: any) => { - const annotations = value.filter(anno => anno !== drillActionConfig); - onChange(annotations); - } - const renderPopover = (popoverKey: string | number, drillActionConfig: any, error: string = "") => { - const id = drillActionConfig?.name || '_new'; - - return ( -
- - addDrillActionConfig(drillActionConfig, newAnnotation) - } - removeDrillActionConfig={() => removeDrillActionConfig(drillActionConfig)} - close={() => { - handleVisibleChange(false, popoverKey); - setState({...state, addedDrillActionConfigIndex: -1 }); - }} - visi - /> -
+const DrillActionConfigControl: React.FC = ({ + colorScheme, + annotationError, + annotationQuery, + vizType, + theme, + validationErrors, + name, + actions, + onChange, + value = [], + ...props +}) => { + // Need to bind function + // Need to preload + const [state, setState] = useState({ + handleVisibleChange: {}, + addedDrillActionConfigIndex: -1, + }); + + const [visiblePopoverIndex, setVisiblePopoverindex] = useState< + string | number + >(-1); + + const addDrillActionConfig = ( + originalDrillActionConfig: any, + newDrillActionConfig: any, + ) => { + let drillActionConfigs = value; + if (drillActionConfigs.includes(originalDrillActionConfig)) { + drillActionConfigs = drillActionConfigs.map(anno => + anno === originalDrillActionConfig ? newDrillActionConfig : anno, ); + } else { + drillActionConfigs = [...drillActionConfigs, newDrillActionConfig]; + setState({ + ...state, + addedDrillActionConfigIndex: drillActionConfigs.length - 1, + }); } + onChange(drillActionConfigs); + }; - const { addedDrillActionConfigIndex } = state; - const addedDrillActionConfig = value[addedDrillActionConfigIndex]; + const handleVisibleChange = (visible: any, popoverKey: string | number) => { + setVisiblePopoverindex(visible ? popoverKey : -1); + }; - const drillactionConfigs = value.map((anno: any, i) => ( - ({ - '&:hover': { - cursor: 'pointer', - backgroundColor: theme.colors.grayscale.light4, - }, - })} - content={renderPopover( - i, - anno, - )} - visible={visiblePopoverIndex === i} - onVisibleChange={visible => handleVisibleChange(visible, i)} - > - - removeDrillActionConfig(anno)} - data-test="add-annotation-layer-button" - className="fa fa-times" - />{' '} -   {anno.name} - - - )); + const removeDrillActionConfig = (drillActionConfig: any) => { + const annotations = value.filter(anno => anno !== drillActionConfig); + onChange(annotations); + }; + const renderPopover = ( + popoverKey: string | number, + drillActionConfig: any, + error: string = '', + ) => { + const id = drillActionConfig?.name || '_new'; - const addLayerPopoverKey = 'add'; return ( - <> - - - - - ({ borderRadius: theme.gridUnit })}> - {drillactionConfigs} - - handleVisibleChange(visible, addLayerPopoverKey) - } - > - - {' '} -   {t('Add Jump Action')} - - - - - +
+ + addDrillActionConfig(drillActionConfig, newAnnotation) + } + removeDrillActionConfig={() => + removeDrillActionConfig(drillActionConfig) + } + close={() => { + handleVisibleChange(false, popoverKey); + setState({ ...state, addedDrillActionConfigIndex: -1 }); + }} + visi + /> +
); - } + }; + const { addedDrillActionConfigIndex } = state; + const addedDrillActionConfig = value[addedDrillActionConfigIndex]; + + const drillactionConfigs = value.map((anno: any, i) => ( + ({ + '&:hover': { + cursor: 'pointer', + backgroundColor: theme.colors.grayscale.light4, + }, + })} + content={renderPopover(i, anno)} + visible={visiblePopoverIndex === i} + onVisibleChange={visible => handleVisibleChange(visible, i)} + > + + removeDrillActionConfig(anno)} + data-test="add-annotation-layer-button" + className="fa fa-times" + />{' '} +   {anno.name} + + + )); + + const addLayerPopoverKey = 'add'; + return ( + <> + + + + + ({ borderRadius: theme.gridUnit })}> + {drillactionConfigs} + + handleVisibleChange(visible, addLayerPopoverKey) + } + > + + {' '} +   {t('Add Jump Action')} + + + + + + ); +}; // Tried to hook this up through stores/control.jsx instead of using redux // directly, could not figure out how to get access to the color_scheme @@ -198,10 +202,6 @@ function mapStateToProps({ charts, explore }: any) { }; } - - const themedDrillActionConfigControl = withTheme(DrillActionConfigControl); -export default connect( - mapStateToProps, -)(themedDrillActionConfigControl); +export default connect(mapStateToProps)(themedDrillActionConfigControl); diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/plugin/controlPanel.tsx b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/plugin/controlPanel.tsx index 65530515d9f8..991a7ca8f4b5 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/plugin/controlPanel.tsx +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/plugin/controlPanel.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react' +import React from 'react'; import { t, @@ -48,7 +48,6 @@ import { StyledColumnOption } from 'src/explore/components/optionRenderers'; import DrillActionConfig from '../components/controls/JumpActionConfigControll'; - export const PAGE_SIZE_OPTIONS = formatSelectOptions([ [0, t('page_size.all')], 10, @@ -104,7 +103,6 @@ const validateAggControlValues = ( : []; }; - // function isIP(v: unknown) { // if (typeof v === 'string' && v.trim().length > 0) { // //console.log(v.trim()); @@ -229,7 +227,7 @@ const config: ControlPanelConfig = { return newState; }, - rerender: ['metrics', 'percent_metrics', 'default_group_by',], + rerender: ['metrics', 'percent_metrics', 'default_group_by'], }, }, ], @@ -332,7 +330,7 @@ const config: ControlPanelConfig = { rerender: ['principalColumns', 'default_group_by'], visibility: isRawMode, canCopy: true, - } + }, }, ], [ @@ -422,9 +420,9 @@ const config: ControlPanelConfig = { controlState: ControlState, ) => { const { controls } = state; - const originalMapStateToProps = isRawMode({ controls }) ? - sharedControls?.columns?.mapStateToProps : - sharedControls?.groupby?.mapStateToProps; + const originalMapStateToProps = isRawMode({ controls }) + ? sharedControls?.columns?.mapStateToProps + : sharedControls?.groupby?.mapStateToProps; const newState = originalMapStateToProps?.(state, controlState) ?? {}; const choices = isRawMode({ controls }) @@ -454,7 +452,6 @@ const config: ControlPanelConfig = { return newState; }, visibility: ({ controls }) => - // TODO properly emsure is Bool Boolean(controls?.emitFilter?.value), canCopy: true, }, @@ -590,8 +587,10 @@ config.controlPanelSections.push({ renderTrigger: true, default: false, description: t( - 'Whether to enable row grouping (this will only take affect after a save)', + 'Whether to enable row grouping (this will only take affect after a save). NOTE: "JSON Row Expand" and "Row Grouping" are mutually exclusive. If "Row Grouping" is selected, "JSON Row Expand" will not be visible.', ), + visibility: ({ controls }) => + Boolean(!controls?.enable_json_expand?.value), }, }, ], @@ -625,9 +624,9 @@ config.controlPanelSections.push({ controlState: ControlState, ) => { const { controls } = state; - const originalMapStateToProps = isRawMode({ controls }) ? - sharedControls?.columns?.mapStateToProps : - sharedControls?.groupby?.mapStateToProps; + const originalMapStateToProps = isRawMode({ controls }) + ? sharedControls?.columns?.mapStateToProps + : sharedControls?.groupby?.mapStateToProps; const newState = originalMapStateToProps?.(state, controlState) ?? {}; const choices = isRawMode({ controls }) @@ -637,9 +636,9 @@ config.controlPanelSections.push({ (o: { column_name: string }) => ensureIsArray(choices).includes(o.column_name), ); - const invalidOptions = ensureIsArray( - controlState.value, - ).filter(c => !ensureIsArray(choices).includes(c)); + const invalidOptions = ensureIsArray(controlState.value).filter( + c => !ensureIsArray(choices).includes(c), + ); newState.externalValidationErrors = invalidOptions.length > 0 ? invalidOptions.length > 1 @@ -648,11 +647,7 @@ config.controlPanelSections.push({ ' are not valid options', )}`, ] - : [ - `'${invalidOptions}'${t( - ' is not a valid option', - )}`, - ] + : [`'${invalidOptions}'${t(' is not a valid option')}`] : []; return newState; }, @@ -660,7 +655,7 @@ config.controlPanelSections.push({ Boolean(controls?.enable_grouping?.value), canCopy: true, }, - } + }, ], [ { @@ -674,6 +669,22 @@ config.controlPanelSections.push({ }, }, ], + [ + { + name: 'enable_json_expand', + config: { + type: 'CheckboxControl', + label: t('JSON Row Expand'), + renderTrigger: true, + default: false, + description: t( + 'Whether to enable row level JSON expand buttons. NOTE: "JSON Row Expand" and "Row Grouping" are mutually exclusive. If "JSON Row Expand" is selected, "Row Grouping" will not be visible.', + ), + visibility: ({ controls }) => + Boolean(!controls?.enable_grouping?.value), + }, + }, + ], [ { name: 'page_length', diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/plugin/transformProps.ts b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/plugin/transformProps.ts index b9123379ff83..347c5c4d3f4e 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/plugin/transformProps.ts +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/plugin/transformProps.ts @@ -83,6 +83,7 @@ export default function transformProps(chartProps: CccsGridChartProps) { enable_grouping, column_state, enable_row_numbers, + enable_json_expand, jump_action_configs, default_group_by, }: CccsGridQueryFormData = { ...DEFAULT_FORM_DATA, ...formData }; @@ -233,10 +234,11 @@ export default function transformProps(chartProps: CccsGridChartProps) { const isSortable = true; const enableRowGroup = true; const columnDescription = columnDescriptionMap[column]; - const rowGroupIndex = default_group_by.findIndex((element: any) => { - return element === column + const autoHeight = true; + const rowGroupIndex = default_group_by.findIndex((element: any) => { + return element === column; }); - const rowGroup = rowGroupIndex >= 0 + const rowGroup = rowGroupIndex >= 0; const hide = rowGroup; return { field: column, @@ -251,6 +253,7 @@ export default function transformProps(chartProps: CccsGridChartProps) { rowGroupIndex, getQuickFilterText: (params: any) => valueFormatter(params), headerTooltip: columnDescription, + autoHeight, }; }); } else { @@ -270,11 +273,12 @@ export default function transformProps(chartProps: CccsGridChartProps) { const isSortable = true; const enableRowGroup = true; const columnDescription = columnDescriptionMap[column]; - const rowGroupIndex = default_group_by.findIndex((element: any) => { - return element === column - }); + const autoHeight = true; + const rowGroupIndex = default_group_by.findIndex( + (element: any) => element === column, + ); const initialRowGroupIndex = rowGroupIndex; - const rowGroup = (rowGroupIndex >= 0) + const rowGroup = rowGroupIndex >= 0; const hide = rowGroup; return { field: column, @@ -288,6 +292,7 @@ export default function transformProps(chartProps: CccsGridChartProps) { hide, getQuickFilterText: (params: any) => valueFormatter(params), headerTooltip: columnDescription, + autoHeight, }; }); columnDefs = columnDefs.concat(groupByColumnDefs); @@ -333,6 +338,7 @@ export default function transformProps(chartProps: CccsGridChartProps) { headerName: '#', colId: 'rowNum', pinned: 'left', + lockVisible: true, valueGetter: (params: any) => params.node ? params.node.rowIndex + 1 : null, } as any); @@ -358,6 +364,19 @@ export default function transformProps(chartProps: CccsGridChartProps) { } }); + // If the flag is set to true, add a column which will contain + // a button to expand all JSON blobs in the row + if (enable_json_expand) { + columnDefs.splice(1, 0, { + colId: 'jsonExpand', + pinned: 'left', + cellRenderer: 'expandAllValueRenderer', + autoHeight: true, + minWidth: 105, + lockVisible: true, + } as any); + } + return { formData, setDataMask, diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/types.ts b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/types.ts index a4754380c79d..4dc11b0fd7fe 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/types.ts +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/types.ts @@ -88,7 +88,7 @@ export interface CccsGridTransformedProps extends CccsGridStylesProps { // add typing here for the props you pass in from transformProps.ts! agGridLicenseKey: string; datasetColumns: Column[]; - jumpActionConfigs?: any[] + jumpActionConfigs?: any[]; } export type EventHandlers = Record; diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/IFrameVisualization.tsx b/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/IFrameVisualization.tsx index fa1cec2d7442..9fb1f9c05a14 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/IFrameVisualization.tsx +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/IFrameVisualization.tsx @@ -1,17 +1,35 @@ import React from 'react'; import { IFrameVisualizationProps } from './types'; - export default function IFrameVisualization(props: IFrameVisualizationProps) { - const { url, url_parameter_value, parameter_name, parameter_prefix, errorMessage} = props - - const parserdUrlParameterName = parameter_name.includes('=') ? parameter_name : `${parameter_name}=${parameter_prefix}` + const { + url, + url_parameter_value, + parameter_name, + parameter_prefix, + errorMessage, + } = props; + + const parserdUrlParameterName = parameter_name.includes('=') + ? parameter_name + : `${parameter_name}=${parameter_prefix}`; return ( <> - { errorMessage ? - <>{errorMessage} : - } + {errorMessage ? ( + <>{errorMessage} + ) : ( + + )} ); } diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/buildQuery.ts b/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/buildQuery.ts index 22405e6d1202..245655da2c1c 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/buildQuery.ts +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/buildQuery.ts @@ -16,42 +16,41 @@ * specific language governing permissions and limitations * under the License. */ - import { buildQueryContext, QueryFormData } from '@superset-ui/core'; +import { buildQueryContext, QueryFormData } from '@superset-ui/core'; - /** - * The buildQuery function is used to create an instance of QueryContext that's - * sent to the chart data endpoint. In addition to containing information of which - * datasource to use, it specifies the type (e.g. full payload, samples, query) and - * format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from - * the datasource as opposed to using a cached copy of the data, if available. - * - * More importantly though, QueryContext contains a property `queries`, which is an array of - * QueryObjects specifying individual data requests to be made. A QueryObject specifies which - * columns, metrics and filters, among others, to use during the query. Usually it will be enough - * to specify just one query based on the baseQueryObject, but for some more advanced use cases - * it is possible to define post processing operations in the QueryObject, or multiple queries - * if a viz needs multiple different result sets. - */ - export default function buildQuery(formData: QueryFormData) { - /* +/** + * The buildQuery function is used to create an instance of QueryContext that's + * sent to the chart data endpoint. In addition to containing information of which + * datasource to use, it specifies the type (e.g. full payload, samples, query) and + * format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from + * the datasource as opposed to using a cached copy of the data, if available. + * + * More importantly though, QueryContext contains a property `queries`, which is an array of + * QueryObjects specifying individual data requests to be made. A QueryObject specifies which + * columns, metrics and filters, among others, to use during the query. Usually it will be enough + * to specify just one query based on the baseQueryObject, but for some more advanced use cases + * it is possible to define post processing operations in the QueryObject, or multiple queries + * if a viz needs multiple different result sets. + */ +export default function buildQuery(formData: QueryFormData) { + /* We receive an ip as a filter, our job is to find everthing there is to know about that ip We fire multiple queries to multiple data sets and collect the results here. */ - - const formDataCopy = { - ...formData, - result_type: 'post_processed', - }; - - return buildQueryContext(formDataCopy, baseQueryObject => { - // RAW mode (not aggregated) - // eslint-disable-next-line no-param-reassign - return [ - { - ...baseQueryObject, - row_limit: 10, - }, - ]; - }); - } - \ No newline at end of file + + const formDataCopy = { + ...formData, + result_type: 'post_processed', + }; + + return buildQueryContext(formDataCopy, baseQueryObject => { + // RAW mode (not aggregated) + // eslint-disable-next-line no-param-reassign + return [ + { + ...baseQueryObject, + row_limit: 10, + }, + ]; + }); +} diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/controlPanel.ts b/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/controlPanel.ts index 5c7a9df66fbc..3b35503b6e30 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/controlPanel.ts +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/controlPanel.ts @@ -25,7 +25,6 @@ import { } from '@superset-ui/chart-controls'; const config: ControlPanelConfig = { - /** * The control panel is split into two tabs: "Query" and * "Chart Options". The controls that define the inputs to @@ -99,7 +98,7 @@ const config: ControlPanelConfig = { * - validateInteger: must be an integer value * - validateNumber: must be an intger or decimal value */ - + // For control input types, see: superset-frontend/src/explore/components/controls/index.js controlPanelSections: [ { @@ -121,7 +120,9 @@ const config: ControlPanelConfig = { sharedControls?.groupby?.mapStateToProps; const newState = originalMapStateToProps?.(state, controlState) ?? {}; - newState.externalValidationErrors = controlState.value ? [] : ["Please add a value for URL."] + newState.externalValidationErrors = controlState.value + ? [] + : ['Please add a value for URL.']; return newState; }, renderTrigger: true, @@ -135,7 +136,8 @@ const config: ControlPanelConfig = { name: 'groupby', override: { label: t('Parameter Column Name'), - description: "The name of the column that will populate the url parameter value.", + description: + 'The name of the column that will populate the url parameter value.', multi: false, allowAll: false, default: [], @@ -148,7 +150,10 @@ const config: ControlPanelConfig = { sharedControls?.groupby?.mapStateToProps; const newState = originalMapStateToProps?.(state, controlState) ?? {}; - newState.externalValidationErrors = ensureIsArray(controlState.value).length > 0 ? [] : ["Please add a value for Parameter Column Name."] + newState.externalValidationErrors = + ensureIsArray(controlState.value).length > 0 + ? [] + : ['Please add a value for Parameter Column Name.']; return newState; }, }, @@ -168,7 +173,9 @@ const config: ControlPanelConfig = { sharedControls?.groupby?.mapStateToProps; const newState = originalMapStateToProps?.(state, controlState) ?? {}; - newState.externalValidationErrors = controlState.value ? [] : ["Please add a value for Parameter Name."] + newState.externalValidationErrors = controlState.value + ? [] + : ['Please add a value for Parameter Name.']; return newState; }, default: '', @@ -183,10 +190,12 @@ const config: ControlPanelConfig = { type: 'TextControl', label: t('Parameter Prefix'), default: '', - description: t('A value that will be prefix the parameter value.'), + description: t( + 'A value that will be prefix the parameter value.', + ), }, }, - ] + ], ], }, ], diff --git a/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/transformProps.ts b/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/transformProps.ts index ee67ef9d2aff..08a54a5b0f61 100644 --- a/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/transformProps.ts +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-iframe/src/plugin/transformProps.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps, TimeseriesDataRecord, } from '@superset-ui/core'; +import { ChartProps, TimeseriesDataRecord } from '@superset-ui/core'; export default function transformProps(chartProps: ChartProps) { /** @@ -51,25 +51,26 @@ export default function transformProps(chartProps: ChartProps) { const formData = chartProps.formData; const queriesData = chartProps.queriesData; - const { url, parameterName, parameterPrefix, groupby } = formData + const { url, parameterName, parameterPrefix, groupby } = formData; const data = queriesData[0]?.data as TimeseriesDataRecord[]; - let value: string | number | true | Date = "" - let errorMessage = ""; + let value: string | number | true | Date = ''; + let errorMessage = ''; - if(Array.isArray(data) && data.length > 1) { - errorMessage = "The query returned too many rows when only one was expected." + if (Array.isArray(data) && data.length > 1) { + errorMessage = + 'The query returned too many rows when only one was expected.'; } - - if(Array.isArray(data) && data.length === 0) { - errorMessage = "The query returned no rows." + + if (Array.isArray(data) && data.length === 0) { + errorMessage = 'The query returned no rows.'; } - if(Array.isArray(data) && data.length === 1) { - value = data[0][groupby] || "" + if (Array.isArray(data) && data.length === 1) { + value = data[0][groupby] || ''; } - + return { url_parameter_value: value, parameter_name: parameterName, diff --git a/tests/unit_tests/explore/utils_test.py b/tests/unit_tests/explore/utils_test.py index d9f259972360..6d4e1b7dcc88 100644 --- a/tests/unit_tests/explore/utils_test.py +++ b/tests/unit_tests/explore/utils_test.py @@ -50,6 +50,7 @@ "superset.connectors.sqla.models.SqlaTable.query_datasources_by_name" ) + def test_unsaved_chart_no_dataset_id(app_context: AppContext) -> None: from superset.explore.utils import check_access as check_chart_access