Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tile extension cross filter support #117

Merged
merged 1 commit into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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
Expand All @@ -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]
)
Expand Down Expand Up @@ -159,7 +242,25 @@ export const EventTester = () => {
<ButtonOutline onClick={clearErrorsClick} width="100%">
Test clear errors
</ButtonOutline>
<ButtonOutline onClick={triggerClick} width="100%">
<ButtonOutline
onClick={runDashboardClick}
width="100%"
disabled={isExploring}
>
Test run dashboard
</ButtonOutline>
<ButtonOutline
onClick={stopDashboardClick}
width="100%"
disabled={isExploring}
>
Test stop dashboard
</ButtonOutline>
<ButtonOutline
onClick={triggerClick}
width="100%"
disabled={!visualizationData}
>
Test trigger
</ButtonOutline>
<ButtonOutline
Expand All @@ -169,26 +270,32 @@ export const EventTester = () => {
>
Test open drill menu
</ButtonOutline>
<ButtonOutline onClick={toggleCrossFilterClick} width="100%">
<ButtonOutline
onClick={toggleCrossFilterClick}
width="100%"
disabled={!visualizationData || isExploring}
ref={toggleCrossFilterButtonRef}
>
Test toggle cross filter
</ButtonOutline>
<ButtonOutline onClick={runDashboardClick} width="100%">
Test run dashboard
</ButtonOutline>
<ButtonOutline onClick={stopDashboardClick} width="100%">
Test stop dashboard
</ButtonOutline>
<Space width="100%">
<ButtonOutline onClick={updateFiltersClick} width="50%">
Test update filters
</ButtonOutline>
<FieldToggleSwitch
label="Run dashboard"
onChange={(event) => setRunDashboard(event.target.checked)}
on={runDashboard}
></FieldToggleSwitch>
<Paragraph width="100%">
Cross filter selection: {currentCrossFiltersSelectionDesc}
</Paragraph>
</Space>
<ButtonOutline onClick={openScheduleDialogClick} width="100%">
<ButtonOutline onClick={updateFiltersClick}>
Test update filters
</ButtonOutline>
<FieldToggleSwitch
label="Run dashboard"
onChange={(event) => setRunDashboard(event.target.checked)}
on={runDashboard}
></FieldToggleSwitch>
<ButtonOutline
onClick={openScheduleDialogClick}
width="100%"
disabled={isExploring || isDashboardEditing}
>
Test open schedule dialog
</ButtonOutline>
<ButtonOutline onClick={updateTileClick} width="100%">
Expand Down
90 changes: 90 additions & 0 deletions react/javascript/tile-sdk/src/utils/utils.js
Original file line number Diff line number Diff line change
@@ -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
}
Loading