From 0c795771019a641935e447330bdb646c9fd3c209 Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 11 Dec 2024 15:03:08 +0000 Subject: [PATCH 1/4] Handle storage quota exceeding via a dialog --- lang/ui.en.json | 8 ++ src/components/DefaultPageLayout.tsx | 2 + .../StorageQuotaExceededErrorDialog.tsx | 60 ++++++++++ src/messages/ui.en.json | 12 ++ src/store.ts | 103 ++++++++++++------ 5 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 src/components/StorageQuotaExceededErrorDialog.tsx diff --git a/lang/ui.en.json b/lang/ui.en.json index 86bcd4c85..0cd91d6a8 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -1447,6 +1447,14 @@ "defaultMessage": "Stop recording", "description": "Button label to stop recording movement data while recording multiple samples" }, + "storage-quota-exceeded-dialog-body": { + "defaultMessage": "You have reached your session's storage limit. Recent changes made on your session cannot be saved. Please reload the page to continue your session.", + "description": "Body of storage error dialog" + }, + "storage-quota-exceeded-dialog-title": { + "defaultMessage": "Error auto-saving your session", + "description": "Title of storage error dialog" + }, "support-request": { "defaultMessage": "Please consider raising a support request.", "description": "Support request link text" diff --git a/src/components/DefaultPageLayout.tsx b/src/components/DefaultPageLayout.tsx index a0ee4be52..396098365 100644 --- a/src/components/DefaultPageLayout.tsx +++ b/src/components/DefaultPageLayout.tsx @@ -37,6 +37,7 @@ import NotCreateAiHexImportDialog from "./NotCreateAiHexImportDialog"; import PreReleaseNotice from "./PreReleaseNotice"; import ProjectDropTarget from "./ProjectDropTarget"; import SaveDialogs from "./SaveDialogs"; +import StorageQuotaExceededErrorDialog from "./StorageQuotaExceededErrorDialog"; interface DefaultPageLayoutProps { titleId?: string; @@ -92,6 +93,7 @@ const DefaultPageLayout = ({ isOpen={postImportDialogState === PostImportDialogState.Error} /> + { + const isOpen = useStore((s) => s.isStorageQuotaExceededDialogOpen); + return ( + {}} + size="2xl" + isCentered + > + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default StorageQuotaExceededErrorDialog; diff --git a/src/messages/ui.en.json b/src/messages/ui.en.json index 1353ebc11..7e14df28a 100644 --- a/src/messages/ui.en.json +++ b/src/messages/ui.en.json @@ -2563,6 +2563,18 @@ "value": "Stop recording" } ], + "storage-quota-exceeded-dialog-body": [ + { + "type": 0, + "value": "You have reached your session's storage limit. Recent changes made on your session cannot be saved. Please reload the page to continue your session." + } + ], + "storage-quota-exceeded-dialog-title": [ + { + "type": 0, + "value": "Error auto-saving your session" + } + ], "support-request": [ { "type": 0, diff --git a/src/store.ts b/src/store.ts index c0bc7f84e..e883dee34 100644 --- a/src/store.ts +++ b/src/store.ts @@ -7,7 +7,7 @@ import { Project } from "@microbit/makecode-embed/react"; import * as tf from "@tensorflow/tfjs"; import { create } from "zustand"; -import { devtools, persist } from "zustand/middleware"; +import { createJSONStorage, devtools, persist } from "zustand/middleware"; import { useShallow } from "zustand/react/shallow"; import { deployment } from "./deployment"; import { flags } from "./flags"; @@ -192,6 +192,7 @@ export interface State { isNameProjectDialogOpen: boolean; isRecordingDialogOpen: boolean; isConnectToRecordDialogOpen: boolean; + isStorageQuotaExceededDialogOpen: boolean; } export interface ConnectOptions { @@ -327,6 +328,7 @@ const createMlStore = (logging: Logging) => { isRecordingDialogOpen: false, isConnectToRecordDialogOpen: false, isDeleteActionDialogOpen: false, + isStorageQuotaExceededDialogOpen: false, isIncompatibleEditorDeviceDialogOpen: false, setSettings(update: Partial) { @@ -587,39 +589,51 @@ const createMlStore = (logging: Logging) => { }, loadDataset(newActions: ActionData[]) { - set(({ project, projectEdited, settings }) => { - const dataWindow = getDataWindowFromActions(newActions); - return { - settings: { - ...settings, - toursCompleted: Array.from( - new Set([...settings.toursCompleted, "DataSamplesRecorded"]) - ), - }, - actions: (() => { - const copy = newActions.map((a) => ({ ...a })); - for (const a of copy) { - if (!a.icon) { - a.icon = actionIcon({ - isFirstAction: false, - existingActions: copy, - }); + try { + set(({ project, projectEdited, settings }) => { + const dataWindow = getDataWindowFromActions(newActions); + return { + settings: { + ...settings, + toursCompleted: Array.from( + new Set([ + ...settings.toursCompleted, + "DataSamplesRecorded", + ]) + ), + }, + actions: (() => { + const copy = newActions.map((a) => ({ ...a })); + for (const a of copy) { + if (!a.icon) { + a.icon = actionIcon({ + isFirstAction: false, + existingActions: copy, + }); + } } - } - return copy; - })(), - dataWindow, - model: undefined, - timestamp: Date.now(), - ...updateProject( - project, - projectEdited, - newActions, - undefined, - dataWindow - ), - }; - }); + return copy; + })(), + dataWindow, + model: undefined, + timestamp: Date.now(), + ...updateProject( + project, + projectEdited, + newActions, + undefined, + dataWindow + ), + }; + }); + } catch (e) { + if ((e as Error).name === "QuotaExceededError") { + return set({ + isStorageQuotaExceededDialogOpen: true, + }); + } + throw e; + } }, /** @@ -1135,6 +1149,7 @@ const createMlStore = (logging: Logging) => { isConnectToRecordDialogOpen, isDeleteActionDialogOpen, isIncompatibleEditorDeviceDialogOpen, + isStorageQuotaExceededDialogOpen, save, } = get(); return ( @@ -1147,6 +1162,7 @@ const createMlStore = (logging: Logging) => { isRecordingDialogOpen || isConnectToRecordDialogOpen || isDeleteActionDialogOpen || + isStorageQuotaExceededDialogOpen || isIncompatibleEditorDeviceDialogOpen || postImportDialogState !== PostImportDialogState.None || isEditorOpen || @@ -1161,6 +1177,7 @@ const createMlStore = (logging: Logging) => { { version: 1, name: "ml", + storage: createJSONStorage(() => mlStorage), partialize: ({ actions, project, @@ -1211,6 +1228,26 @@ const createMlStore = (logging: Logging) => { ); }; +const mlStorage = { + getItem: localStorage.getItem, + setItem: (name: string, value: string) => { + try { + localStorage.setItem(name, value); + } catch (e) { + if ((e as Error).name === "QuotaExceededError") { + const prevValue = JSON.parse(value) as object; + const newValue = { + ...prevValue, + isStorageQuotaExceededDialogOpen: true, + }; + return localStorage.setItem(name, JSON.stringify(newValue)); + } + throw e; + } + }, + removeItem: localStorage.removeItem, +}; + export const useStore = createMlStore(deployment.logging); const getDataWindowFromActions = (actions: ActionData[]): DataWindow => { From 5e46c31aea8a56cbb65f663d49a6b26547acffb0 Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 11 Dec 2024 15:08:10 +0000 Subject: [PATCH 2/4] Revert changes in loadDataset --- src/store.ts | 76 ++++++++++++++++++++++------------------------------ 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/src/store.ts b/src/store.ts index e883dee34..f9c7f4854 100644 --- a/src/store.ts +++ b/src/store.ts @@ -589,51 +589,39 @@ const createMlStore = (logging: Logging) => { }, loadDataset(newActions: ActionData[]) { - try { - set(({ project, projectEdited, settings }) => { - const dataWindow = getDataWindowFromActions(newActions); - return { - settings: { - ...settings, - toursCompleted: Array.from( - new Set([ - ...settings.toursCompleted, - "DataSamplesRecorded", - ]) - ), - }, - actions: (() => { - const copy = newActions.map((a) => ({ ...a })); - for (const a of copy) { - if (!a.icon) { - a.icon = actionIcon({ - isFirstAction: false, - existingActions: copy, - }); - } - } - return copy; - })(), - dataWindow, - model: undefined, - timestamp: Date.now(), - ...updateProject( - project, - projectEdited, - newActions, - undefined, - dataWindow + set(({ project, projectEdited, settings }) => { + const dataWindow = getDataWindowFromActions(newActions); + return { + settings: { + ...settings, + toursCompleted: Array.from( + new Set([...settings.toursCompleted, "DataSamplesRecorded"]) ), - }; - }); - } catch (e) { - if ((e as Error).name === "QuotaExceededError") { - return set({ - isStorageQuotaExceededDialogOpen: true, - }); - } - throw e; - } + }, + actions: (() => { + const copy = newActions.map((a) => ({ ...a })); + for (const a of copy) { + if (!a.icon) { + a.icon = actionIcon({ + isFirstAction: false, + existingActions: copy, + }); + } + } + return copy; + })(), + dataWindow, + model: undefined, + timestamp: Date.now(), + ...updateProject( + project, + projectEdited, + newActions, + undefined, + dataWindow + ), + }; + }); }, /** From ff76598730cd6b60282fc3ac798bd69b7a1143e7 Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 11 Dec 2024 16:36:59 +0000 Subject: [PATCH 3/4] Use storage quota exceeded key to control dialog --- src/components/DefaultPageLayout.tsx | 11 ++++++++--- .../StorageQuotaExceededErrorDialog.tsx | 10 +++++++--- src/store.ts | 15 +++++++++------ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/components/DefaultPageLayout.tsx b/src/components/DefaultPageLayout.tsx index 396098365..28a39e5a6 100644 --- a/src/components/DefaultPageLayout.tsx +++ b/src/components/DefaultPageLayout.tsx @@ -24,7 +24,7 @@ import { useProject } from "../hooks/project-hooks"; import { keyboardShortcuts, useShortcut } from "../keyboard-shortcut-hooks"; import { PostImportDialogState } from "../model"; import Tour from "../pages/Tour"; -import { useStore } from "../store"; +import { isStorageQuotaExceeded, useStore } from "../store"; import { createHomePageUrl } from "../urls"; import ActionBar from "./ActionBar/ActionBar"; import ItemsRight from "./ActionBar/ActionBarItemsRight"; @@ -77,11 +77,14 @@ const DefaultPageLayout = ({ const isFeedbackOpen = useStore((s) => s.isFeedbackFormOpen); const closeDialog = useStore((s) => s.closeDialog); + const isStorageQuotaExceededDialogOpen = isStorageQuotaExceeded(); return ( <> {/* Suppress dialogs to prevent overlapping dialogs */} - {!isNonConnectionDialogOpen && } + {!isNonConnectionDialogOpen && !isStorageQuotaExceededDialogOpen && ( + + )} - + { - const isOpen = useStore((s) => s.isStorageQuotaExceededDialogOpen); +interface StorageQuotaExceededErrorDialogProps { + isOpen: boolean; +} + +const StorageQuotaExceededErrorDialog = ({ + isOpen, +}: StorageQuotaExceededErrorDialogProps) => { return ( { ); }; +const storageQuotaExceededKey = "QuotaExceededError"; const mlStorage = { getItem: localStorage.getItem, setItem: (name: string, value: string) => { @@ -1223,12 +1224,7 @@ const mlStorage = { localStorage.setItem(name, value); } catch (e) { if ((e as Error).name === "QuotaExceededError") { - const prevValue = JSON.parse(value) as object; - const newValue = { - ...prevValue, - isStorageQuotaExceededDialogOpen: true, - }; - return localStorage.setItem(name, JSON.stringify(newValue)); + return localStorage.setItem(storageQuotaExceededKey, "1"); } throw e; } @@ -1236,6 +1232,10 @@ const mlStorage = { removeItem: localStorage.removeItem, }; +export const isStorageQuotaExceeded = () => { + return localStorage.getItem(storageQuotaExceededKey) === "1"; +}; + export const useStore = createMlStore(deployment.logging); const getDataWindowFromActions = (actions: ActionData[]): DataWindow => { @@ -1245,6 +1245,9 @@ const getDataWindowFromActions = (actions: ActionData[]): DataWindow => { : currentDataWindow; }; +// Reset storage quota exceeded state +localStorage.setItem(storageQuotaExceededKey, "0"); + // Get data window from actions on app load. const { actions } = useStore.getState(); useStore.setState( From 10dfd59bd75d35817f40ea17c456b9c0af767713 Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 11 Dec 2024 16:38:52 +0000 Subject: [PATCH 4/4] Remove remnants of isStorageQuotaExceededDialogOpen --- src/store.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/store.ts b/src/store.ts index 4d5719187..4567e7bf5 100644 --- a/src/store.ts +++ b/src/store.ts @@ -192,7 +192,6 @@ export interface State { isNameProjectDialogOpen: boolean; isRecordingDialogOpen: boolean; isConnectToRecordDialogOpen: boolean; - isStorageQuotaExceededDialogOpen: boolean; } export interface ConnectOptions { @@ -328,7 +327,6 @@ const createMlStore = (logging: Logging) => { isRecordingDialogOpen: false, isConnectToRecordDialogOpen: false, isDeleteActionDialogOpen: false, - isStorageQuotaExceededDialogOpen: false, isIncompatibleEditorDeviceDialogOpen: false, setSettings(update: Partial) { @@ -1137,7 +1135,6 @@ const createMlStore = (logging: Logging) => { isConnectToRecordDialogOpen, isDeleteActionDialogOpen, isIncompatibleEditorDeviceDialogOpen, - isStorageQuotaExceededDialogOpen, save, } = get(); return ( @@ -1150,7 +1147,6 @@ const createMlStore = (logging: Logging) => { isRecordingDialogOpen || isConnectToRecordDialogOpen || isDeleteActionDialogOpen || - isStorageQuotaExceededDialogOpen || isIncompatibleEditorDeviceDialogOpen || postImportDialogState !== PostImportDialogState.None || isEditorOpen ||