From f70a913ec67f34ab26037cdb292ece3cffe15e70 Mon Sep 17 00:00:00 2001 From: scespinoza Date: Tue, 15 Jul 2025 14:56:00 -0400 Subject: [PATCH 1/5] feat: implements time complete from tesseract annotations --- src/api/tesseract/parse.ts | 6 ++++- src/components/DrawerMenu.tsx | 50 ++++++++++++++++++++++++++++++++--- src/components/TableView.tsx | 8 +++--- src/context/query.tsx | 6 +++-- src/hooks/permalink.tsx | 2 +- src/state/queries.ts | 14 +++++++++- src/state/utils.ts | 14 ++++++++-- src/utils/structs.ts | 4 ++- 8 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/api/tesseract/parse.ts b/src/api/tesseract/parse.ts index 71a6b07..9ca2b08 100644 --- a/src/api/tesseract/parse.ts +++ b/src/api/tesseract/parse.ts @@ -36,7 +36,8 @@ export function queryParamsToRequest(params: QueryParams): TesseractDataRequest item.active ? filterSerialize(item) : null ).join(","), limit: `${params.pagiLimit || 0},${params.pagiOffset || 0}`, - sort: params.sortKey ? `${params.sortKey}.${params.sortDir}` : undefined + sort: params.sortKey ? `${params.sortKey}.${params.sortDir}` : undefined, + time: params.timeComplete ? `${params.timeComplete}.complete` : undefined // sparse: params.sparse, // ranking: // typeof params.ranking === "boolean" @@ -108,6 +109,8 @@ export function requestToQueryParams(cube: TesseractCube, search: URLSearchParam const [limit = "0", offset = "0"] = (search.get("limit") || "0").split(","); const [sortKey, sortDir] = (search.get("sort") || "").split("."); + const timeComplete = search.get("time")?.split(".")[0] || undefined; + return { cube: cube.name, locale: search.get("locale") || undefined, @@ -120,6 +123,7 @@ export function requestToQueryParams(cube: TesseractCube, search: URLSearchParam sortDir: sortDir === "asc" ? "asc" : "desc", sortKey: sortKey || undefined, isPreview: false, + timeComplete: timeComplete || undefined, booleans: { // parents: search.get("parents") || undefined, } diff --git a/src/components/DrawerMenu.tsx b/src/components/DrawerMenu.tsx index f70a27a..0155615 100644 --- a/src/components/DrawerMenu.tsx +++ b/src/components/DrawerMenu.tsx @@ -327,19 +327,61 @@ function LevelItem({ const paddingLeft = `${5 * depth + 5}px`; const properties = currentDrilldown.properties.length ? currentDrilldown.properties : null; + + const dimensionIsTimeComplete = dimension.annotations.de_time_complete === "true"; return ( currentDrilldown && ( <> - + { actions.updateDrilldown({ ...currentDrilldown, - active: !currentDrilldown.active, + active: !currentDrilldown.active }); - if (cut && cut.members.length > 0) - actions.updateCut({...cut, active: !cut.active}); + if (cut && cut.members.length > 0) actions.updateCut({...cut, active: !cut.active}); + + // if current dimension has time complete annotation + if (dimensionIsTimeComplete) { + const hierarchyLevels = + dimension.hierarchies.find(h => h.name === hierarchy.name)?.levels || []; + + // select all levels that are either active or match the current drilldown level to be added + const availableLevels = hierarchyLevels.filter( + l => + l.name && + activeItems.some(item => + !currentDrilldown.active + ? item.level === l.name || l.name === currentDrilldown.level + : item.level === l.name && item.level !== currentDrilldown.level + ) + ); + + // take the higher order level + const timeCompleteLevel = availableLevels.find( + l => l.depth === Math.min(...availableLevels.map(level => level.depth)) + ); + const highestDepthLevel = hierarchyLevels.find( + l => l.depth === Math.max(...hierarchyLevels.map(level => level.depth)) + ); + if ( + timeCompleteLevel && + highestDepthLevel && + timeCompleteLevel.depth < highestDepthLevel.depth + ) { + actions.updateTimeComplete(timeCompleteLevel.name); + } else { + actions.removeTimeComplete(); + } + } }} checked={checked} label={label} diff --git a/src/components/TableView.tsx b/src/components/TableView.tsx index 3638536..5379004 100644 --- a/src/components/TableView.tsx +++ b/src/components/TableView.tsx @@ -97,7 +97,7 @@ function isColumnSorted(column: string, key: string) { const removeColumn = ( queryItem: QueryItem, - entity: TesseractMeasure | TesseractProperty | TesseractLevel, + entity: TesseractMeasure | TesseractProperty | TesseractLevel ) => { const newQuery = buildQuery(cloneDeep(queryItem)); const params = newQuery.params; @@ -122,15 +122,15 @@ const removeColumn = ( const mapPropertyToDrilldown = Object.fromEntries( Object.values(params.drilldowns) .filter(drilldown => drilldown.active) - .flatMap(drilldown => drilldown.properties.map(prop => [prop.name, drilldown])), + .flatMap(drilldown => drilldown.properties.map(prop => [prop.name, drilldown])) ); const drilldown = mapPropertyToDrilldown[entity.name]; if (drilldown) { params.drilldowns[drilldown.key] = { ...drilldown, properties: drilldown.properties.map(prop => - prop.name === entity.name ? {...prop, active: false} : prop, - ), + prop.name === entity.name ? {...prop, active: false} : prop + ) }; return newQuery; } diff --git a/src/context/query.tsx b/src/context/query.tsx index 58f6047..cd35b61 100644 --- a/src/context/query.tsx +++ b/src/context/query.tsx @@ -183,7 +183,8 @@ export function QueryProvider({children, defaultCube}: QueryProviderProps) { }; function setDefaultValues(cube: TesseractCube) { - const drilldowns = pickDefaultDrilldowns(cube.dimensions, cube).map(level => + const {levels, timeComplete} = pickDefaultDrilldowns(cube.dimensions, cube); + const drilldowns = levels.map(level => buildDrilldown({ ...level, key: level.name, @@ -213,7 +214,8 @@ export function QueryProvider({children, defaultCube}: QueryProviderProps) { cube: cube.name, measures: keyBy(measures, item => item.key), drilldowns: keyBy(drilldowns, item => item.key), - locale + locale, + timeComplete }, panel: panel ?? "table" }); diff --git a/src/hooks/permalink.tsx b/src/hooks/permalink.tsx index 48cd444..6f12a96 100644 --- a/src/hooks/permalink.tsx +++ b/src/hooks/permalink.tsx @@ -1,4 +1,4 @@ -import {useCallback, useEffect} from "react"; +import {useCallback} from "react"; import type {TesseractCube} from "../api"; import {queryParamsToRequest, requestToQueryParams} from "../api/tesseract/parse"; import {selectCurrentQueryItem} from "../state/queries"; diff --git a/src/state/queries.ts b/src/state/queries.ts index 667a46a..c15db8b 100644 --- a/src/state/queries.ts +++ b/src/state/queries.ts @@ -127,6 +127,10 @@ export const queriesSlice = createSlice({ delete query.params.filters[action.payload]; }, + removeTimeComplete(state) { + const query = taintCurrentQuery(state); + delete query.params.timeComplete; + }, /** * Replaces multiple QueryParams for the current QueryItem at once. */ @@ -233,6 +237,15 @@ export const queriesSlice = createSlice({ query.params.drilldowns[payload.key] = payload; }, + /** + * Replaces the timeComplete value in the current QueryItem. + */ + updateTimeComplete(state, {payload}: Action) { + const query = taintCurrentQuery(state); + if (payload !== query.params.timeComplete) { + query.params.timeComplete = payload; + } + }, /** * Replaces a single FilterItem in the current QueryItem. */ @@ -247,7 +260,6 @@ export const queriesSlice = createSlice({ updateLocale(state, {payload}: Action) { const query = state.itemMap[state.current]; if (payload !== query.params.locale) { - // query.isDirty = true; query.params.locale = payload; } }, diff --git a/src/state/utils.ts b/src/state/utils.ts index 6c2a251..ba2e6dd 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -9,6 +9,7 @@ function calcMaxMemberCount(lengths) { */ export function pickDefaultDrilldowns(dimensions: TesseractDimension[], cube: TesseractCube) { const levels: TesseractLevel[] = []; + let timeComplete; let suggestedLevels: string[] = []; for (const key in cube.annotations) { if (key === "suggested_levels") { @@ -22,9 +23,18 @@ export function pickDefaultDrilldowns(dimensions: TesseractDimension[], cube: Te for (const dimension of dimensions) { if (dimension.type === "time" || levels.length < 4) { const hierarchy = findDefaultHierarchy(dimension); + const hierarchyDepth = Math.max(...hierarchy.levels.map(l => l.depth)); // uses deepest level for geo dimensions const levelIndex = dimension.type === "geo" ? hierarchy.levels.length - 1 : 0; - levels.push({...hierarchy.levels[levelIndex], type: dimension.type}); + const defaultLevel = hierarchy.levels[levelIndex]; + if ( + dimension.type === "time" && + dimension.annotations.de_time_complete === "true" && + defaultLevel.depth < hierarchyDepth + ) { + timeComplete = defaultLevel.name; + } + levels.push({...defaultLevel, type: dimension.type}); } } @@ -67,5 +77,5 @@ export function pickDefaultDrilldowns(dimensions: TesseractDimension[], cube: Te totalCount = calcMaxMemberCount(levels.map(l => l.count)); // Recalculate totalCount } - return levels; + return {levels, timeComplete}; } diff --git a/src/utils/structs.ts b/src/utils/structs.ts index fbad758..87125e2 100644 --- a/src/utils/structs.ts +++ b/src/utils/structs.ts @@ -28,6 +28,7 @@ export interface QueryParams { pagiOffset: number; sortDir: "asc" | "desc"; sortKey: string | undefined; + timeComplete: string | undefined; } export interface QueryResult> { @@ -159,7 +160,8 @@ export function buildQueryParams(props): QueryParams { pagiLimit: props.pagiLimit || props.limitAmount || props.limit || 100, pagiOffset: props.pagiOffset || props.limitOffset || props.offset || 0, sortDir: props.sortDir || props.sortDirection || props.sortOrder || props.order || "desc", - sortKey: props.sortKey || props.sortProperty || "" + sortKey: props.sortKey || props.sortProperty || "", + timeComplete: props.timeComplete || undefined }; } From 414a6078557b525430cc158cb80672fd432ea064 Mon Sep 17 00:00:00 2001 From: scespinoza Date: Tue, 15 Jul 2025 15:04:24 -0400 Subject: [PATCH 2/5] refactor: simplify LevelItem component by removing conditional background color --- src/components/DrawerMenu.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/DrawerMenu.tsx b/src/components/DrawerMenu.tsx index 0155615..57e26cd 100644 --- a/src/components/DrawerMenu.tsx +++ b/src/components/DrawerMenu.tsx @@ -332,14 +332,7 @@ function LevelItem({ return ( currentDrilldown && ( <> - + { From bec3a1b611c53e5a6a15b41edc9dae7d0476e1ed Mon Sep 17 00:00:00 2001 From: scespinoza Date: Tue, 15 Jul 2025 15:25:06 -0400 Subject: [PATCH 3/5] fix: update time completion logic to consider deepest level availability --- src/components/DrawerMenu.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/DrawerMenu.tsx b/src/components/DrawerMenu.tsx index 57e26cd..fd13f0b 100644 --- a/src/components/DrawerMenu.tsx +++ b/src/components/DrawerMenu.tsx @@ -362,13 +362,19 @@ function LevelItem({ const timeCompleteLevel = availableLevels.find( l => l.depth === Math.min(...availableLevels.map(level => level.depth)) ); - const highestDepthLevel = hierarchyLevels.find( + const deepestLevel = hierarchyLevels.find( l => l.depth === Math.max(...hierarchyLevels.map(level => level.depth)) ); + + const deepestLevelAvailable = availableLevels.find( + l => l.depth === deepestLevel?.depth + ); + if ( timeCompleteLevel && - highestDepthLevel && - timeCompleteLevel.depth < highestDepthLevel.depth + deepestLevel && + timeCompleteLevel.depth < deepestLevel.depth && + !deepestLevelAvailable ) { actions.updateTimeComplete(timeCompleteLevel.name); } else { From 175363d4a2193857c38ebdfcd13852aa538b7133 Mon Sep 17 00:00:00 2001 From: "Pablo H. Paladino" Date: Tue, 15 Jul 2025 17:39:26 -0300 Subject: [PATCH 4/5] include blank to source link --- src/components/CubeSource.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/CubeSource.tsx b/src/components/CubeSource.tsx index f4791b6..030d7ef 100644 --- a/src/components/CubeSource.tsx +++ b/src/components/CubeSource.tsx @@ -2,11 +2,10 @@ import {Anchor, Text, TextProps} from "@mantine/core"; import React from "react"; import {useSelector} from "react-redux"; import {useTranslation} from "../hooks/translation"; -import {selectOlapCube} from "../state/selectors"; import {getAnnotation} from "../utils/string"; -import {selectCurrentQueryItem, selectLocale} from "../state/queries"; +import {selectLocale} from "../state/queries"; import type {Annotated} from "../utils/types"; -import {useSelectedItem, useServerSchema} from "../hooks/useQueryApi"; +import {useSelectedItem} from "../hooks/useQueryApi"; export function CubeAnnotation( props: TextProps & { @@ -42,14 +41,13 @@ export function CubeSourceAnchor( return ( {`${t("params.label_source")}: `} - {srcLink ? {srcName} : {srcName}} + {srcLink ? {srcName} : {srcName}} ); } export default function CubeSource() { const selectedItem = useSelectedItem(); - // TODO: agregar locale const {code: locale} = useSelector(selectLocale); return ( selectedItem && ( From f55820461e483fd51c5bb9c85801fe9f2df016c3 Mon Sep 17 00:00:00 2001 From: "Pablo H. Paladino" Date: Tue, 15 Jul 2025 17:39:41 -0300 Subject: [PATCH 5/5] clean up legacy code --- src/hooks/useQueryApi.ts | 258 +---------------------- src/state/index.ts | 4 - src/state/thunks.ts | 436 --------------------------------------- 3 files changed, 2 insertions(+), 696 deletions(-) delete mode 100644 src/state/thunks.ts diff --git a/src/hooks/useQueryApi.ts b/src/hooks/useQueryApi.ts index d6b0181..dd8d6f7 100644 --- a/src/hooks/useQueryApi.ts +++ b/src/hooks/useQueryApi.ts @@ -1,26 +1,20 @@ -import {useQuery, useMutation, useQueryClient, keepPreviousData} from "@tanstack/react-query"; +import {useQuery, useMutation, keepPreviousData} from "@tanstack/react-query"; import type { TesseractCube, TesseractDataResponse, TesseractFormat, - TesseractMembersResponse } from "../api"; -import type {TesseractLevel, TesseractHierarchy, TesseractDimension} from "../api/tesseract/schema"; -import {queryParamsToRequest, requestToQueryParams} from "../api/tesseract/parse"; -import {mapDimensionHierarchyLevels} from "../api/traverse"; +import {queryParamsToRequest} from "../api/tesseract/parse"; import {filterMap} from "../utils/array"; import {describeData, getOrderValue, getValues} from "../utils/object"; import { buildDrilldown, - buildMeasure, buildProperty, - buildQuery, QueryParams } from "../utils/structs"; import {keyBy} from "../utils/transform"; import type {FileDescriptor} from "../utils/types"; import {isValidQuery} from "../utils/validation"; -import {pickDefaultDrilldowns} from "../state/utils"; import {useLogicLayer} from "../api/context"; import {useSettings} from "./settings"; import {useSelector} from "../state"; @@ -255,251 +249,3 @@ export function useFetchQuery( placeholderData: withoutPagination ? undefined : keepPreviousData }); } - -// Hook to fetch members for a level -export function useMembers(level: string, localeStr?: string, cubeName?: string) { - const {tesseract} = useLogicLayer(); - - return useQuery({ - queryKey: ["members", level, localeStr, cubeName], - queryFn: async (): Promise => { - return tesseract.fetchMembers({ - request: {cube: cubeName || "", level, locale: localeStr} - }); - }, - enabled: !!level - }); -} - -// Hook to hydrate params -export function useHydrateParams( - cubeMap: Record, - queries: Array<{ - key: string; - params: { - cube: string; - measures: Record; - drilldowns: Record< - string, - {level: string; properties: Array<{active: boolean; name: string}>} - >; - }; - }>, - suggestedCube = "" -) { - const queryClient = useQueryClient(); - const {tesseract} = useLogicLayer(); - - function isCompleteTuple( - tuple: [TesseractLevel, TesseractHierarchy, TesseractDimension] | undefined - ): tuple is [TesseractLevel, TesseractHierarchy, TesseractDimension] { - return tuple !== undefined && tuple.length === 3 && tuple.every(item => item !== undefined); - } - - return useMutation({ - mutationFn: async () => { - const defaultCube = cubeMap[suggestedCube] || Object.values(cubeMap)[0]; - - const queryPromises = queries.map(queryItem => { - const {params} = queryItem; - const {measures: measureItems} = params; - - const cube = cubeMap[params.cube] || defaultCube; - const levelMap = mapDimensionHierarchyLevels(cube); - - const resolvedMeasures = cube.measures.map(measure => - buildMeasure( - measureItems[measure.name] || { - active: false, - key: measure.name, - name: measure.name, - caption: measure.caption - } - ) - ); - - const resolvedDrilldowns = filterMap( - Object.values(params.drilldowns), - (item: {level: string; properties: Array<{active: boolean; name: string}>}) => { - const levelTuple = levelMap[item.level]; - if (!isCompleteTuple(levelTuple)) return null; - - const [level, hierarchy, dimension] = levelTuple!; - - const activeProperties = filterMap( - item.properties, - (prop: {active: boolean; name: string}) => (prop.active ? prop.name : null) - ); - return buildDrilldown({ - active: true, - key: level.name, - dimension: dimension.name!, - hierarchy: hierarchy.name!, - level: level.name, - captionProperty: "", - members: [], - properties: level.properties.map(property => - buildProperty({ - active: activeProperties.includes(property.name), - level: level.name, - name: property.name - }) - ) - }); - } - ); - - return { - ...queryItem, - params: { - ...params, - cube: cube.name, - drilldowns: keyBy(resolvedDrilldowns, item => item.key), - measures: keyBy(resolvedMeasures, item => item.key) - } - }; - }); - - const resolvedQueries = await Promise.all(queryPromises); - return keyBy(resolvedQueries, i => i.key); - }, - onSuccess: queryMap => { - // Update the query client cache with the hydrated queries - queryClient.setQueryData(["hydratedQueries"], queryMap); - } - }); -} - -// Hook to parse query URL -export function useParseQueryUrl() { - const queryClient = useQueryClient(); - const {tesseract} = useLogicLayer(); - - return useMutation({ - mutationFn: async ({ - url, - cubeMap - }: { - url: string | URL; - cubeMap: Record; - }) => { - const search = new URL(url).searchParams; - const cube = search.get("cube"); - - if (cube && cubeMap[cube]) { - const params = requestToQueryParams(cubeMap[cube], search); - - const queryItem = buildQuery({ - panel: search.get("panel") || "table", - chart: search.get("chart") || "", - params - }); - - return queryItem; - } - - return null; - }, - onSuccess: queryItem => { - if (queryItem) { - // Update the query client cache - queryClient.setQueryData(["currentQuery"], queryItem); - queryClient.setQueryData(["selectedQuery"], queryItem.key); - } - } - }); -} - -// Hook to reload cubes -export function useReloadCubes() { - const {tesseract} = useLogicLayer(); - - return useMutation({ - mutationFn: async ({locale}: {locale: {code: string}}) => { - const schema = await tesseract.fetchSchema({locale: locale.code}); - const cubes = schema.cubes.filter(cube => !cube.annotations.hide_in_ui); - return keyBy(cubes, i => i.name); - }, - onSuccess: cubeMap => { - // Update the query client cache - const queryClient = useQueryClient(); - queryClient.setQueryData(["cubeMap"], cubeMap); - } - }); -} - -// Hook to set cube -export function useSetCube() { - const queryClient = useQueryClient(); - const {tesseract, dataLocale} = useLogicLayer(); - - return useMutation({ - mutationFn: async ({ - cubeName, - cubeMap, - measuresActive - }: { - cubeName: string; - cubeMap: Record; - measuresActive?: number; - }) => { - const nextCube = cubeMap[cubeName]; - if (!nextCube) return null; - - const measuresLimit = - typeof measuresActive !== "undefined" ? measuresActive : nextCube.measures.length; - - const nextMeasures = nextCube.measures.slice(0, measuresLimit).map(measure => { - return buildMeasure({ - active: true, - key: measure.name, - name: measure.name, - caption: measure.caption - }); - }); - - const nextDrilldowns = pickDefaultDrilldowns(nextCube.dimensions).map(level => - buildDrilldown({ - ...level, - key: level.name, - active: true, - properties: level.properties.map(prop => - buildProperty({level: level.name, name: prop.name}) - ) - }) - ); - - // Fetch members for each drilldown - const drilldownPromises = nextDrilldowns.map(async dd => { - const levelMeta = await tesseract.fetchMembers({ - request: {cube: nextCube.name, level: dd.level, locale: dataLocale} - }); - - return { - ...dd, - members: levelMeta.members - }; - }); - - const drilldownsWithMembers = await Promise.all(drilldownPromises); - - return { - cube: nextCube.name, - measures: keyBy(nextMeasures, item => item.key), - drilldowns: keyBy(drilldownsWithMembers, item => item.key), - locale: dataLocale - }; - }, - onSuccess: result => { - if (result) { - // Update the query client cache - queryClient.setQueryData(["currentCube"], result.cube); - queryClient.setQueryData(["measures"], result.measures); - queryClient.setQueryData(["drilldowns"], result.drilldowns); - if (result.locale) { - queryClient.setQueryData(["locale"], result.locale); - } - } - } - }); -} diff --git a/src/state/index.ts b/src/state/index.ts index 90f5d6b..fe54c38 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,6 +1,4 @@ import {queriesActions} from "./queries"; -// import {serverActions} from "./server"; -// import * as thunks from "./thunks"; export type {QueriesState} from "./queries"; export type {ServerState} from "./server"; @@ -17,5 +15,3 @@ export {queriesActions}; export type ExplorerActionMap = typeof actions; export const actions = queriesActions; - -// TODO: Remove thunks diff --git a/src/state/thunks.ts b/src/state/thunks.ts deleted file mode 100644 index f09c8dc..0000000 --- a/src/state/thunks.ts +++ /dev/null @@ -1,436 +0,0 @@ -import type { - TesseractCube, - TesseractDataResponse, - TesseractFormat, - TesseractMembersResponse -} from "../api"; -import {queryParamsToRequest, requestToQueryParams} from "../api/tesseract/parse"; -import {mapDimensionHierarchyLevels} from "../api/traverse"; -import {filterMap} from "../utils/array"; -import {describeData} from "../utils/object"; -import { - type QueryResult, - buildCut, - buildDrilldown, - buildMeasure, - buildProperty, - buildQuery -} from "../utils/structs"; -import {keyBy} from "../utils/transform"; -import type {FileDescriptor} from "../utils/types"; -import {isValidQuery, noop} from "../utils/validation"; -import {loadingActions} from "./loading"; -import { - queriesActions, - selectCubeName, - selectCurrentQueryParams, - selectLocale, - selectQueryItems -} from "./queries"; -import {selectOlapCubeMap, serverActions} from "./server"; -import type {ExplorerThunk} from "./store"; -import {pickDefaultDrilldowns} from "./utils"; - -/** - * Initiates a new download of the queried data by the current parameters. - * - * @param format - The format the user wants the data to be. - * @returns A blob with the data contents, in the request format. - */ -export function willDownloadQuery( - format: `${TesseractFormat}` -): ExplorerThunk> { - return (dispatch, getState, {tesseract}) => { - const state = getState(); - const params = selectCurrentQueryParams(state); - - if (!isValidQuery(params)) { - return Promise.reject(new Error("The current query is not valid.")); - } - - const queryParams = {...params, pagiLimit: 0, pagiOffset: 0}; - return tesseract - .fetchData({request: queryParamsToRequest(queryParams), format}) - .then(response => response.blob()) - .then(result => ({ - content: result, - extension: format.replace(/json\w+/, "json"), - name: `${params.cube}_${new Date().toISOString()}` - })); - }; -} - -/** - * Takes the current parameters, and queries the OLAP server for data with them. - * The result is stored in QueryItem["result"]. - * This operation does not activate the Loading overlay in the UI; you must use - * `willRequestQuery()` for that. - */ -export function willExecuteQuery(params?: { - limit?: number; - offset?: number; -}): ExplorerThunk> { - const {limit = 0, offset = 0} = params || {}; - return dispatch => { - return dispatch(willFetchQuery(params)).then( - result => { - dispatch(queriesActions.updateResult(result)); - }, - error => { - if (error.name === "TypeError" && error.message.includes("NetworkError")) { - console.error("Network error or CORS error occurred:", error); - } else if (error.message.includes("Unexpected token")) { - console.error("Syntax error while parsing JSON:", error); - } else if (error.message.includes("Backend Error")) { - console.error("Server returned an error response:", error); - } else { - console.error("An unknown error occurred:", error); - } - dispatch( - queriesActions.updateResult({ - data: [], - types: {}, - page: {limit, offset, total: 0}, - status: 0, - url: "" - }) - ); - } - ); - }; -} - -export function willFetchQuery(params?: { - limit?: number; - offset?: number; - withoutPagination?: boolean; -}): ExplorerThunk> { - const {limit = 0, offset = 0} = params || {}; - return (dispatch, getState, {tesseract}) => { - const state = getState(); - const queryParams = selectCurrentQueryParams(state); - const cube = selectOlapCubeMap(state)[queryParams.cube]; - - if (!isValidQuery(queryParams) || !cube) { - return Promise.reject(new Error("Invalid query")); - } - - const request = queryParamsToRequest(queryParams); - if (limit || offset) { - request.limit = `${limit},${offset}`; - } else if (params?.withoutPagination) { - request.limit = "0,0"; - } - - return tesseract.fetchData({request, format: "jsonrecords"}).then(response => - response.json().then((content: TesseractDataResponse) => { - if (!response.ok) { - throw new Error(`Backend Error: ${content.detail}`); - } - return { - data: content.data, - page: content.page, - types: describeData(cube, queryParams, content), - headers: Object.fromEntries(response.headers), - status: response.status || 200, - url: response.url - }; - }) - ); - }; -} - -/** - * Requests the list of associated Members for a certain Level. - * - * @param level - The name of the Level for whom we want to retrieve members. - * @returns The list of members for the requested level. - */ -export function willFetchMembers( - level: string, - localeStr?: string, - cubeName?: string -): ExplorerThunk> { - return (dispatch, getState, {tesseract}) => { - const state = getState(); - const cube = selectCubeName(state); - const locale = selectLocale(state); - - return tesseract.fetchMembers({ - request: {cube: cubeName || cube, level, locale: localeStr || locale.code} - }); - }; -} - -/** - * Checks the state of the current QueryParams and fills missing information. - * - * @param suggestedCube The cube to resolve the missing data from. - */ -export function willHydrateParams(suggestedCube = ""): ExplorerThunk> { - return (dispatch, getState) => { - const state = getState(); - const cubeMap = selectOlapCubeMap(state); - const queries = selectQueryItems(state); - - const defaultCube = cubeMap[suggestedCube] || Object.values(cubeMap)[0]; - - const queryPromises = queries.map(queryItem => { - const {params} = queryItem; - const {measures: measureItems} = params; - - const cube = cubeMap[params.cube] || defaultCube; - const levelMap = mapDimensionHierarchyLevels(cube); - - const resolvedMeasures = cube.measures.map(measure => - buildMeasure( - measureItems[measure.name] || { - active: false, - key: measure.name, - name: measure.name, - caption: measure.caption - } - ) - ); - - const resolvedDrilldowns = filterMap(Object.values(params.drilldowns), item => { - const [level, hierarchy, dimension] = levelMap[item.level] || []; - if (!level) return null; - const activeProperties = filterMap(item.properties, prop => - prop.active ? prop.name : null - ); - return level - ? buildDrilldown({ - ...item, - key: level.name, - dimension: dimension.name, - hierarchy: hierarchy.name, - properties: level.properties.map(property => - buildProperty({ - active: activeProperties.includes(property.name), - level: level.name, - name: property.name - }) - ) - }) - : null; - }); - - return { - ...queryItem, - params: { - ...params, - locale: params.locale || state.explorerServer.locale, - cube: cube.name, - drilldowns: keyBy(resolvedDrilldowns, item => item.key), - measures: keyBy(resolvedMeasures, item => item.key) - } - }; - }); - - return Promise.all(queryPromises).then(resolvedQueries => { - const queryMap = keyBy(resolvedQueries, i => i.key); - dispatch(queriesActions.resetQueries(queryMap)); - }); - }; -} - -/** - * Parses the search parameters in an URL to create a QueryParam object, - * then creates a new QueryItem in the UI containing it. - */ -export function willParseQueryUrl(url: string | URL): ExplorerThunk> { - return async (dispatch, getState) => { - const state = getState(); - const cubeMap = selectOlapCubeMap(state); - - const search = new URL(url).searchParams; - const cube = search.get("cube"); - if (cube && cubeMap[cube]) { - const params = requestToQueryParams(cubeMap[cube], search); - // const promises = Object.values(params.drilldowns).map(dd => { - // return dispatch(willFetchMembers(dd.level)).then(levelMeta => { - // dispatch(queriesActions.updateCut(buildCut({...dd, active: true}))); - // return { - // ...dd, - // members: levelMeta.members - // }; - // }); - // }); - - // const dds = await Promise.all(promises); - - const queryItem = buildQuery({ - panel: search.get("panel") || "table", - chart: search.get("chart") || "", - // params: {...params, drilldowns: keyBy(dds, "key")} - params - }); - - dispatch(queriesActions.updateQuery(queryItem)); - dispatch(queriesActions.selectQuery(queryItem.key)); - } - - return Promise.resolve(); - }; -} - -/** - * Performs a full replacement of the cubes stored in the state with fresh data - * from the server. - */ - -export function willReloadCubes(params?: {locale: {code: string}}): ExplorerThunk< - Promise<{[k: string]: TesseractCube}> -> { - const {locale} = params || {}; - return (dispatch, getState, {tesseract}) => { - const state = getState(); - const newLocale = locale || selectLocale(state); - - return tesseract.fetchSchema({locale: newLocale.code}).then(schema => { - const cubes = schema.cubes.filter(cube => !cube.annotations.hide_in_ui); - const cubeMap = keyBy(cubes, i => i.name); - dispatch(serverActions.updateServer({cubeMap})); - return cubeMap; - }); - }; -} - -/** - * Executes the full Query request procedure, including the calls to activate - * the loading overlay. - */ -export function willRequestQuery(): ExplorerThunk> { - return (dispatch, getState) => { - const state = getState(); - const params = selectCurrentQueryParams(state); - - if (!isValidQuery(params)) return Promise.resolve(); - - dispatch(loadingActions.setLoadingState("FETCHING")); - return dispatch(willExecuteQuery()).then( - () => { - dispatch(loadingActions.setLoadingState("SUCCESS")); - }, - error => { - dispatch(loadingActions.setLoadingState("FAILURE", error.message)); - } - ); - }; -} - -/** - * Changes the current cube and updates related state - * If the new cube contains a measure with the same name as a measure in the - * previous cube, keep its state. - * - * @param cubeName The name of the cube we intend to switch to. - */ -export function willSetCube( - cubeName: string, - measuresActive?: number, - locale?: string -): ExplorerThunk> { - return (dispatch, getState) => { - const state = getState(); - - const cubeMap = selectOlapCubeMap(state); - const nextCube = cubeMap[cubeName]; - if (!nextCube) return Promise.resolve(); - - const measuresLimit = - typeof measuresActive !== "undefined" ? measuresActive : nextCube.measures.length; - - const nextMeasures = nextCube.measures.slice(0, measuresLimit).map(measure => { - return buildMeasure({ - active: true, - key: measure.name, - name: measure.name, - caption: measure.caption - }); - }); - - const nextDrilldowns = pickDefaultDrilldowns(nextCube.dimensions).map(level => - buildDrilldown({ - ...level, - key: level.name, - active: true, - properties: level.properties.map(prop => - buildProperty({level: level.name, name: prop.name}) - ) - }) - ); - - locale && dispatch(queriesActions.updateLocale(locale)); - - dispatch( - queriesActions.updateCube({ - cube: nextCube.name, - measures: keyBy(nextMeasures, item => item.key), - drilldowns: keyBy(nextDrilldowns, item => item.key) - }) - ); - - const promises = nextDrilldowns.map(dd => { - return dispatch(willFetchMembers(dd.level, locale)).then(levelMeta => { - dispatch( - queriesActions.updateDrilldown({ - ...dd, - members: levelMeta.members - }) - ); - dispatch(queriesActions.updateCut(buildCut({...dd, active: false}))); - }); - }); - - return Promise.all(promises).then(noop); - }; -} - -export function willReloadCube({locale}: {locale: {code: string}}): ExplorerThunk> { - return (dispatch, getState) => { - const state = getState(); - const cubeName = selectCubeName(state); - - return dispatch(willSetCube(cubeName, undefined, locale.code)); - }; -} -/** - * Sets the necessary info for the client instance to be able to connect to the - * server, then loads the base data from its schema. - */ -export function willSetupClient( - baseURL: string, - defaultLocale?: string, - requestConfig?: RequestInit -): ExplorerThunk> { - return (dispatch, getState, {tesseract}) => { - tesseract.baseURL = baseURL.replace(/\/?$/, '/'); - const state = getState(); - const search = new URLSearchParams(location.search); - const locale = search.get("locale"); - Object.assign(tesseract.requestConfig, requestConfig || {headers: new Headers()}); - - return tesseract.fetchSchema({locale: locale || defaultLocale}).then( - schema => { - const cubes = schema.cubes.filter(cube => !cube.annotations.hide_in_ui); - const cubeMap = keyBy(cubes, "name"); - dispatch( - serverActions.updateServer({ - cubeMap, - locale: defaultLocale || schema.default_locale, - localeOptions: schema.locales, - online: true, - url: baseURL - }) - ); - return cubeMap; - }, - error => { - dispatch(serverActions.updateServer({online: false, url: baseURL})); - throw error; - } - ); - }; -}