From b13eb03a30277b0067c01b550da913f7f36ffa6c Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 30 Jun 2025 09:53:30 +0200 Subject: [PATCH 1/9] WIP --- .../dataset/dataset_settings_context.tsx | 48 ++ .../dataset/dataset_settings_data_tab.tsx | 15 +- .../dataset/dataset_settings_delete_tab.tsx | 13 +- .../dataset/dataset_settings_provider.tsx | 377 ++++++++++++++++ .../dataset/dataset_settings_sharing_tab.tsx | 14 +- .../dataset/dataset_settings_view.tsx | 416 ++---------------- .../dataset_settings_viewconfig_tab.tsx | 5 +- frontend/javascripts/router.tsx | 15 +- 8 files changed, 483 insertions(+), 420 deletions(-) create mode 100644 frontend/javascripts/dashboard/dataset/dataset_settings_context.tsx create mode 100644 frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_context.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_context.tsx new file mode 100644 index 00000000000..82a09e053a0 --- /dev/null +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_context.tsx @@ -0,0 +1,48 @@ +import type { FormInstance } from "antd"; +import { createContext, useContext } from "react"; +import type { APIDataSource, APIDataset } from "types/api_types"; +import type { DatasetConfiguration } from "viewer/store"; +import type { DatasetRotationAndMirroringSettings } from "./dataset_rotation_form_item"; + +export type FormData = { + dataSource: APIDataSource; + dataSourceJson: string; + dataset: APIDataset; + defaultConfiguration: DatasetConfiguration; + defaultConfigurationLayersJson: string; + datasetRotation?: DatasetRotationAndMirroringSettings; +}; + + +export type DatasetSettingsContextValue = { + form: FormInstance; + isLoading: boolean; + hasUnsavedChanges: boolean; + dataset: APIDataset | null | undefined; + datasetId: string; + datasetDefaultConfiguration: DatasetConfiguration | null | undefined; + activeDataSourceEditMode: "simple" | "advanced"; + savedDataSourceOnServer: APIDataSource | null | undefined; + isEditingMode: boolean; + onComplete: () => void; + onCancel: () => void; + handleSubmit: () => void; + handleCancel: () => void; + handleDataSourceEditModeChange: (activeEditMode: "simple" | "advanced") => void; + onValuesChange: (changedValues: FormData, allValues: FormData) => void; + getFormValidationSummary: () => Record; +}; + +export const DatasetSettingsContext = createContext( + undefined, +); + +export const useDatasetSettingsContext = () => { + const context = useContext(DatasetSettingsContext); + if (!context) { + throw new Error( + "useDatasetSettingsContext must be used within a DatasetSettingsProvider", + ); + } + return context; +}; diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx index 9fb89dfb133..947351db34d 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx @@ -1,6 +1,5 @@ import { CopyOutlined, DeleteOutlined } from "@ant-design/icons"; import { getDatasetNameRules, layerNameRules } from "admin/dataset/dataset_components"; -import { useStartAndPollJob } from "admin/job/job_hooks"; import { startFindLargestSegmentIdJob } from "admin/rest_api"; import { Button, @@ -16,6 +15,7 @@ import { Switch, Tooltip, } from "antd"; +import { useStartAndPollJob } from "admin/job/job_hooks"; import { FormItemWithInfo, Hideable, @@ -35,11 +35,12 @@ import { AllUnits, LongUnitToShortUnitMap, type Vector3 } from "viewer/constants import { getSupportedValueRangeForElementClass } from "viewer/model/bucket_data_handling/data_rendering_logic"; import type { BoundingBoxObject } from "viewer/store"; import { AxisRotationSettingForDataset } from "./dataset_rotation_form_item"; +import { useDatasetSettingsContext } from "./dataset_settings_context"; const FormItem = Form.Item; export const syncDataSourceFields = ( - form: FormInstance, + form: FormInstance, // Keep form as a prop for this utility function syncTargetTabKey: "simple" | "advanced", // Syncing the dataset name is optional as this is needed for the add remote view, but not for the edit view. // In the edit view, the datasource.id fields should never be changed and the backend will automatically ignore all changes to the id field. @@ -78,16 +79,14 @@ export const syncDataSourceFields = ( }; export default function DatasetSettingsDataTab({ - form, - activeDataSourceEditMode, onChange, - dataset, }: { - form: FormInstance; - activeDataSourceEditMode: "simple" | "advanced"; + // Form is passed explicitly because syncDataSourceFields is a utility function + // that needs the form instance, and it's called from the parent. + // The rest of the props are now derived from context. onChange: (arg0: "simple" | "advanced") => void; - dataset?: APIDataset | null | undefined; }) { + const { dataset, form, activeDataSourceEditMode } = useDatasetSettingsContext(); // Using the return value of useWatch for the `dataSource` var // yields outdated values. Therefore, the hook only exists for listening. Form.useWatch("dataSource", form); diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx index a18ab33ad2a..62a8f9b8e15 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx @@ -1,23 +1,20 @@ import { useQueryClient } from "@tanstack/react-query"; -import { deleteDatasetOnDisk, getDataset } from "admin/rest_api"; +import { deleteDatasetOnDisk } from "admin/rest_api"; import { Button } from "antd"; -import { useFetch } from "libs/react_helpers"; import Toast from "libs/toast"; import messages from "messages"; import { useState } from "react"; import { useHistory } from "react-router-dom"; +import { useDatasetSettingsContext } from "./dataset_settings_context"; import { confirmAsync } from "./helper_components"; -type Props = { - datasetId: string; -}; - -const DatasetSettingsDeleteTab = ({ datasetId }: Props) => { +const DatasetSettingsDeleteTab = () => { + const { dataset } = useDatasetSettingsContext(); const [isDeleting, setIsDeleting] = useState(false); const queryClient = useQueryClient(); const history = useHistory(); - const dataset = useFetch(() => getDataset(datasetId), null, [datasetId]); + // const dataset = useFetch(() => getDataset(datasetId), null, [datasetId]); async function handleDeleteButtonClicked(): Promise { if (!dataset) { diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx new file mode 100644 index 00000000000..fa80065696d --- /dev/null +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx @@ -0,0 +1,377 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { + getDataset, + getDatasetDefaultConfiguration, + readDatasetDatasource, + sendAnalyticsEvent, + updateDatasetDatasource, + updateDatasetDefaultConfiguration, + updateDatasetPartial, + updateDatasetTeams, +} from "admin/rest_api"; +import { Form } from "antd"; +import dayjs from "dayjs"; +import type { UnregisterCallback } from "history"; +import { handleGenericError } from "libs/error_handling"; +import Toast from "libs/toast"; +import { jsonStringify } from "libs/utils"; +import _ from "lodash"; +import messages from "messages"; +import { useCallback, useEffect, useRef, useState } from "react"; +import type { APIDataSource, APIDataset, MutableAPIDataset } from "types/api_types"; +import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults"; +import { + EXPECTED_TRANSFORMATION_LENGTH, + doAllLayersHaveTheSameRotation, + getRotationSettingsFromTransformationIn90DegreeSteps, +} from "viewer/model/accessors/dataset_layer_transformation_accessor"; +import type { DatasetConfiguration } from "viewer/store"; +import type { DatasetRotationAndMirroringSettings } from "./dataset_rotation_form_item"; +import { + DatasetSettingsContext, + type DatasetSettingsContextValue, +} from "./dataset_settings_context"; +import { syncDataSourceFields } from "./dataset_settings_data_tab"; +import type { FormData } from "./dataset_settings_context"; +import { hasFormError } from "./helper_components"; +import useBeforeUnload from "./useBeforeUnload_hook"; + +type DatasetSettingsProviderProps = { + children: React.ReactNode; + datasetId: string; + isEditingMode: boolean; + onComplete: () => void; + onCancel: () => void; +}; + +export const DatasetSettingsProvider: React.FC = ({ + children, + datasetId, + isEditingMode, + onComplete, + onCancel, +}) => { + const [form] = Form.useForm(); + const queryClient = useQueryClient(); + + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [dataset, setDataset] = useState(null); + const [datasetDefaultConfiguration, setDatasetDefaultConfiguration] = useState< + DatasetConfiguration | null | undefined + >(null); + const [isLoading, setIsLoading] = useState(true); + const [activeDataSourceEditMode, setActiveDataSourceEditMode] = useState<"simple" | "advanced">( + "simple", + ); + const [savedDataSourceOnServer, setSavedDataSourceOnServer] = useState< + APIDataSource | null | undefined + >(null); + + const unblockRef = useRef(null); + const blockTimeoutIdRef = useRef(null); + + const unblockHistory = useCallback(() => { + window.onbeforeunload = null; + + if (blockTimeoutIdRef.current != null) { + clearTimeout(blockTimeoutIdRef.current); + blockTimeoutIdRef.current = null; + } + + if (unblockRef.current != null) { + unblockRef.current(); + unblockRef.current = null; + } + }, []); + + const fetchData = useCallback(async (): Promise => { + try { + setIsLoading(true); + let fetchedDataset = await getDataset(datasetId); + const dataSource = await readDatasetDatasource(fetchedDataset); + + setSavedDataSourceOnServer(dataSource); + + if (dataSource == null) { + throw new Error("No datasource received from server."); + } + + if (fetchedDataset.dataSource.status?.includes("Error")) { + const datasetClone = _.cloneDeep(fetchedDataset) as any as MutableAPIDataset; + datasetClone.dataSource.status = fetchedDataset.dataSource.status; + fetchedDataset = datasetClone as APIDataset; + } + + form.setFieldsValue({ + dataSourceJson: jsonStringify(dataSource), + dataset: { + name: fetchedDataset.name, + isPublic: fetchedDataset.isPublic || false, + description: fetchedDataset.description || undefined, + allowedTeams: fetchedDataset.allowedTeams || [], + sortingKey: dayjs(fetchedDataset.sortingKey), + }, + }); + + form.setFieldsValue({ + dataSource, + }); + + if (doAllLayersHaveTheSameRotation(dataSource.dataLayers)) { + const firstLayerTransformations = dataSource.dataLayers[0].coordinateTransformations; + let initialDatasetRotationSettings: DatasetRotationAndMirroringSettings; + if ( + !firstLayerTransformations || + firstLayerTransformations.length !== EXPECTED_TRANSFORMATION_LENGTH + ) { + const nulledSetting = { rotationInDegrees: 0, isMirrored: false }; + initialDatasetRotationSettings = { x: nulledSetting, y: nulledSetting, z: nulledSetting }; + } else { + initialDatasetRotationSettings = { + x: getRotationSettingsFromTransformationIn90DegreeSteps( + firstLayerTransformations[1], + "x", + ), + y: getRotationSettingsFromTransformationIn90DegreeSteps( + firstLayerTransformations[2], + "y", + ), + z: getRotationSettingsFromTransformationIn90DegreeSteps( + firstLayerTransformations[3], + "z", + ), + }; + } + form.setFieldsValue({ + datasetRotation: initialDatasetRotationSettings, + }); + } + + const fetchedDatasetDefaultConfiguration = await getDatasetDefaultConfiguration(datasetId); + enforceValidatedDatasetViewConfiguration( + fetchedDatasetDefaultConfiguration, + fetchedDataset, + true, + ); + form.setFieldsValue({ + defaultConfiguration: fetchedDatasetDefaultConfiguration, + defaultConfigurationLayersJson: JSON.stringify( + fetchedDatasetDefaultConfiguration.layers, + null, + " ", + ), + }); + + setDatasetDefaultConfiguration(fetchedDatasetDefaultConfiguration); + setDataset(fetchedDataset); + } catch (error) { + handleGenericError(error as Error); + } finally { + setIsLoading(false); + form.validateFields(); + } + }, [datasetId, form]); + + const getFormValidationSummary = useCallback((): Record => { + const err = form.getFieldsError(); + const formErrors: Record = {}; + + if (!err || !dataset) { + return formErrors; + } + + const hasErr = hasFormError; + + if (hasErr(err, "dataSource") || hasErr(err, "dataSourceJson")) { + formErrors.data = true; + } + + if (hasErr(err, "dataset")) { + formErrors.general = true; + } + + if (hasErr(err, "defaultConfiguration") || hasErr(err, "defaultConfigurationLayersJson")) { + formErrors.defaultConfig = true; + } + + return formErrors; + }, [form, dataset]); + + const didDatasourceChange = useCallback( + (dataSource: Record) => { + return !_.isEqual(dataSource, savedDataSourceOnServer || {}); + }, + [savedDataSourceOnServer], + ); + + const didDatasourceIdChange = useCallback( + (dataSource: Record) => { + const savedDatasourceId = savedDataSourceOnServer?.id; + if (!savedDatasourceId) { + return false; + } + return ( + savedDatasourceId.name !== dataSource.id.name || + savedDatasourceId.team !== dataSource.id.team + ); + }, + [savedDataSourceOnServer], + ); + + const isOnlyDatasourceIncorrectAndNotEdited = useCallback(() => { + const validationSummary = getFormValidationSummary(); + + if (_.size(validationSummary) === 1 && validationSummary.data) { + try { + const dataSource = JSON.parse(form.getFieldValue("dataSourceJson")); + const didNotEditDatasource = !didDatasourceChange(dataSource); + return didNotEditDatasource; + } catch (_e) { + return false; + } + } + + return false; + }, [getFormValidationSummary, form, didDatasourceChange]); + + const submitForm = useCallback( + async (formValues: FormData) => { + const datasetChangeValues = { ...formValues.dataset }; + + if (datasetChangeValues.sortingKey != null) { + datasetChangeValues.sortingKey = datasetChangeValues.sortingKey.valueOf(); + } + + const teamIds = formValues.dataset.allowedTeams.map((t) => t.id); + await updateDatasetPartial(datasetId, datasetChangeValues); + + if (datasetDefaultConfiguration != null) { + await updateDatasetDefaultConfiguration( + datasetId, + _.extend({}, datasetDefaultConfiguration, formValues.defaultConfiguration, { + layers: JSON.parse(formValues.defaultConfigurationLayersJson), + }), + ); + } + + await updateDatasetTeams(datasetId, teamIds); + const dataSource = JSON.parse(formValues.dataSourceJson); + + if (dataset != null && didDatasourceChange(dataSource)) { + if (didDatasourceIdChange(dataSource)) { + Toast.warning(messages["dataset.settings.updated_datasource_id_warning"]); + } + await updateDatasetDatasource(dataset.directoryName, dataset.dataStore.url, dataSource); + setSavedDataSourceOnServer(dataSource); + } + + const verb = isEditingMode ? "updated" : "imported"; + Toast.success(`Successfully ${verb} ${dataset?.name || datasetId}.`); + setHasUnsavedChanges(false); + + if (dataset && queryClient) { + queryClient.invalidateQueries({ + queryKey: ["datasetsByFolder", dataset.folderId], + }); + queryClient.invalidateQueries({ queryKey: ["dataset", "search"] }); + } + + onComplete(); + }, + [ + datasetId, + datasetDefaultConfiguration, + dataset, + didDatasourceChange, + didDatasourceIdChange, + isEditingMode, + queryClient, + onComplete, + ], + ); + + const handleValidationFailed = useCallback( + ({ values }: { values: FormData }) => { + const isOnlyDatasourceIncorrectAndNotEditedResult = isOnlyDatasourceIncorrectAndNotEdited(); + + if (!isOnlyDatasourceIncorrectAndNotEditedResult || !dataset) { + // TODO: Add logic to switch to problematic tab + console.warn("Validation failed, switching to problematic tab logic needed."); + Toast.warning(messages["dataset.import.invalid_fields"]); + } else { + submitForm(values); + } + }, + [isOnlyDatasourceIncorrectAndNotEdited, dataset, submitForm], + ); + + const handleSubmit = useCallback(() => { + syncDataSourceFields(form, activeDataSourceEditMode === "simple" ? "advanced" : "simple"); + + const afterForceUpdateCallback = () => { + setTimeout( + () => + form + .validateFields() + .then((formValues) => submitForm(formValues)) + .catch((errorInfo) => handleValidationFailed(errorInfo)), + 0, + ); + }; + + setActiveDataSourceEditMode((prev) => prev); + setTimeout(afterForceUpdateCallback, 0); + }, [form, activeDataSourceEditMode, submitForm, handleValidationFailed]); + + const onValuesChange = useCallback((_changedValues: FormData, _allValues: FormData) => { + setHasUnsavedChanges(true); + }, []); + + const handleCancel = useCallback(() => { + unblockHistory(); + onCancel(); + }, [unblockHistory, onCancel]); + + const handleDataSourceEditModeChange = useCallback( + (activeEditMode: "simple" | "advanced") => { + syncDataSourceFields(form, activeEditMode); + form.validateFields(); + setActiveDataSourceEditMode(activeEditMode); + }, + [form], + ); + + useBeforeUnload(hasUnsavedChanges, messages["dataset.leave_with_unsaved_changes"]); + + useEffect(() => { + fetchData(); + sendAnalyticsEvent("open_dataset_settings", { + datasetName: dataset ? dataset.name : "Not found dataset", + }); + }, [fetchData, dataset]); + + const contextValue: DatasetSettingsContextValue = { + form, + isLoading, + hasUnsavedChanges, + dataset, + datasetId, + datasetDefaultConfiguration, + activeDataSourceEditMode, + savedDataSourceOnServer, + isEditingMode, + onComplete, + onCancel, + handleSubmit, + handleCancel, + handleDataSourceEditModeChange, + onValuesChange, + getFormValidationSummary, + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx index 7a72e941cef..91505db7f1f 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx @@ -1,7 +1,7 @@ import { CopyOutlined, InfoCircleOutlined, RetweetOutlined } from "@ant-design/icons"; import { PricingPlanEnum } from "admin/organization/pricing_plan_utils"; import { getDatasetSharingToken, revokeDatasetSharingToken } from "admin/rest_api"; -import { Button, Checkbox, Collapse, type FormInstance, Input, Space, Tooltip } from "antd"; +import { Button, Checkbox, Collapse, Input, Space, Tooltip } from "antd"; import { AsyncButton } from "components/async_clickables"; import { PricingEnforcedBlur } from "components/pricing_enforcers"; import DatasetAccessListView from "dashboard/advanced_dataset/dataset_access_list_view"; @@ -10,19 +10,13 @@ import { useWkSelector } from "libs/react_hooks"; import Toast from "libs/toast"; import { isUserAdminOrDatasetManager, isUserAdminOrTeamManager } from "libs/utils"; import window from "libs/window"; -import type React from "react"; import { useEffect, useState } from "react"; -import type { APIDataset } from "types/api_types"; import { getReadableURLPart } from "viewer/model/accessors/dataset_accessor"; +import { useDatasetSettingsContext } from "./dataset_settings_context"; import { FormItemWithInfo } from "./helper_components"; -type Props = { - form: FormInstance | null; - datasetId: string; - dataset: APIDataset | null | undefined; -}; - -export default function DatasetSettingsSharingTab({ form, datasetId, dataset }: Props) { +export default function DatasetSettingsSharingTab() { + const { form, datasetId, dataset } = useDatasetSettingsContext(); const [sharingToken, setSharingToken] = useState(""); const activeUser = useWkSelector((state) => state.activeUser); const isDatasetManagerOrAdmin = isUserAdminOrDatasetManager(activeUser); diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx index 6f2ef048f99..1b5e08f5e94 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx @@ -1,230 +1,40 @@ import { ExclamationCircleOutlined } from "@ant-design/icons"; -import { useQueryClient } from "@tanstack/react-query"; -import { - getDataset, - getDatasetDefaultConfiguration, - readDatasetDatasource, - sendAnalyticsEvent, - updateDatasetDatasource, - updateDatasetDefaultConfiguration, - updateDatasetPartial, - updateDatasetTeams, -} from "admin/rest_api"; import { Alert, Button, Card, Form, Spin, Tabs, Tooltip } from "antd"; -import dayjs from "dayjs"; import features from "features"; -import type { UnregisterCallback } from "history"; -import { handleGenericError } from "libs/error_handling"; +import { useDatasetSettingsContext } from "dashboard/dataset/dataset_settings_context"; +import DatasetSettingsDataTab from "dashboard/dataset/dataset_settings_data_tab"; +import DatasetSettingsDeleteTab from "dashboard/dataset/dataset_settings_delete_tab"; +import DatasetSettingsMetadataTab from "dashboard/dataset/dataset_settings_metadata_tab"; +import DatasetSettingsSharingTab from "dashboard/dataset/dataset_settings_sharing_tab"; import { useWkSelector } from "libs/react_hooks"; -import Toast from "libs/toast"; -import { jsonStringify } from "libs/utils"; -import _ from "lodash"; -import messages from "messages"; -import { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback } from "react"; import { Link } from "react-router-dom"; -import type { APIDataSource, APIDataset, MutableAPIDataset } from "types/api_types"; -import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults"; +import DatasetSettingsViewConfigTab from "./dataset_settings_viewconfig_tab"; +import { Hideable } from "./helper_components"; import { Unicode } from "viewer/constants"; +import messages from "messages"; import { getReadableURLPart } from "viewer/model/accessors/dataset_accessor"; -import { - EXPECTED_TRANSFORMATION_LENGTH, - doAllLayersHaveTheSameRotation, - getRotationSettingsFromTransformationIn90DegreeSteps, -} from "viewer/model/accessors/dataset_layer_transformation_accessor"; -import type { DatasetConfiguration } from "viewer/store"; -import type { DatasetRotationAndMirroringSettings } from "./dataset_rotation_form_item"; -import DatasetSettingsDataTab, { syncDataSourceFields } from "./dataset_settings_data_tab"; -import DatasetSettingsDeleteTab from "./dataset_settings_delete_tab"; -import DatasetSettingsMetadataTab from "./dataset_settings_metadata_tab"; -import DatasetSettingsSharingTab from "./dataset_settings_sharing_tab"; -import DatasetSettingsViewConfigTab from "./dataset_settings_viewconfig_tab"; -import { Hideable, hasFormError } from "./helper_components"; -import useBeforeUnload from "./useBeforeUnload_hook"; const FormItem = Form.Item; const notImportedYetStatus = "Not imported yet."; -type DatasetSettingsViewProps = { - datasetId: string; - isEditingMode: boolean; - onComplete: () => void; - onCancel: () => void; -}; - type TabKey = "data" | "general" | "defaultConfig" | "sharing" | "deleteDataset"; -export type FormData = { - dataSource: APIDataSource; - dataSourceJson: string; - dataset: APIDataset; - defaultConfiguration: DatasetConfiguration; - defaultConfigurationLayersJson: string; - datasetRotation?: DatasetRotationAndMirroringSettings; -}; - -const DatasetSettingsView: React.FC = ({ - datasetId, - isEditingMode, - onComplete, - onCancel, -}) => { - const [form] = Form.useForm(); - const queryClient = useQueryClient(); +const DatasetSettingsView: React.FC = () => { + const { + form, + isLoading, + isEditingMode, + dataset, + datasetId, + handleSubmit, + handleCancel, + handleDataSourceEditModeChange, + onValuesChange, + getFormValidationSummary, + } = useDatasetSettingsContext(); const isUserAdmin = useWkSelector((state) => state.activeUser?.isAdmin || false); - - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [dataset, setDataset] = useState(null); - const [datasetDefaultConfiguration, setDatasetDefaultConfiguration] = useState< - DatasetConfiguration | null | undefined - >(null); - const [isLoading, setIsLoading] = useState(true); - const [activeDataSourceEditMode, setActiveDataSourceEditMode] = useState<"simple" | "advanced">( - "simple", - ); - const [activeTabKey, setActiveTabKey] = useState("data"); - const [savedDataSourceOnServer, setSavedDataSourceOnServer] = useState< - APIDataSource | null | undefined - >(null); - - const unblockRef = useRef(null); - const blockTimeoutIdRef = useRef(null); - - const unblockHistory = useCallback(() => { - window.onbeforeunload = null; - - if (blockTimeoutIdRef.current != null) { - clearTimeout(blockTimeoutIdRef.current); - blockTimeoutIdRef.current = null; - } - - if (unblockRef.current != null) { - unblockRef.current(); - unblockRef.current = null; - } - }, []); - - const fetchData = useCallback(async (): Promise => { - try { - setIsLoading(true); - let fetchedDataset = await getDataset(datasetId); - const dataSource = await readDatasetDatasource(fetchedDataset); - - // Ensure that zarr layers (which aren't inferred by the back-end) are still - // included in the inferred data source - setSavedDataSourceOnServer(dataSource); - - if (dataSource == null) { - throw new Error("No datasource received from server."); - } - - if (fetchedDataset.dataSource.status?.includes("Error")) { - // If the datasource-properties.json could not be parsed due to schema errors, - // we replace it with the version that is at least parsable. - const datasetClone = _.cloneDeep(fetchedDataset) as any as MutableAPIDataset; - // We are keeping the error message to display it to the user. - datasetClone.dataSource.status = fetchedDataset.dataSource.status; - fetchedDataset = datasetClone as APIDataset; - } - - form.setFieldsValue({ - dataSourceJson: jsonStringify(dataSource), - dataset: { - name: fetchedDataset.name, - isPublic: fetchedDataset.isPublic || false, - description: fetchedDataset.description || undefined, - allowedTeams: fetchedDataset.allowedTeams || [], - sortingKey: dayjs(fetchedDataset.sortingKey), - }, - }); - - // This call cannot be combined with the previous setFieldsValue, - // since the layer values wouldn't be initialized correctly. - form.setFieldsValue({ - dataSource, - }); - - // Retrieve the initial dataset rotation settings from the data source config. - if (doAllLayersHaveTheSameRotation(dataSource.dataLayers)) { - const firstLayerTransformations = dataSource.dataLayers[0].coordinateTransformations; - let initialDatasetRotationSettings: DatasetRotationAndMirroringSettings; - if ( - !firstLayerTransformations || - firstLayerTransformations.length !== EXPECTED_TRANSFORMATION_LENGTH - ) { - const nulledSetting = { rotationInDegrees: 0, isMirrored: false }; - initialDatasetRotationSettings = { x: nulledSetting, y: nulledSetting, z: nulledSetting }; - } else { - initialDatasetRotationSettings = { - // First transformation is a translation to the coordinate system origin. - x: getRotationSettingsFromTransformationIn90DegreeSteps( - firstLayerTransformations[1], - "x", - ), - y: getRotationSettingsFromTransformationIn90DegreeSteps( - firstLayerTransformations[2], - "y", - ), - z: getRotationSettingsFromTransformationIn90DegreeSteps( - firstLayerTransformations[3], - "z", - ), - // Fifth transformation is a translation back to the original position. - }; - } - form.setFieldsValue({ - datasetRotation: initialDatasetRotationSettings, - }); - } - - const fetchedDatasetDefaultConfiguration = await getDatasetDefaultConfiguration(datasetId); - enforceValidatedDatasetViewConfiguration( - fetchedDatasetDefaultConfiguration, - fetchedDataset, - true, - ); - form.setFieldsValue({ - defaultConfiguration: fetchedDatasetDefaultConfiguration, - defaultConfigurationLayersJson: JSON.stringify( - fetchedDatasetDefaultConfiguration.layers, - null, - " ", - ), - }); - - setDatasetDefaultConfiguration(fetchedDatasetDefaultConfiguration); - setDataset(fetchedDataset); - } catch (error) { - handleGenericError(error as Error); - } finally { - setIsLoading(false); - form.validateFields(); - } - }, [datasetId, form]); - - const getFormValidationSummary = useCallback((): Record => { - const err = form.getFieldsError(); - const formErrors: Record = {}; - - if (!err || !dataset) { - return formErrors; - } - - const hasErr = hasFormError; - - if (hasErr(err, "dataSource") || hasErr(err, "dataSourceJson")) { - formErrors.data = true; - } - - if (hasErr(err, "dataset")) { - formErrors.general = true; - } - - if (hasErr(err, "defaultConfiguration") || hasErr(err, "defaultConfigurationLayersJson")) { - formErrors.defaultConfig = true; - } - - return formErrors; - }, [form, dataset]); + const [activeTabKey, setActiveTabKey] = React.useState("data"); const switchToProblematicTab = useCallback(() => { const validationSummary = getFormValidationSummary(); @@ -235,8 +45,8 @@ const DatasetSettingsView: React.FC = ({ } // Switch to the earliest, problematic tab - const problematicTab = _.find( - ["data", "general", "defaultConfig"], + const problematicTab = + ["data", "general", "defaultConfig"].find( (key) => validationSummary[key], ); @@ -245,171 +55,6 @@ const DatasetSettingsView: React.FC = ({ } }, [getFormValidationSummary, activeTabKey]); - const didDatasourceChange = useCallback( - (dataSource: Record) => { - return !_.isEqual(dataSource, savedDataSourceOnServer || {}); - }, - [savedDataSourceOnServer], - ); - - const didDatasourceIdChange = useCallback( - (dataSource: Record) => { - const savedDatasourceId = savedDataSourceOnServer?.id; - if (!savedDatasourceId) { - return false; - } - return ( - savedDatasourceId.name !== dataSource.id.name || - savedDatasourceId.team !== dataSource.id.team - ); - }, - [savedDataSourceOnServer], - ); - - const isOnlyDatasourceIncorrectAndNotEdited = useCallback(() => { - const validationSummary = getFormValidationSummary(); - - if (_.size(validationSummary) === 1 && validationSummary.data) { - try { - const dataSource = JSON.parse(form.getFieldValue("dataSourceJson")); - const didNotEditDatasource = !didDatasourceChange(dataSource); - return didNotEditDatasource; - } catch (_e) { - return false; - } - } - - return false; - }, [getFormValidationSummary, form, didDatasourceChange]); - - const submit = useCallback( - async (formValues: FormData) => { - const datasetChangeValues = { ...formValues.dataset }; - - if (datasetChangeValues.sortingKey != null) { - datasetChangeValues.sortingKey = datasetChangeValues.sortingKey.valueOf(); - } - - const teamIds = formValues.dataset.allowedTeams.map((t) => t.id); - await updateDatasetPartial(datasetId, datasetChangeValues); - - if (datasetDefaultConfiguration != null) { - await updateDatasetDefaultConfiguration( - datasetId, - _.extend({}, datasetDefaultConfiguration, formValues.defaultConfiguration, { - layers: JSON.parse(formValues.defaultConfigurationLayersJson), - }), - ); - } - - await updateDatasetTeams(datasetId, teamIds); - const dataSource = JSON.parse(formValues.dataSourceJson); - - if (dataset != null && didDatasourceChange(dataSource)) { - if (didDatasourceIdChange(dataSource)) { - Toast.warning(messages["dataset.settings.updated_datasource_id_warning"]); - } - await updateDatasetDatasource(dataset.directoryName, dataset.dataStore.url, dataSource); - setSavedDataSourceOnServer(dataSource); - } - - const verb = isEditingMode ? "updated" : "imported"; - Toast.success(`Successfully ${verb} ${dataset?.name || datasetId}.`); - setHasUnsavedChanges(false); - - if (dataset && queryClient) { - // Update new cache - queryClient.invalidateQueries({ - queryKey: ["datasetsByFolder", dataset.folderId], - }); - queryClient.invalidateQueries({ queryKey: ["dataset", "search"] }); - } - - onComplete(); - }, - [ - datasetId, - datasetDefaultConfiguration, - dataset, - didDatasourceChange, - didDatasourceIdChange, - isEditingMode, - queryClient, - onComplete, - ], - ); - - const handleValidationFailed = useCallback( - ({ values }: { values: FormData }) => { - const isOnlyDatasourceIncorrectAndNotEditedResult = isOnlyDatasourceIncorrectAndNotEdited(); - - // Check whether the validation error was introduced or existed before - if (!isOnlyDatasourceIncorrectAndNotEditedResult || !dataset) { - switchToProblematicTab(); - Toast.warning(messages["dataset.import.invalid_fields"]); - } else { - // If the validation error existed before, still attempt to update dataset - submit(values); - } - }, - [isOnlyDatasourceIncorrectAndNotEdited, dataset, switchToProblematicTab, submit], - ); - - const handleSubmit = useCallback(() => { - // Ensure that all form fields are in sync - syncDataSourceFields(form, activeDataSourceEditMode === "simple" ? "advanced" : "simple"); - - const afterForceUpdateCallback = () => { - // Trigger validation manually, because fields may have been updated - // and defer the validation as it is done asynchronously by antd or so. - setTimeout( - () => - form - .validateFields() - .then((formValues) => submit(formValues)) - .catch((errorInfo) => handleValidationFailed(errorInfo)), - 0, - ); - }; - - // Need to force update of the SimpleAdvancedDataForm as removing a layer in the advanced tab does not update - // the form items in the simple tab (only the values are updated). The form items automatically update once - // the simple tab renders, but this is not the case when the user directly submits the changes. - // In functional components, we can trigger a re-render by updating state - setActiveDataSourceEditMode((prev) => prev); // Force re-render - setTimeout(afterForceUpdateCallback, 0); - }, [form, activeDataSourceEditMode, submit, handleValidationFailed]); - - const onValuesChange = useCallback((_changedValues: FormData, _allValues: FormData) => { - setHasUnsavedChanges(true); - }, []); - - const handleCancel = useCallback(() => { - unblockHistory(); - onCancel(); - }, [unblockHistory, onCancel]); - - const handleDataSourceEditModeChange = useCallback( - (activeEditMode: "simple" | "advanced") => { - syncDataSourceFields(form, activeEditMode); - form.validateFields(); - setActiveDataSourceEditMode(activeEditMode); - }, - [form], - ); - - // Setup beforeunload handling - useBeforeUnload(hasUnsavedChanges, messages["dataset.leave_with_unsaved_changes"]); - - // Initial data fetch and analytics - // biome-ignore lint/correctness/useExhaustiveDependencies: dataset dependency removed to avoid infinite loop - useEffect(() => { - fetchData(); - sendAnalyticsEvent("open_dataset_settings", { - datasetName: dataset ? dataset.name : "Not found dataset", - }); - }, [fetchData]); - const getMessageComponents = useCallback(() => { if (dataset == null) { return null; @@ -513,9 +158,6 @@ const DatasetSettingsView: React.FC = ({ } @@ -528,7 +170,7 @@ const DatasetSettingsView: React.FC = ({ forceRender: true, children: ( ), }, @@ -570,7 +212,7 @@ const DatasetSettingsView: React.FC = ({ forceRender: true, children: ( ), }); @@ -598,7 +240,7 @@ const DatasetSettingsView: React.FC = ({ setActiveTabKey(key as TabKey)} + onChange={(key) => setActiveTabKey(key as TabKey)} // Update active tab key items={tabs} /> @@ -611,7 +253,7 @@ const DatasetSettingsView: React.FC = ({ {confirmString} {Unicode.NonBreakingSpace} - + {/* Use handleCancel from context */} diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx index 3ac2277deb6..b608e6bcbfd 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx @@ -29,6 +29,7 @@ import { syncValidator, validateLayerViewConfigurationObjectJSON } from "types/v import { BLEND_MODES } from "viewer/constants"; import type { DatasetConfiguration, DatasetLayerConfiguration } from "viewer/store"; import ColorLayerOrderingTable from "./color_layer_ordering_component"; +import { useDatasetSettingsContext } from "./dataset_settings_context"; import { FormItemWithInfo, jsonEditStyle } from "./helper_components"; const FormItem = Form.Item; @@ -42,6 +43,7 @@ export default function DatasetSettingsViewConfigTab(props: { Record >({}); + const { dataset } = useDatasetSettingsContext(); const validateDefaultMappings = useMemo( () => async (configStr: string, dataStoreURL: string, dataSourceId: APIDataSourceId) => { let config = {} as DatasetConfiguration["layers"]; @@ -92,7 +94,7 @@ export default function DatasetSettingsViewConfigTab(props: { throw new Error("The following mappings are invalid: " + errors.join("\n")); } }, - [availableMappingsPerLayerCache], + [availableMappingsPerLayerCache, dataset], // Add dataset to dependencies for dataSourceId ); const columns = [ @@ -303,6 +305,7 @@ export default function DatasetSettingsViewConfigTab(props: { { validator: (_rule, config: string) => Promise.all([ + // Use dataset.dataSource.id for dataSourceId validateLayerViewConfigurationObjectJSON(_rule, config), dataStoreURL ? validateDefaultMappings(config, dataStoreURL, dataSourceId) diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index 6176b7aa097..d57fc5b4044 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -75,6 +75,7 @@ import type { EmptyObject } from "types/globals"; import { getDatasetIdOrNameFromReadableURLPart } from "viewer/model/accessors/dataset_accessor"; import { Store } from "viewer/singletons"; import { CommandPalette } from "viewer/view/components/command_palette"; +import { DatasetSettingsProvider } from "dashboard/dataset/dataset_settings_provider"; const { Content } = Layout; @@ -546,12 +547,14 @@ class ReactRouter extends React.Component { ); } return ( - window.history.back()} - onCancel={() => window.history.back()} - /> + window.history.back()} + onCancel={() => window.history.back()}> + + ); }} /> From 81e8ac2b3ff76781008bdaea836ff15d176ec175 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 30 Jun 2025 10:17:39 +0200 Subject: [PATCH 2/9] fix loading loop --- .../dashboard/dataset/dataset_settings_provider.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx index fa80065696d..e526aabbb3e 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx @@ -84,7 +84,7 @@ export const DatasetSettingsProvider: React.FC = ( } }, []); - const fetchData = useCallback(async (): Promise => { + const fetchData = useCallback(async (): Promise => { try { setIsLoading(true); let fetchedDataset = await getDataset(datasetId); @@ -164,8 +164,10 @@ export const DatasetSettingsProvider: React.FC = ( setDatasetDefaultConfiguration(fetchedDatasetDefaultConfiguration); setDataset(fetchedDataset); + return fetchedDataset.name; } catch (error) { handleGenericError(error as Error); + return undefined; } finally { setIsLoading(false); form.validateFields(); @@ -344,11 +346,12 @@ export const DatasetSettingsProvider: React.FC = ( useBeforeUnload(hasUnsavedChanges, messages["dataset.leave_with_unsaved_changes"]); useEffect(() => { - fetchData(); - sendAnalyticsEvent("open_dataset_settings", { - datasetName: dataset ? dataset.name : "Not found dataset", + fetchData().then((datasetName) => { + sendAnalyticsEvent("open_dataset_settings", { + datasetName: datasetName ?? "Not found dataset", + }); }); - }, [fetchData, dataset]); + }, [fetchData]); const contextValue: DatasetSettingsContextValue = { form, From 36e3cabb0ee6650cde1049e094a566d2093aa391 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 30 Jun 2025 13:19:57 +0200 Subject: [PATCH 3/9] WIP rework dataset settings as individual routes --- .../dataset/dataset_settings_provider.tsx | 8 +- .../dataset/dataset_settings_sharing_tab.tsx | 126 +++++---- .../dataset/dataset_settings_view.tsx | 251 ++++++++++-------- frontend/javascripts/router.tsx | 23 +- 4 files changed, 224 insertions(+), 184 deletions(-) diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx index e526aabbb3e..b6f405dc5dd 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_provider.tsx @@ -40,16 +40,12 @@ type DatasetSettingsProviderProps = { children: React.ReactNode; datasetId: string; isEditingMode: boolean; - onComplete: () => void; - onCancel: () => void; }; export const DatasetSettingsProvider: React.FC = ({ children, datasetId, isEditingMode, - onComplete, - onCancel, }) => { const [form] = Form.useForm(); const queryClient = useQueryClient(); @@ -84,6 +80,10 @@ export const DatasetSettingsProvider: React.FC = ( } }, []); + const onComplete = () => window.history.back(); + + const onCancel = () => window.history.back(); + const fetchData = useCallback(async (): Promise => { try { setIsLoading(true); diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx index 91505db7f1f..5a01fd6e58f 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_sharing_tab.tsx @@ -1,19 +1,20 @@ import { CopyOutlined, InfoCircleOutlined, RetweetOutlined } from "@ant-design/icons"; import { PricingPlanEnum } from "admin/organization/pricing_plan_utils"; import { getDatasetSharingToken, revokeDatasetSharingToken } from "admin/rest_api"; -import { Button, Checkbox, Collapse, Input, Space, Tooltip } from "antd"; +import { Button, Checkbox, Col, Collapse, Form, Input, Row, Space, Switch, Tooltip } from "antd"; import { AsyncButton } from "components/async_clickables"; import { PricingEnforcedBlur } from "components/pricing_enforcers"; import DatasetAccessListView from "dashboard/advanced_dataset/dataset_access_list_view"; import TeamSelectionComponent from "dashboard/dataset/team_selection_component"; -import { useWkSelector } from "libs/react_hooks"; +import { useEffectOnlyOnce, useWkSelector } from "libs/react_hooks"; import Toast from "libs/toast"; import { isUserAdminOrDatasetManager, isUserAdminOrTeamManager } from "libs/utils"; import window from "libs/window"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { getReadableURLPart } from "viewer/model/accessors/dataset_accessor"; import { useDatasetSettingsContext } from "./dataset_settings_context"; -import { FormItemWithInfo } from "./helper_components"; +import { SettingsTitle } from "admin/account/helpers/settings_title"; +import { SettingsCard } from "admin/account/helpers/settings_card"; export default function DatasetSettingsSharingTab() { const { form, datasetId, dataset } = useDatasetSettingsContext(); @@ -21,28 +22,14 @@ export default function DatasetSettingsSharingTab() { const activeUser = useWkSelector((state) => state.activeUser); const isDatasetManagerOrAdmin = isUserAdminOrDatasetManager(activeUser); - const allowedTeamsComponent = ( - - - - - - ); - async function fetch() { const newSharingToken = await getDatasetSharingToken(datasetId); setSharingToken(newSharingToken); } - // biome-ignore lint/correctness/useExhaustiveDependencies(fetch): - useEffect(() => { + useEffectOnlyOnce(() => { fetch(); - }, []); + }); function handleSelectCode(event: React.MouseEvent): void { event.currentTarget.select(); @@ -68,7 +55,7 @@ export default function DatasetSettingsSharingTab() { function getSharingLink() { if (!form) return undefined; - const doesNeedToken = !form.getFieldValue("dataset.isPublic"); + const doesNeedToken = !form.getFieldValue(["dataset", "isPublic"]); const tokenSuffix = `?token=${sharingToken}`; return `${window.location.origin}/datasets/${dataset ? getReadableURLPart(dataset) : datasetId}/view${doesNeedToken ? tokenSuffix : ""}`; } @@ -77,22 +64,15 @@ export default function DatasetSettingsSharingTab() { if (!activeUser || !dataset) return undefined; if (!isUserAdminOrTeamManager(activeUser)) return undefined; - const panelLabel = ( - - All users with access permission to work with this dataset{" "} - - - - - ); - return ( , }, ]} @@ -100,27 +80,35 @@ export default function DatasetSettingsSharingTab() { ); } - return form ? ( -
- - Make dataset publicly accessible - - {allowedTeamsComponent} - - The sharing link can be used to allow unregistered users to view this dataset. If the - dataset itself is not public, the link contains a secret token which ensures that the - dataset can be opened if you know the special link. - - } - > + const sharingItems = [ + { + title: "Make dataset publicly accessible", + explanation: + "Make your dataset public, for anonymous/unregistered users to access your dataset.", + value: ( + + + + ), + }, + { + title: "Additional team access permissions for this dataset", + explanation: + "The dataset can be seen by administrators, dataset managers and by teams that have access to the folder in which the dataset is located. If you want to grant additional teams access, define these teams here.", + value: ( + + + + + + ), + }, + { + title: "Sharing Link", + value: ( }> Copy - {!form.getFieldValue("dataset.isPublic") && ( + {!form.getFieldValue(["dataset", "isPublic"]) && ( @@ -148,8 +136,36 @@ export default function DatasetSettingsSharingTab() { )} - - {getUserAccessList()} + ), + explanation: + "The sharing link can be used to allow unregistered users to view this dataset. If the dataset itself is not public, the link contains a secret token which ensures that the dataset can be opened if you know the special link.", + }, + + { + title: "Users with access permission to work with this dataset", + explanation: + "Dataset access is based on the specified team permissions and individual user roles. Any changes will only appear after pressing the Save button.", + value: getUserAccessList(), + }, + ]; + + return form ? ( +
+ + + {sharingItems.map((item) => ( + + + + ))} +
) : null; } diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx index 7d69d8909dd..9d20bf798e0 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx @@ -1,58 +1,113 @@ -import { ExclamationCircleOutlined } from "@ant-design/icons"; -import { Alert, Button, Card, Form, Spin, Tabs, Tooltip } from "antd"; +import { + CodeSandboxOutlined, + DeleteOutlined, + ExclamationCircleOutlined, + FileTextOutlined, + SafetyOutlined, + SettingOutlined, + TeamOutlined, + UserOutlined, +} from "@ant-design/icons"; +import { Alert, Breadcrumb, Button, Form, Layout, Menu, Tooltip } from "antd"; import features from "features"; import { useDatasetSettingsContext } from "dashboard/dataset/dataset_settings_context"; import DatasetSettingsDataTab from "dashboard/dataset/dataset_settings_data_tab"; import DatasetSettingsDeleteTab from "dashboard/dataset/dataset_settings_delete_tab"; import DatasetSettingsMetadataTab from "dashboard/dataset/dataset_settings_metadata_tab"; import DatasetSettingsSharingTab from "dashboard/dataset/dataset_settings_sharing_tab"; -import { useWkSelector } from "libs/react_hooks"; -import React, { useCallback } from "react"; -import { Link } from "react-router-dom"; import DatasetSettingsViewConfigTab from "./dataset_settings_viewconfig_tab"; -import { Hideable } from "./helper_components"; +import { useWkSelector } from "libs/react_hooks"; +import type React from "react"; +import { useCallback } from "react"; +import { Link, Route, useHistory, useLocation, Switch } from "react-router-dom"; import { Unicode } from "viewer/constants"; import messages from "messages"; import { getReadableURLPart } from "viewer/model/accessors/dataset_accessor"; +import type { MenuItemGroupType } from "antd/es/menu/interface"; +const { Sider, Content } = Layout; const FormItem = Form.Item; const notImportedYetStatus = "Not imported yet."; -type TabKey = "data" | "general" | "defaultConfig" | "sharing" | "deleteDataset"; +const BREADCRUMB_LABELS = { + data: "Data Source", + sharing: "Sharing & Permissions", + metadata: "Metadata", + defaultConfig: "View Configuration", + delete: "Delete Dataset", +}; + +const MENU_ITEMS: MenuItemGroupType[] = [ + { + label: "Dataset Settings", + type: "group", + children: [ + { + key: "data", + icon: , + label: "Data Source", + }, + { + key: "sharing", + icon: , + label: "Sharing & Permissions", + }, + { + key: "metadata", + icon: , + label: "Metadata", + }, + { + key: "defaultConfig", + icon: , + label: "View Configuration", + }, + { + key: "delete", + icon: , + label: "Delete", + }, + ], + }, +]; + +// if (isUserAdmin && features().allowDeleteDatasets) { +// MENU_ITEMS[0].children.push(); +// } const DatasetSettingsView: React.FC = () => { const { form, - isLoading, isEditingMode, dataset, datasetId, handleSubmit, handleCancel, - handleDataSourceEditModeChange, onValuesChange, getFormValidationSummary, } = useDatasetSettingsContext(); const isUserAdmin = useWkSelector((state) => state.activeUser?.isAdmin || false); - const [activeTabKey, setActiveTabKey] = React.useState("data"); + const location = useLocation(); + const history = useHistory(); + const selectedKey = location.pathname.split("/").filter(Boolean).pop() || "data"; - const switchToProblematicTab = useCallback(() => { - const validationSummary = getFormValidationSummary(); + // const switchToProblematicTab = useCallback(() => { + // const validationSummary = getFormValidationSummary(); - if (validationSummary[activeTabKey]) { - // Active tab is already problematic - return; - } + // if (validationSummary[activeTabKey]) { + // // Active tab is already problematic + // return; + // } - // Switch to the earliest, problematic tab - const problematicTab = ["data", "general", "defaultConfig"].find( - (key) => validationSummary[key], - ); + // // Switch to the earliest, problematic tab + // const problematicTab = ["data", "general", "defaultConfig"].find( + // (key) => validationSummary[key], + // ); - if (problematicTab) { - setActiveTabKey(problematicTab as TabKey); - } - }, [getFormValidationSummary, activeTabKey]); + // if (problematicTab) { + // setActiveTabKey(problematicTab as TabKey); + // } + // }, [getFormValidationSummary, activeTabKey]); const getMessageComponents = useCallback(() => { if (dataset == null) { @@ -144,105 +199,65 @@ const DatasetSettingsView: React.FC = () => { ); - const tabs = [ + const breadcrumbItems = [ { - label: Data {formErrors.data ? errorIcon : ""}, - key: "data", - forceRender: true, - children: ( - - ), + title: "Dataset Settings", }, - { - label: Sharing & Permissions {formErrors.general ? errorIcon : null}, - key: "sharing", - forceRender: true, - children: ( - - ), - }, - - { - label: Metadata, - key: "general", - forceRender: true, - children: ( - - ), - }, - - { - label: View Configuration {formErrors.defaultConfig ? errorIcon : ""}, - key: "defaultConfig", - forceRender: true, - children: ( - - ), + title: BREADCRUMB_LABELS[selectedKey as keyof typeof BREADCRUMB_LABELS], }, ]; - if (isUserAdmin && features().allowDeleteDatasets) - tabs.push({ - label: Delete Dataset , - key: "deleteDataset", - forceRender: true, - children: ( - - ), - }); - return ( -
- - {titleString} Dataset {datasetLinkOrName} - - } - > - - {getMessageComponents()} - - - setActiveTabKey(key as TabKey)} // Update active tab key - items={tabs} + + history.push(`/datasets/${datasetId}/edit/${key}`)} + /> + + + + {getMessageComponents()} + + + + + + ( + + )} + /> + - + { {confirmString} {Unicode.NonBreakingSpace} - {/* Use handleCancel from context */} + - - - + + + ); }; diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index a6cab0ae08c..34cca7db77c 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -546,13 +546,22 @@ class ReactRouter extends React.Component { ); } return ( - window.history.back()} - onCancel={() => window.history.back()}> - + + + + ); + }} + /> + { + const { datasetId } = getDatasetIdOrNameFromReadableURLPart( + match.params.datasetNameAndId, + ); + return ( + + ); }} From 92e78b8e8f40ab591b937aacfc844b848d072f97 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 30 Jun 2025 14:28:40 +0200 Subject: [PATCH 4/9] renamed Settingscard props --- .../admin/account/account_auth_token_view.tsx | 20 ++-- .../admin/account/account_password_view.tsx | 10 +- .../admin/account/account_profile_view.tsx | 22 ++-- .../admin/account/helpers/settings_card.tsx | 20 ++-- .../organization_danger_zone_view.tsx | 2 +- .../organization_notifications_view.tsx | 12 +- .../organization_overview_view.tsx | 24 ++-- .../dataset/dataset_settings_metadata_tab.tsx | 71 ++++++----- .../dataset/dataset_settings_sharing_tab.tsx | 112 +++++++----------- .../dataset/dataset_settings_view.tsx | 15 ++- 10 files changed, 148 insertions(+), 160 deletions(-) diff --git a/frontend/javascripts/admin/account/account_auth_token_view.tsx b/frontend/javascripts/admin/account/account_auth_token_view.tsx index e1bb6f77358..e5cde44f217 100644 --- a/frontend/javascripts/admin/account/account_auth_token_view.tsx +++ b/frontend/javascripts/admin/account/account_auth_token_view.tsx @@ -4,7 +4,7 @@ import { Button, Col, Row, Spin, Typography } from "antd"; import { useWkSelector } from "libs/react_hooks"; import Toast from "libs/toast"; import { useEffect, useState } from "react"; -import { SettingsCard } from "./helpers/settings_card"; +import { SettingsCard, type SettingsCardProps } from "./helpers/settings_card"; import { SettingsTitle } from "./helpers/settings_title"; const { Text } = Typography; @@ -41,10 +41,10 @@ function AccountAuthTokenView() { } }; - const APIitems = [ + const APIitems: SettingsCardProps[] = [ { title: "Auth Token", - value: ( + content: ( {currentToken} @@ -52,9 +52,9 @@ function AccountAuthTokenView() { }, { title: "Token Revocation", - explanation: + tooltip: "Revoke your token if it has been compromised or if you suspect someone else has gained access to it. This will invalidate all active sessions.", - value: ( + content: ( @@ -64,7 +64,7 @@ function AccountAuthTokenView() { ? [ { title: "Organization ID", - value: ( + content: ( {activeUser.organization} @@ -74,7 +74,7 @@ function AccountAuthTokenView() { : []), { title: "API Documentation", - value: ( + content: ( Read the docs @@ -92,11 +92,7 @@ function AccountAuthTokenView() { {APIitems.map((item) => ( - + ))} diff --git a/frontend/javascripts/admin/account/account_password_view.tsx b/frontend/javascripts/admin/account/account_password_view.tsx index 9d74a6e6546..ec05ecaf633 100644 --- a/frontend/javascripts/admin/account/account_password_view.tsx +++ b/frontend/javascripts/admin/account/account_password_view.tsx @@ -7,7 +7,7 @@ import { useState } from "react"; import { useHistory } from "react-router-dom"; import { logoutUserAction } from "viewer/model/actions/user_actions"; import Store from "viewer/store"; -import { SettingsCard } from "./helpers/settings_card"; +import { SettingsCard, type SettingsCardProps } from "./helpers/settings_card"; import { SettingsTitle } from "./helpers/settings_title"; const FormItem = Form.Item; const { Password } = Input; @@ -155,10 +155,10 @@ function AccountPasswordView() { setResetPasswordVisible(true); } - const passKeyList = [ + const passKeyList: SettingsCardProps[] = [ { title: "Coming soon", - value: "Passwordless login with passkeys is coming soon", + content: "Passwordless login with passkeys is coming soon", // action: - {!form.getFieldValue(["dataset", "isPublic"]) && ( - - The URL contains a secret token which enables anybody with this link to view the - dataset. Renew the token to make the old link invalid. - - } - > - }> - Renew - - - )} - + content: ( + <> + + {getSharingLink()} + + + {!isDatasetPublic && ( + + The URL contains a secret token which enables anybody with this link to view the + dataset. Renew the token to make the old link invalid. + + } + > + }> + Renew Authorization Token + + + )} + + ), - explanation: + tooltip: "The sharing link can be used to allow unregistered users to view this dataset. If the dataset itself is not public, the link contains a secret token which ensures that the dataset can be opened if you know the special link.", }, { title: "Users with access permission to work with this dataset", - explanation: + tooltip: "Dataset access is based on the specified team permissions and individual user roles. Any changes will only appear after pressing the Save button.", - value: getUserAccessList(), + content: getUserAccessList(), }, ]; - return form ? ( + return (
{sharingItems.map((item) => ( - + ))}
- ) : null; + ); } diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx index 9d20bf798e0..4e2bc21bf56 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx @@ -2,14 +2,12 @@ import { CodeSandboxOutlined, DeleteOutlined, ExclamationCircleOutlined, + ExportOutlined, FileTextOutlined, - SafetyOutlined, SettingOutlined, TeamOutlined, - UserOutlined, } from "@ant-design/icons"; import { Alert, Breadcrumb, Button, Form, Layout, Menu, Tooltip } from "antd"; -import features from "features"; import { useDatasetSettingsContext } from "dashboard/dataset/dataset_settings_context"; import DatasetSettingsDataTab from "dashboard/dataset/dataset_settings_data_tab"; import DatasetSettingsDeleteTab from "dashboard/dataset/dataset_settings_delete_tab"; @@ -69,6 +67,17 @@ const MENU_ITEMS: MenuItemGroupType[] = [ }, ], }, + { type: "divider" }, + { + type: "group", + children: [ + { + key: "open", + icon: , + label: "Open in WEBKNOSSOS", + }, + ], + }, ]; // if (isUserAdmin && features().allowDeleteDatasets) { From b550de481027928789b39b52dc6d830f78dbcae0 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 30 Jun 2025 15:45:03 +0200 Subject: [PATCH 5/9] WIP restyle the dataset settings form --- .../admin/account/helpers/settings_card.tsx | 2 +- .../color_layer_ordering_component.tsx | 20 +- .../dataset/dataset_settings_delete_tab.tsx | 34 +- .../dataset/dataset_settings_view.tsx | 2 +- .../dataset_settings_viewconfig_tab.tsx | 347 +++++++++--------- frontend/javascripts/messages.tsx | 5 +- 6 files changed, 219 insertions(+), 191 deletions(-) diff --git a/frontend/javascripts/admin/account/helpers/settings_card.tsx b/frontend/javascripts/admin/account/helpers/settings_card.tsx index 438e1b65701..158f0997351 100644 --- a/frontend/javascripts/admin/account/helpers/settings_card.tsx +++ b/frontend/javascripts/admin/account/helpers/settings_card.tsx @@ -18,7 +18,7 @@ export function SettingsCard({ title, content, tooltip, action }: SettingsCardPr {tooltip != null ? ( - + ) : null}
diff --git a/frontend/javascripts/dashboard/dataset/color_layer_ordering_component.tsx b/frontend/javascripts/dashboard/dataset/color_layer_ordering_component.tsx index 2f07a49d394..d06042452a9 100644 --- a/frontend/javascripts/dashboard/dataset/color_layer_ordering_component.tsx +++ b/frontend/javascripts/dashboard/dataset/color_layer_ordering_component.tsx @@ -54,29 +54,18 @@ export default function ColorLayerOrderingTable({ const isSettingEnabled = colorLayerNames && colorLayerNames.length > 1; const sortingItems = isSettingEnabled ? colorLayerNames.map((name) => name) : []; - const collapsibleDisabledExplanation = + const settingsIsDisabledExplanation = "The order of layers can only be configured when the dataset has multiple color layers."; - const panelTitle = ( - - {settings.colorLayerOrder}{" "} - - - - - ); - const collapseItems: CollapseProps["items"] = [ { - label: panelTitle, + label: settingsTooltips.colorLayerOrder, key: "1", children: sortingItems.map((name) => ), }, ]; - return ( + return isSettingEnabled ? ( { @@ -89,8 +78,11 @@ export default function ColorLayerOrderingTable({ defaultActiveKey={[]} collapsible={isSettingEnabled ? "header" : "disabled"} items={collapseItems} + ghost /> + ) : ( + settingsIsDisabledExplanation ); } diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx index 62a8f9b8e15..684695a4296 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_delete_tab.tsx @@ -1,12 +1,14 @@ import { useQueryClient } from "@tanstack/react-query"; import { deleteDatasetOnDisk } from "admin/rest_api"; -import { Button } from "antd"; +import { Button, Col, Row } from "antd"; import Toast from "libs/toast"; import messages from "messages"; import { useState } from "react"; import { useHistory } from "react-router-dom"; import { useDatasetSettingsContext } from "./dataset_settings_context"; import { confirmAsync } from "./helper_components"; +import { SettingsCard } from "admin/account/helpers/settings_card"; +import { SettingsTitle } from "admin/account/helpers/settings_title"; const DatasetSettingsDeleteTab = () => { const { dataset } = useDatasetSettingsContext(); @@ -14,13 +16,12 @@ const DatasetSettingsDeleteTab = () => { const queryClient = useQueryClient(); const history = useHistory(); - // const dataset = useFetch(() => getDataset(datasetId), null, [datasetId]); - async function handleDeleteButtonClicked(): Promise { if (!dataset) { return; } + // TODO why is this an async confirm? Sync should work too and get proper antd styling const deleteDataset = await confirmAsync({ title: `Deleting a dataset on disk cannot be undone. Are you certain to delete dataset ${dataset.name}? Note that the name of a dataset is not guaranteed to be free to use afterwards.`, okText: "Yes, Delete Dataset on Disk now", @@ -53,12 +54,27 @@ const DatasetSettingsDeleteTab = () => { return (
-

Deleting a dataset on disk cannot be undone. Please be certain.

-

Note that annotations for the dataset stay downloadable and the name stays reserved.

-

Only admins are allowed to delete datasets.

- + + + + +

Deleting a dataset on disk cannot be undone. Please be certain.

+

+ Note that annotations for the dataset stay downloadable and the name stays + reserved. +

+

Only admins are allowed to delete datasets.

+ + + } + /> + +
); }; diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx index 4e2bc21bf56..62e6e1d274e 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx @@ -221,7 +221,7 @@ const DatasetSettingsView: React.FC = () => { - + - ).map(([settingsName, spanWidth]) => ( - - - - {settings[settingsName]}{" "} - - - - - - - )); + + const viewConfigItems: SettingsCardProps[] = [ + { + title: "Position", + tooltip: "The default position is defined in voxel-coordinates (x, y, z).", + content: ( + + + + ), + }, + { + title: "Zoom Level", + tooltip: + "A zoom level of “1” will display the data in its original magnification.", + content: ( + value == null || value > 0, + "The zoom value must be greater than 0.", + ), + }, + ]} + > + + + ), + }, + { + title: "Rotation - Arbitrary Modes only", + tooltip: "The default rotation that will be used in oblique and flight view mode.", + content: ( + + + + ), + }, + { + title: settings.interpolation, + tooltip: settingsTooltips.interpolation, + content: ( + + + + ), + }, + { + title: settings.fourBit, + tooltip: settingsTooltips.fourBit, + content: ( + + + + ), + }, + { + title: settings.renderMissingDataBlack, + tooltip: settingsTooltips.renderMissingDataBlack, + content: ( + + + + ), + }, + { + title: settings.segmentationPatternOpacity, + tooltip: settingsTooltips.segmentationPatternOpacity, + content: ( + + + + + + + + + + + + + ), + }, + { + title: settings.blendMode, + tooltip: settingsTooltips.blendMode, + content: ( + + + + ), + }, + { + title: settings.loadingStrategy, + tooltip: settingsTooltips.loadingStrategy, + content: ( + + + + ), + }, + { + title: "Color Layer Order", + tooltip: + "Set the order in which color layers are rendered. This setting is only relevant if the cover blend mode is active.", + content: ( + + + + ), + }, + ]; return (
- - - - - - - - - value == null || value > 0, - "The zoom value must be greater than 0.", - ), - }, - ]} - > - - - - - - - - + + {viewConfigItems.map((item) => ( + + + + ))} - {checkboxSettings} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - Promise.all([ - // Use dataset.dataSource.id for dataSourceId - validateLayerViewConfigurationObjectJSON(_rule, config), - dataStoreURL - ? validateDefaultMappings(config, dataStoreURL, dataSourceId) - : Promise.resolve(), - ]), - }, - ]} - > - - + + Promise.all([ + // Use dataset.dataSource.id for dataSourceId + validateLayerViewConfigurationObjectJSON(_rule, config), + dataStoreURL + ? validateDefaultMappings(config, dataStoreURL, dataSourceId) + : Promise.resolve(), + ]), + }, + ]} + > + + + } + /> - Valid layer view configurations and their default values: -
-
- + } /> diff --git a/frontend/javascripts/messages.tsx b/frontend/javascripts/messages.tsx index 5b709334965..25a3a017fb0 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -39,7 +39,7 @@ export const settings: Partial> = sphericalCapRadius: "Sphere Radius", crosshairSize: "Crosshair Size", brushSize: "Brush Size", - segmentationPatternOpacity: "Pattern Opacity", + segmentationPatternOpacity: "Segmentation Pattern Opacity", loadingStrategy: "Loading Strategy", gpuMemoryFactor: "Hardware Utilization", overwriteMode: "Volume Annotation Overwrite Mode", @@ -50,6 +50,8 @@ export const settings: Partial> = colorLayerOrder: "Color Layer Order", }; export const settingsTooltips: Partial> = { + segmentationPatternOpacity: + "The opacity of the pattern overlaid on any segmentation layer for improved contrast.", loadingStrategy: `You can choose between loading the best quality first (will take longer until you see data) or alternatively, improving the quality progressively (data will be loaded faster, @@ -82,6 +84,7 @@ export const settingsTooltips: Partial> = { color: "Color", alpha: "Layer opacity", From 3eecbeba75f04680ddbc37129ec574b631329cb0 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 30 Jun 2025 16:38:05 +0200 Subject: [PATCH 6/9] WIP --- .../admin/account/helpers/settings_card.tsx | 7 +- .../dataset/dataset_settings_data_tab.tsx | 263 +++++++++--------- .../dataset/dataset_settings_view.tsx | 12 + 3 files changed, 142 insertions(+), 140 deletions(-) diff --git a/frontend/javascripts/admin/account/helpers/settings_card.tsx b/frontend/javascripts/admin/account/helpers/settings_card.tsx index 158f0997351..130b9f1c74f 100644 --- a/frontend/javascripts/admin/account/helpers/settings_card.tsx +++ b/frontend/javascripts/admin/account/helpers/settings_card.tsx @@ -6,11 +6,12 @@ export type SettingsCardProps = { content: React.ReactNode; tooltip?: React.ReactNode; action?: React.ReactNode; + style?: React.CSSProperties; }; -export function SettingsCard({ title, content, tooltip, action }: SettingsCardProps) { +export function SettingsCard({ title, content, tooltip, action, style }: SettingsCardProps) { return ( - +
@@ -18,7 +19,7 @@ export function SettingsCard({ title, content, tooltip, action }: SettingsCardPr {tooltip != null ? ( - + ) : null}
diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx index 947351db34d..fb8f6f44e49 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx @@ -36,6 +36,8 @@ import { getSupportedValueRangeForElementClass } from "viewer/model/bucket_data_ import type { BoundingBoxObject } from "viewer/store"; import { AxisRotationSettingForDataset } from "./dataset_rotation_form_item"; import { useDatasetSettingsContext } from "./dataset_settings_context"; +import { SettingsTitle } from "admin/account/helpers/settings_title"; +import { SettingsCard } from "admin/account/helpers/settings_card"; const FormItem = Form.Item; @@ -101,9 +103,9 @@ export default function DatasetSettingsDataTab({ return (
-
-
+ +
- + + + + + + + + - - - - - - - - value?.every((el) => el > 0), - "Each component of the voxel size must be greater than 0", - ), - }, - ]} - > - - - - - ({ + value: unit, + label: ( + + {LongUnitToShortUnitMap[unit]} + + ), + }))} + /> + + + + } + style={{ marginBottom: 24 }} + /> - - Layers - + + + + + } - > - {dataSource?.dataLayers?.map((layer: DataLayer, idx: number) => ( - // the layer name may change in this view, the order does not, so idx is the right key choice here - - + + {dataSource?.dataLayers?.map((layer: DataLayer, idx: number) => ( + // the layer name may change in this view, the order does not, so idx is the right key choice here + + + + } /> - - ))} - + + + ))} ); } diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx index 62e6e1d274e..d80176a7ac6 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx @@ -84,6 +84,17 @@ const MENU_ITEMS: MenuItemGroupType[] = [ // MENU_ITEMS[0].children.push(); // } +const FIXED_SIDER_STYLE: React.CSSProperties = { + overflow: "auto", + height: "100vh", + position: "sticky", + insetInlineStart: 0, + top: 0, + bottom: 0, + scrollbarWidth: "thin", + scrollbarGutter: "stable", +}; + const DatasetSettingsView: React.FC = () => { const { form, @@ -212,6 +223,7 @@ const DatasetSettingsView: React.FC = () => { { title: "Dataset Settings", }, + { title: dataset?.name }, { title: BREADCRUMB_LABELS[selectedKey as keyof typeof BREADCRUMB_LABELS], }, From 131598dbf862533712024046d15b38cc6e6d228f Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 1 Jul 2025 14:32:56 +0200 Subject: [PATCH 7/9] WIP --- .../dataset/dataset_rotation_form_item.tsx | 5 +- .../dataset/dataset_settings_context.tsx | 9 +- .../dataset/dataset_settings_data_tab.tsx | 30 ++- .../dataset/dataset_settings_provider.tsx | 188 ++++++++---------- .../dataset/dataset_settings_view.tsx | 148 +++++++------- .../dataset_settings_viewconfig_tab.tsx | 37 ++-- .../dashboard/dataset/useBeforeUnload_hook.ts | 2 +- 7 files changed, 192 insertions(+), 227 deletions(-) diff --git a/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx b/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx index 38bee1be18a..bbc1e778347 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx @@ -42,7 +42,10 @@ export const AxisRotationFormItem: React.FC = ({ form, axis, }: AxisRotationFormItemProps) => { - const dataLayers: APIDataLayer[] = Form.useWatch(["dataSource", "dataLayers"], form); + const dataLayers: APIDataLayer[] = Form.useWatch(["dataSource", "dataLayers"], { + form, + preserve: true, + }); const datasetBoundingBox = useMemo( () => getDatasetBoundingBoxFromLayers(dataLayers), [dataLayers], diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_context.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_context.tsx index 82a09e053a0..92202ba3748 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_context.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_context.tsx @@ -13,19 +13,14 @@ export type FormData = { datasetRotation?: DatasetRotationAndMirroringSettings; }; - export type DatasetSettingsContextValue = { form: FormInstance; isLoading: boolean; - hasUnsavedChanges: boolean; dataset: APIDataset | null | undefined; datasetId: string; datasetDefaultConfiguration: DatasetConfiguration | null | undefined; activeDataSourceEditMode: "simple" | "advanced"; - savedDataSourceOnServer: APIDataSource | null | undefined; isEditingMode: boolean; - onComplete: () => void; - onCancel: () => void; handleSubmit: () => void; handleCancel: () => void; handleDataSourceEditModeChange: (activeEditMode: "simple" | "advanced") => void; @@ -40,9 +35,7 @@ export const DatasetSettingsContext = createContext { const context = useContext(DatasetSettingsContext); if (!context) { - throw new Error( - "useDatasetSettingsContext must be used within a DatasetSettingsProvider", - ); + throw new Error("useDatasetSettingsContext must be used within a DatasetSettingsProvider"); } return context; }; diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx index fb8f6f44e49..6ce0432df9b 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx @@ -80,15 +80,9 @@ export const syncDataSourceFields = ( } }; -export default function DatasetSettingsDataTab({ - onChange, -}: { - // Form is passed explicitly because syncDataSourceFields is a utility function - // that needs the form instance, and it's called from the parent. - // The rest of the props are now derived from context. - onChange: (arg0: "simple" | "advanced") => void; -}) { - const { dataset, form, activeDataSourceEditMode } = useDatasetSettingsContext(); +export default function DatasetSettingsDataTab() { + const { dataset, form, activeDataSourceEditMode, handleDataSourceEditModeChange } = + useDatasetSettingsContext(); // Using the return value of useWatch for the `dataSource` var // yields outdated values. Therefore, the hook only exists for listening. Form.useWatch("dataSource", form); @@ -98,7 +92,6 @@ export default function DatasetSettingsDataTab({ const datasetStoredLocationInfo = dataset ? ` (as stored on datastore ${dataset?.dataStore.name} at ${dataset?.owningOrganization}/${dataset?.directoryName})` : ""; - const isJSONValid = isValidJSON(dataSourceJson); return ( @@ -125,7 +118,7 @@ export default function DatasetSettingsDataTab({ }} onChange={(bool) => { const key = bool ? "advanced" : "simple"; - onChange(key); + handleDataSourceEditModeChange(key); }} /> @@ -193,11 +186,15 @@ function SimpleDatasetForm({ }); syncDataSourceFields(form, "advanced"); }; + const marginBottom: React.CSSProperties = { + marginBottom: 24, + }; return (
@@ -289,11 +286,11 @@ function SimpleDatasetForm({ } - style={{ marginBottom: 24 }} /> @@ -309,6 +306,7 @@ function SimpleDatasetForm({