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 - - - )} - - - {getUserAccessList()} + + + {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 9e72fb5d419..cdad574641a 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx @@ -1,391 +1,81 @@ -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"; + CodeSandboxOutlined, + DeleteOutlined, + ExclamationCircleOutlined, + ExportOutlined, + FileTextOutlined, + SettingOutlined, + TeamOutlined, +} from "@ant-design/icons"; +import { Alert, Breadcrumb, Button, Form, Layout, Menu, Tooltip } from "antd"; +import type { ItemType } from "antd/es/menu/interface"; +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 features from "features"; -import { handleGenericError } from "libs/error_handling"; 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, useState } 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 type React from "react"; +import { useCallback } from "react"; +import { + Redirect, + Route, + type RouteComponentProps, + Switch, + useHistory, + useLocation, +} from "react-router-dom"; import { Unicode } from "viewer/constants"; 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 { Sider, Content } = Layout; 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 BREADCRUMB_LABELS = { + data: "Data Source", + sharing: "Sharing & Permissions", + metadata: "Metadata", + defaultConfig: "View Configuration", + delete: "Delete Dataset", }; -const DatasetSettingsView: React.FC = ({ - datasetId, - isEditingMode, - onComplete, - onCancel, -}) => { - const [form] = Form.useForm(); - const queryClient = useQueryClient(); +const DatasetSettingsView: React.FC = () => { + const { + form, + isEditingMode, + dataset, + datasetId, + handleSubmit, + handleCancel, + onValuesChange, + getFormValidationSummary, + } = useDatasetSettingsContext(); const isUserAdmin = useWkSelector((state) => state.activeUser?.isAdmin || false); + const location = useLocation(); + const history = useHistory(); + const selectedKey = location.pathname.split("/").filter(Boolean).pop() || "data"; - 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 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 switchToProblematicTab = useCallback(() => { - const validationSummary = getFormValidationSummary(); - - if (validationSummary[activeTabKey]) { - // Active tab is already problematic - return; - } - - // Switch to the earliest, problematic tab - const problematicTab = _.find( - ["data", "general", "defaultConfig"], - (key) => validationSummary[key], - ); - - if (problematicTab) { - setActiveTabKey(problematicTab as TabKey); - } - }, [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 switchToProblematicTab = useCallback(() => { + // const validationSummary = getFormValidationSummary(); - const handleDataSourceEditModeChange = useCallback( - (activeEditMode: "simple" | "advanced") => { - syncDataSourceFields(form, activeEditMode); - form.validateFields(); - setActiveDataSourceEditMode(activeEditMode); - }, - [form], - ); + // if (validationSummary[activeTabKey]) { + // // Active tab is already problematic + // return; + // } - // Setup beforeunload handling - useBeforeUnload(hasUnsavedChanges, messages["dataset.leave_with_unsaved_changes"]); + // // Switch to the earliest, problematic tab + // const problematicTab = ["data", "general", "defaultConfig"].find( + // (key) => validationSummary[key], + // ); - // 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]); + // if (problematicTab) { + // setActiveTabKey(problematicTab as TabKey); + // } + // }, [getFormValidationSummary, activeTabKey]); const getMessageComponents = useCallback(() => { if (dataset == null) { @@ -447,22 +137,9 @@ const DatasetSettingsView: React.FC = ({ ); }, [dataset, isEditingMode]); + const titleString = isEditingMode ? "Dataset Settings" : "Import"; const maybeStoredDatasetName = dataset?.name || datasetId; - const maybeDataSourceId = dataset - ? { - owningOrganization: dataset.owningOrganization, - directoryName: dataset.directoryName, - } - : null; - const titleString = isEditingMode ? "Settings for" : "Import"; - const datasetLinkOrName = isEditingMode ? ( - - {maybeStoredDatasetName} - - ) : ( - maybeStoredDatasetName - ); const confirmString = isEditingMode || (dataset != null && dataset.dataSource.status == null) ? "Save" : "Import"; const formErrors = getFormValidationSummary(); @@ -477,108 +154,122 @@ const DatasetSettingsView: React.FC = ({ ); - const tabs = [ + const menuItems: ItemType[] = [ { - label: Data {formErrors.data ? errorIcon : ""}, - key: "data", - forceRender: true, - children: ( - - ), + label: titleString, + type: "group", + children: [ + { + key: "data", + icon: formErrors.data ? errorIcon : , + label: "Data Source", + }, + { + key: "sharing", + icon: formErrors.sharing ? errorIcon : , + label: "Sharing & Permissions", + }, + { + key: "metadata", + icon: formErrors.metadata ? errorIcon : , + label: "Metadata", + }, + { + key: "defaultConfig", + icon: formErrors.defaultConfig ? errorIcon : , + label: "View Configuration", + }, + isUserAdmin && features().allowDeleteDatasets + ? { + key: "delete", + icon: , + label: "Delete", + } + : null, + ], }, - + { type: "divider" }, { - label: Sharing & Permissions {formErrors.general ? errorIcon : null}, - key: "sharing", - forceRender: true, - children: ( - - ), + type: "group", + children: isEditingMode + ? [ + { + key: "open", + icon: , + label: "Open in WEBKNOSSOS", + onClick: () => + window.open( + `/datasets/${dataset ? getReadableURLPart(dataset) : datasetId}/view`, + "_blank", + "noopener", + ), + }, + ] + : [], }, + ]; + const breadcrumbItems = [ { - label: Metadata, - key: "general", - forceRender: true, - children: ( - - ), + title: titleString, }, - + { title: maybeStoredDatasetName }, { - 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)} - items={tabs} + + history.push(`/datasets/${datasetId}/edit/${key}`)} + /> + + + + {getMessageComponents()} + + + + + + + + ) => ( + + )} /> - + = ({ {confirmString} {Unicode.NonBreakingSpace} - + - - - + + + ); }; diff --git a/frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx b/frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx index 3ac2277deb6..7fdf1f4a04e 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx +++ b/frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx @@ -1,27 +1,12 @@ import { InfoCircleOutlined } from "@ant-design/icons"; +import { SettingsCard, type SettingsCardProps } from "admin/account/helpers/settings_card"; +import { SettingsTitle } from "admin/account/helpers/settings_title"; import { getAgglomeratesForDatasetLayer, getMappingsForDatasetLayer } from "admin/rest_api"; -import { - Alert, - Checkbox, - Col, - Divider, - Form, - Input, - InputNumber, - Row, - Select, - Table, - Tooltip, -} from "antd"; +import { Col, Form, Input, InputNumber, Row, Select, Switch, Table, Tooltip } from "antd"; import { Slider } from "components/slider"; import { Vector3Input } from "libs/vector_input"; import _ from "lodash"; -import messages, { - type RecommendedConfiguration, - layerViewConfigurations, - settings, - settingsTooltips, -} from "messages"; +import messages, { layerViewConfigurations, settings, settingsTooltips } from "messages"; import { useMemo, useState } from "react"; import type { APIDataSourceId } from "types/api_types"; import { getDefaultLayerViewConfiguration } from "types/schemas/dataset_view_configuration.schema"; @@ -29,19 +14,26 @@ 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 { FormItemWithInfo, jsonEditStyle } from "./helper_components"; +import { useDatasetSettingsContext } from "./dataset_settings_context"; +import { jsonEditStyle } from "./helper_components"; const FormItem = Form.Item; -export default function DatasetSettingsViewConfigTab(props: { - dataSourceId: APIDataSourceId; - dataStoreURL: string | undefined; -}) { - const { dataSourceId, dataStoreURL } = props; +export default function DatasetSettingsViewConfigTab() { const [availableMappingsPerLayerCache, setAvailableMappingsPerLayer] = useState< Record >({}); + const { dataset } = useDatasetSettingsContext(); + const dataStoreURL = dataset?.dataStore.url; + const dataSourceId: APIDataSourceId | null = dataset + ? { + owningOrganization: dataset.owningOrganization, + directoryName: dataset.directoryName, + } + : null; + + // biome-ignore lint/correctness/useExhaustiveDependencies: validate on dataset change const validateDefaultMappings = useMemo( () => async (configStr: string, dataStoreURL: string, dataSourceId: APIDataSourceId) => { let config = {} as DatasetConfiguration["layers"]; @@ -92,7 +84,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 = [ @@ -158,175 +150,191 @@ export default function DatasetSettingsViewConfigTab(props: { }; }, ); - const checkboxSettings = ( - [ - ["interpolation", 6], - ["fourBit", 6], - ["renderMissingDataBlack", 6], - ] as Array<[keyof RecommendedConfiguration, number]> - ).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 as string, + tooltip: settingsTooltips.interpolation, + content: ( + + + + ), + }, + { + title: settings.fourBit as string, + tooltip: settingsTooltips.fourBit, + content: ( + + + + ), + }, + { + title: settings.renderMissingDataBlack as string, + tooltip: settingsTooltips.renderMissingDataBlack, + content: ( + + + + ), + }, + { + title: settings.segmentationPatternOpacity as string, + tooltip: settingsTooltips.segmentationPatternOpacity, + content: ( + + + + + + + + + + + + + ), + }, + { + title: settings.blendMode as string, + tooltip: settingsTooltips.blendMode, + content: ( + + + + ), + }, + { + title: settings.loadingStrategy as string, + 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.", - ), - }, - ]} - > - - - - - - - - - - {checkboxSettings} - - - - - - - - - - - - - - - - - - - - - - - - - + + {viewConfigItems.map((item) => ( + + + + ))} - - - - - - - - - + + - - Promise.all([ - 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/dashboard/dataset/helper_components.tsx b/frontend/javascripts/dashboard/dataset/helper_components.tsx index 53f4ac04462..a41eecca0cd 100644 --- a/frontend/javascripts/dashboard/dataset/helper_components.tsx +++ b/frontend/javascripts/dashboard/dataset/helper_components.tsx @@ -2,9 +2,9 @@ import { InfoCircleOutlined } from "@ant-design/icons"; import { Alert, Form, Modal, Tooltip } from "antd"; import type { FormItemProps, Rule } from "antd/lib/form"; import type { NamePath } from "antd/lib/form/interface"; -import _ from "lodash"; +import { sum } from "lodash"; import type { FieldError } from "rc-field-form/es/interface"; -import * as React from "react"; +import React from "react"; const FormItem = Form.Item; @@ -125,5 +125,5 @@ export const hasFormError = (formErrors: FieldError[], key: string): boolean => const errorsForKey = formErrors.map((errorObj) => errorObj.name[0] === key ? errorObj.errors.length : 0, ); - return _.sum(errorsForKey) > 0; + return sum(errorsForKey) > 0; }; diff --git a/frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts b/frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts index 315afd2e89d..66a09282334 100644 --- a/frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts +++ b/frontend/javascripts/dashboard/dataset/useBeforeUnload_hook.ts @@ -31,7 +31,7 @@ const useBeforeUnload = (hasUnsavedChanges: boolean, message: string) => { // Only show the prompt if this is a proper beforeUnload event from the browser // or the pathname changed // This check has to be done because history.block triggers this function even if only the url hash changed - if (action === undefined || newLocation.pathname !== window.location.pathname) { + if (action === undefined || !newLocation.pathname.includes("/datasets")) { if (hasUnsavedChanges) { window.onbeforeunload = null; // clear the event handler otherwise it would be called twice. Once from history.block once from the beforeunload event blockTimeoutIdRef.current = window.setTimeout(() => { 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", diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index 84dcf22e2a3..3cb69beaefa 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -69,6 +69,7 @@ import AiModelListView from "admin/voxelytics/ai_model_list_view"; import { CheckCertificateModal } from "components/check_certificate_modal"; import ErrorBoundary from "components/error_boundary"; import { CheckTermsOfServices } from "components/terms_of_services_check"; +import { DatasetSettingsProvider } from "dashboard/dataset/dataset_settings_provider"; import loadable from "libs/lazy_loader"; import type { EmptyObject } from "types/globals"; import { getDatasetIdOrNameFromReadableURLPart } from "viewer/model/accessors/dataset_accessor"; @@ -545,12 +546,23 @@ class ReactRouter extends React.Component { ); } return ( - window.history.back()} - onCancel={() => window.history.back()} - /> + + + + ); + }} + /> + { + const { datasetId } = getDatasetIdOrNameFromReadableURLPart( + match.params.datasetNameAndId, + ); + return ( + + + ); }} />