/** * Copyright (c) 2026-present, Goldman Sachs * * Licensed 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 { Dialog, Modal, ModalBody, ModalFooter, ModalFooterButton, ModalHeader, TimesIcon, } from '@finos/legend-art'; import { buildCubeRunningStepLambda, CubeButton, CubeEditor, formatDisabledReasons, type CubeEditorState, } from '@finos/legend-cube-builder'; import type { IngestDefinition } from '@finos/legend-graph'; import { noop, type PlainObject } from '@finos/legend-shared'; import { observer } from 'mobx-react-lite'; import { useState } from 'react'; import type { EmbeddedCubeState } from '../../../stores/editor/embedded-cube/EmbeddedCubeState.js'; import { LegendStudioCubeHost } from '../../../stores/editor/embedded-cube/LegendStudioCubeHost.js'; export const EMBEDDED_CUBE_TEST_ID = { DIALOG: 'embedded-cube__dialog', ADD_MATERIALIZED_VIEW: 'embedded-cube__add-materialized-view', SAVE_MATERIALIZED_VIEW: 'embedded-cube__save-materialized-view', }; /** * The classification of the view's data: one of the classifications a data * product's access points offer, or typed when Studio's config gives none */ const MaterializedViewClassification = observer( (props: { embeddedCubeState: EmbeddedCubeState; sensitivity: string; setSensitivity: (sensitivity: string) => void; }) => { const { embeddedCubeState, sensitivity, setSensitivity } = props; const { classifications } = embeddedCubeState; return ( ); }, ); /** * The view's primary key: none or some of the columns it can take, those of * the step that runs but the milestoning ones, kept in the order of the * columns */ const MaterializedViewPrimaryKey: React.FC<{ columns: readonly string[]; primaryKey: readonly string[]; setPrimaryKey: (primaryKey: string[]) => void; }> = ({ columns, primaryKey, setPrimaryKey }) => (
); /** What a header button gives its dialog */ interface MaterializedViewDialogInput { /** The step that runs, as a lambda with no runtime and no row limit */ readonly lambda: PlainObject; /** The columns of the step that runs, the view's; none when Cube doesn't know them */ readonly columns: readonly string[] | undefined; /** Why Save MatView asks again; none to add a new view */ readonly saveProblems?: readonly string[] | undefined; } /** * Names the materialized view, classifies its data and picks its primary * key, then adds it to the ingest definition (Add MatView). For the view * opened to edit, asks again for what it can't be saved with as it was, the * classification of its data or a primary key whose column is gone, then * saves it in place (Save MatView) */ const MaterializedViewDialog = observer( (props: { embeddedCubeState: EmbeddedCubeState; input: MaterializedViewDialogInput; close: () => void; }) => { const { embeddedCubeState, input, close } = props; const { lambda, saveProblems } = input; const saved = saveProblems ? embeddedCubeState.matViewDataSet : undefined; // the columns the key can take; a view saved when Cube doesn't know the // step's columns keeps its key const columns = embeddedCubeState.getMaterializedViewKeyColumns( input.columns ?? saved?.primaryKey ?? [], ); // as Cube's dialogs: the dialog follows the theme, and its primary button // is an accent button in every theme (legend-art's light one is grey) const darkMode = !embeddedCubeState.editorStore.applicationStore.layoutService .TEMPORARY__isLightColorThemeEnabled; const [name, setName] = useState(''); // a view saved again starts on its own classification, if it can keep it, // and on the columns of its key the step still has const [sensitivity, setSensitivity] = useState(() => saved?.sensitivity !== undefined && embeddedCubeState.getMaterializedViewClassificationProblem( saved.sensitivity, ) === undefined ? saved.sensitivity : '', ); const [primaryKey, setPrimaryKey] = useState(() => columns.filter((column) => saved?.primaryKey.includes(column)), ); const nameProblem = saveProblems ? undefined : embeddedCubeState.getMaterializedViewNameProblem(name); const problem = nameProblem ?? embeddedCubeState.getMaterializedViewClassificationProblem(sensitivity); const verb = saveProblems ? 'Save' : 'Add'; const confirm = (): void => { if (problem !== undefined) { return; } if (saveProblems) { embeddedCubeState.saveMaterializedView(lambda, { primaryKey, sensitivity, }); } else { embeddedCubeState.addMaterializedView(name, lambda, { primaryKey, sensitivity, }); } }; return ( ); }, ); /** * Cube's header button: the step that runs, as a lambda with no runtime and * no row limit, becomes a materialized view of the ingest definition (Add * MatView, named, classified and keyed in a dialog), or replaces the one * opened to edit (Save MatView), at once unless its classification or its * key must be asked again */ const MaterializedViewButton = observer( (props: { embeddedCubeState: EmbeddedCubeState; editorState: CubeEditorState; }) => { const { embeddedCubeState, editorState } = props; const { execution } = editorState; const { matView } = embeddedCubeState; const [dialogInput, setDialogInput] = useState< MaterializedViewDialogInput | undefined >(); const [isBuilding, setIsBuilding] = useState(false); const start = (): void => { // the open step editor's edits go in first, as for Execute if (!editorState.nodeEditor.finishApplied()) { return; } // the columns of the step that runs, read with its lambda const selected = editorState.document.query.selected; const columns = ( selected === undefined ? undefined : editorState.analysis.schemas.get(selected) )?.columns.map((column) => column.name); setIsBuilding(true); buildCubeRunningStepLambda(editorState) .then((lambda) => { if (matView === undefined) { setDialogInput({ lambda, columns }); return; } const saveProblems = embeddedCubeState.getMaterializedViewSaveProblems(columns); if (saveProblems.length) { setDialogInput({ lambda, columns, saveProblems }); } else { embeddedCubeState.saveMaterializedView(lambda); } }) .catch(editorState.host.applicationStore.alertUnhandledError) .finally(() => setIsBuilding(false)); }; return ( <>