diff --git a/cccs-build/superset/Dockerfile b/cccs-build/superset/Dockerfile index 034ad4433c2c6..85843ad2941f5 100644 --- a/cccs-build/superset/Dockerfile +++ b/cccs-build/superset/Dockerfile @@ -5,6 +5,7 @@ FROM uchimera.azurecr.io/cccs/superset-base:cccs-2.0_20230117193544_b6002 + USER root COPY *requirements.txt /tmp/ 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 14c8ba5b47619..67167ad1860fc 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 @@ -53,6 +53,7 @@ import '@ag-grid-community/core/dist/styles/ag-grid.css'; import '@ag-grid-community/core/dist/styles/ag-theme-balham.css'; import { PAGE_SIZE_OPTIONS } from './plugin/controlPanel'; +import rison from 'rison'; const DEFAULT_COLUMN_DEF = { editable: false, @@ -81,14 +82,18 @@ export default function CccsGrid({ enable_grouping = false, column_state, filters: initialFilters = {}, + datasetColumns, + jumpActionConfigs, }: CccsGridTransformedProps) { LicenseManager.setLicenseKey(agGridLicenseKey); const dispatch = useDispatch(); 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 [filters, setFilters] = useState(initialFilters); const [principalColumnFilters, setPrincipalColumnFilters] = useState({}); const [searchValue, setSearchValue] = useState(''); const [pageSize, setPageSize] = useState(page_length); @@ -100,7 +105,6 @@ export default function CccsGrid({ if (!emitFilter) { return; } - const groupBy = Object.keys(filters); const groupByValues = Object.values(filters); setDataMask({ @@ -127,57 +131,131 @@ export default function CccsGrid({ }, }); }, - [emitFilter, setDataMask, filters, principalColumnFilters], + [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 navtiveFilter = { + extraFormData: { + filters: [ + {col: column, + op: "IN", + val: stringSelectedData} + ] + }, + filterState: { + label: stringSelectedData, + validateStatus: false, + value: stringSelectedData + }, + id: nativefilterID, + 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 = "" + advancedDataTypeNativeFilters.forEach((element: any) => { + jumpActionName = element.name + let advancedDataType = element['advancedDataType']; + let nativefilters: any[] = element["nativefilters"]; + let selectedDataForUrl = selectedData[advancedDataType]; + + if (selectedDataForUrl && nativefilters) { + nativefilters.forEach( filter => { + nativeFilterUrls[filter["value"]] = generateNativeFilterUrlString(filter["value"], selectedDataForUrl, filter["column"]) + }); + } + + }); + + 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)}` + window.open(url, '_blank'); + } + + let DashboardMenuItem = { + name: jumpActionName, + action + } + + sub_menu.push(DashboardMenuItem) + } + + } + + 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)[] = []; - if (!emitFilter) { - result = ['copy', 'copyWithHeaders', 'paste', 'separator', 'export']; - } else { - result = [ - 'copy', - 'copyWithHeaders', - 'paste', - 'separator', - { - name: 'Emit Filter(s)', - disabled: params.value === null, - action: () => handleChange(filters), - // 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 = ['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( + [ 'separator', - 'export', - ]; - } + 'export' + ] + ) + return result; }, [ emitFilter, + selectedDataByColumnName, principalColumns, crossFilterValue, handleChange, - filters, principalColumnFilters, dispatch, formData.slice_id, @@ -220,26 +298,51 @@ export default function CccsGrid({ const gridApi = params.api; const cellRanges = gridApi.getCellRanges(); - - const updatedFilters = {}; + + 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); - updatedFilters[col] = updatedFilters[col] || []; + let advancedDataType = datasetColumns.find( (column) => { return column.column_name == col })?.advanced_data_type || "" + + + updatedSelectedData[col] = updatedSelectedData[col] || []; + + newSelectedbyAdvancedType[advancedDataType] = newSelectedbyAdvancedType[advancedDataType] || [] + const startRow = Math.min( range.startRow.rowIndex, range.endRow.rowIndex, ); + const endRow = Math.max(range.startRow.rowIndex, range.endRow.rowIndex); - for (let rowIndex = startRow; rowIndex <= endRow; rowIndex += 1) { + for (let rowIndex = startRow; rowIndex <= endRow; rowIndex++) { + + const rowNode = gridApi.getModel().getRow(rowIndex) const value = gridApi.getValue( column, - gridApi.getModel().getRow(rowIndex), + rowNode, ); - if (!updatedFilters[col].includes(value)) { - updatedFilters[col].push(value); + + 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}) + } + renderedValue = valueRendererObjt ? valueRendererObjt.render() : value + + if (!updatedSelectedData[col].includes(value)) { + updatedSelectedData[col].push(value); + } + if (!newSelectedbyAdvancedType[advancedDataType].includes(renderedValue) && renderedValue) { + newSelectedbyAdvancedType[advancedDataType].push(renderedValue); } } }); @@ -263,8 +366,8 @@ export default function CccsGrid({ } }); }); - - setFilters(updatedFilters); + setselectedDataByAdvancedType(newSelectedbyAdvancedType); + setSelectedDataColumnName(updatedSelectedData); setPrincipalColumnFilters(updatedPrincipalColumnFilters); }; 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 new file mode 100644 index 0000000000000..30221152c8e26 --- /dev/null +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/JumpActionConfig.tsx @@ -0,0 +1,202 @@ +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'; + + + + +interface Props { + 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 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 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 new file mode 100644 index 0000000000000..018c354bf46ff --- /dev/null +++ b/superset-frontend/src/cccs-viz/plugins/plugin-chart-cccs-grid/src/components/controls/JumpActionConfigControll/index.tsx @@ -0,0 +1,207 @@ +/** + * 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, { useState } from 'react'; +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 { connect } from 'react-redux'; +import { + HeaderContainer, + LabelsContainer, +} 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, +}; + +const DrillActionConfig = AsyncEsmComponent( + () => import('./JumpActionConfig'), + // size of overlay inner content + () =>
, +); + + + 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 { 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 +function mapStateToProps({ charts, explore }: any) { + return { + // eslint-disable-next-line camelcase + colorScheme: explore.controls?.color_scheme?.value, + vizType: explore.controls.viz_type.value, + }; +} + + + +const themedDrillActionConfigControl = withTheme(DrillActionConfigControl); + +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 f34e80aa2dc9b..d69a73c510610 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,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import React from 'react' + import { t, FeatureFlag, @@ -44,7 +46,8 @@ import { } from '@superset-ui/chart-controls'; import { StyledColumnOption } from 'src/explore/components/optionRenderers'; -//import cidrRegex from 'cidr-regex'; +import DrillActionConfig from '../components/controls/JumpActionConfigControll'; + export const PAGE_SIZE_OPTIONS = formatSelectOptions([ [0, t('page_size.all')], @@ -619,6 +622,17 @@ config.controlPanelSections.push({ }, }, ], + [ + { + name: 'jump_action_configs', + config: { + type: DrillActionConfig, + renderTrigger: true, + label: t('Jump Actions'), + description: t('Configure dashboard jump actions.'), + }, + }, + ], ], }); 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 a7dad737b934d..4e087ae870739 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, + jump_action_configs, }: CccsGridQueryFormData = { ...DEFAULT_FORM_DATA, ...formData }; const data = queriesData[0].data as TimeseriesDataRecord[]; const agGridLicenseKey = queriesData[0].agGridLicenseKey as String; @@ -315,6 +316,24 @@ export default function transformProps(chartProps: CccsGridChartProps) { params.node ? params.node.rowIndex + 1 : null, } as any); } + let parsed_jump_action_configs = {} + jump_action_configs?.forEach( (e: any) => + { + if (e.dashboardID in parsed_jump_action_configs) { + parsed_jump_action_configs[e.dashboardID] = parsed_jump_action_configs[e.dashboardID].concat({ + advancedDataType: e.advancedDataType, + nativefilters: e.filters, + name: e.dashBoardName + }) + } + else { + parsed_jump_action_configs[e.dashboardID] = [{ + advancedDataType: e.advancedDataType, + nativefilters: e.filters, + name: e.dashBoardName + }] + } + }); return { formData, @@ -335,5 +354,7 @@ export default function transformProps(chartProps: CccsGridChartProps) { enable_grouping, column_state, agGridLicenseKey, + datasetColumns: columns, + jumpActionConfigs: parsed_jump_action_configs }; } 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 625abd08599a3..a4754380c79df 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 @@ -25,6 +25,7 @@ import { SetDataMaskHook, supersetTheme, TimeseriesDataRecord, + Column, } from '@superset-ui/core'; export type CccsGridQueryFormData = QueryFormData & { @@ -37,6 +38,7 @@ export type CccsGridQueryFormData = QueryFormData & { enable_grouping: boolean; column_state: ColumnState[]; enable_row_numbers: boolean; + jump_action_configs?: any[]; }; export interface CccsGridStylesProps { @@ -85,6 +87,8 @@ export interface CccsGridTransformedProps extends CccsGridStylesProps { column_state: ColumnState[]; // add typing here for the props you pass in from transformProps.ts! agGridLicenseKey: string; + datasetColumns: Column[]; + jumpActionConfigs?: any[] } export type EventHandlers = Record;