From cd815ee25fadb9aaa58fedd6055d56b15d548fe7 Mon Sep 17 00:00:00 2001 From: Bryn Ryans Date: Fri, 27 Oct 2023 23:16:35 +0000 Subject: [PATCH] feat: tile extension cross filter support --- .../components/EventTester/EventTester.js | 189 ++++++++++++++---- react/javascript/tile-sdk/src/utils/utils.js | 90 +++++++++ 2 files changed, 238 insertions(+), 41 deletions(-) create mode 100644 react/javascript/tile-sdk/src/utils/utils.js diff --git a/react/javascript/tile-sdk/src/components/Inspector/components/EventTester/EventTester.js b/react/javascript/tile-sdk/src/components/Inspector/components/EventTester/EventTester.js index 5f4bce5..a71b5e3 100644 --- a/react/javascript/tile-sdk/src/components/Inspector/components/EventTester/EventTester.js +++ b/react/javascript/tile-sdk/src/components/Inspector/components/EventTester/EventTester.js @@ -23,7 +23,13 @@ SOFTWARE. */ -import React, { useCallback, useContext, useState, useRef } from 'react' +import React, { + useCallback, + useContext, + useState, + useRef, + useMemo, +} from 'react' import { Space, Accordion2, @@ -32,18 +38,75 @@ import { Grid, ButtonOutline, FieldToggleSwitch, + Paragraph, } from '@looker/components' import { ExtensionContext40 } from '@looker/extension-sdk-react' +import { + getDrillLinks, + getCrossfilterSelection, + CrossfilterSelection, +} from '../../../../utils/utils' export const EventTester = () => { const { extensionSDK, tileSDK, - tileHostData: { dashboardFilters }, + tileHostData: { + dashboardFilters, + isExploring, + isDashboardEditing, + isDashboardCrossFilteringEnabled, + }, visualizationData, + visualizationSDK, } = useContext(ExtensionContext40) const [runDashboard, setRunDashboard] = useState(false) const openDrillMenuButtonRef = useRef() + const toggleCrossFilterButtonRef = useRef() + + const currentCrossFiltersSelection = useMemo(() => { + if (isDashboardCrossFilteringEnabled && visualizationSDK) { + const queryResponse = visualizationSDK.queryResponse + if (queryResponse) { + let row + let pivot + if (queryResponse?.data.length > 0) { + row = queryResponse?.data[0] + } + if (queryResponse?.pivot?.length > 0) { + pivot = queryResponse?.pivot[0] + } + return getCrossfilterSelection(row, pivot) + } + } + return undefined + }, [isDashboardCrossFilteringEnabled, visualizationSDK, visualizationData]) + + const currentCrossFiltersSelectionDesc = useMemo(() => { + if (!isExploring) { + switch (currentCrossFiltersSelection) { + case CrossfilterSelection.NONE: { + return 'None' + } + case CrossfilterSelection.SELECTED: { + return 'Selected' + } + case CrossfilterSelection.UNSELECTED: { + return 'Unselected' + } + default: { + return isDashboardCrossFilteringEnabled + ? 'Unknown' + : 'Cross filtering disabled' + } + } + } + return 'Not supported when exploring' + }, [ + currentCrossFiltersSelection, + isDashboardCrossFilteringEnabled, + isExploring, + ]) const addErrorsClick = useCallback(() => { tileSDK.addErrors({ @@ -56,6 +119,17 @@ export const EventTester = () => { tileSDK.clearErrors() }, [tileSDK]) + const buildEvent = useCallback((buttonRef) => { + let event = { pageX: 0, pageY: 0 } + if (buttonRef.current) { + const { bottom, left } = buttonRef.current.getBoundingClientRect() + // Add 95px to the x coordinate to shift the menu + // under the button. + event = { pageX: left + 95, pageY: bottom } + } + return event + }, []) + const triggerClick = useCallback( (event) => { // Taken from custom visualizations 2 @@ -80,34 +154,43 @@ export const EventTester = () => { const toggleCrossFilterClick = useCallback( (event) => { - // TODO pivot and row data needs to be populated - tileSDK.toggleCrossFilter({ pivot: {}, row: {} }, event) + if (isDashboardCrossFilteringEnabled && visualizationSDK) { + const queryResponse = visualizationSDK.queryResponse + if (queryResponse) { + let row + let pivot + if (queryResponse?.data.length > 0) { + row = queryResponse?.data[0] + } + if (queryResponse?.pivot?.length > 0) { + pivot = queryResponse?.pivot[0] + } + tileSDK.toggleCrossFilter( + { pivot, row }, + buildEvent(toggleCrossFilterButtonRef) + ) + } + } }, - [tileSDK] + [ + tileSDK, + isDashboardCrossFilteringEnabled, + visualizationSDK, + visualizationData, + ] ) const openDrillMenuClick = useCallback( (_event) => { - let event = { pageX: 0, pageY: 0 } - let links = [] - const data = visualizationData?.queryResponse?.data - if (data && data.length > 0) { - const row = data[0] - const column = Array.from(Object.keys(row)).find( - (column) => row[column].links?.length > 0 - ) - if (column) { - links = [...row[column].links] - } - } - if (openDrillMenuButtonRef.current) { - const { bottom, left } = - openDrillMenuButtonRef.current.getBoundingClientRect() - // Add 95px to the x coordinate to shift the menu - // under the button. - event = { pageX: left + 95, pageY: bottom } + const links = getDrillLinks(visualizationData?.queryResponse?.data, 0, 0) + if (links.length === 0) { + tileSDK.addErrors({ + title: 'Drilling Error', + message: 'No drill links found', + }) + } else { + tileSDK.openDrillMenu({ links }, buildEvent(openDrillMenuButtonRef)) } - tileSDK.openDrillMenu({ links }, event) }, [tileSDK, visualizationData] ) @@ -159,7 +242,25 @@ export const EventTester = () => { Test clear errors - + + Test run dashboard + + + Test stop dashboard + + Test trigger { > Test open drill menu - + Test toggle cross filter - - Test run dashboard - - - Test stop dashboard - - - Test update filters - - setRunDashboard(event.target.checked)} - on={runDashboard} - > + + Cross filter selection: {currentCrossFiltersSelectionDesc} + - + + Test update filters + + setRunDashboard(event.target.checked)} + on={runDashboard} + > + Test open schedule dialog diff --git a/react/javascript/tile-sdk/src/utils/utils.js b/react/javascript/tile-sdk/src/utils/utils.js new file mode 100644 index 0000000..9260bb8 --- /dev/null +++ b/react/javascript/tile-sdk/src/utils/utils.js @@ -0,0 +1,90 @@ +/* + + MIT License + + Copyright (c) 2023 Looker Data Sciences, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ + +// TODO move these methods into the extension SDK + +/** + * Get drill links for a row and a column. Column can be a numeric + * index OR the name of the column. + */ +export const getDrillLinks = (data, row, column) => { + let links = [] + if (data && data.length && row < data.length) { + const selectedRow = data[row] + const columnKeys = Array.from(Object.keys(selectedRow)) + let selectedColumnKey + if (typeof column === 'number') { + selectedColumnKey = columnKeys[column] + } else if (columnKeys.includes(column)) { + selectedColumnKey = column + } + if (selectedColumnKey) { + const selectedColumn = selectedRow[selectedColumnKey] + if (selectedColumn?.links?.length && selectedColumn?.links?.length > 0) { + links = [...selectedColumn.links] + } + } + } + return links +} + +export const CrossfilterSelection = Object.seal({ + NONE: 0, + SELECTED: 1, + UNSELECTED: 2, +}) + +/** + * Checks if crossfilters are selected for a Row + Pivot + * by inspecting the crossfilterSelection property of the cells + * Will return true if every cell is selected (ignoring undefined selections) + * Call only when crossfilters are present in the element + */ +export const getCrossfilterSelection = (row, pivot) => { + // get row crossfilterSelection values + const rowCells = Object.values(row || {}).map( + (item) => item.crossfilterSelection + ) + // get pivot crossfilterSelection values + const pivotCells = Object.values(pivot?.metadata || {}).map( + (item) => item.crossfilterSelection + ) + // merge both lists and remove undefined values + const cells = [...rowCells, ...pivotCells].filter((value) => !!value) + + // if there are non undefined any selection values + if (cells.length) { + // if every cell is selected + return cells.every((value) => value === CrossfilterSelection.SELECTED) + ? // return selected + CrossfilterSelection.SELECTED + : // if at least one is unselected, return unselected + CrossfilterSelection.UNSELECTED + } + + // if all cells are undefined, return none + return CrossfilterSelection.NONE +}