Skip to content

Commit 8ccf34c

Browse files
committed
add migration view
1 parent 6c079d0 commit 8ccf34c

12 files changed

Lines changed: 285 additions & 44 deletions

File tree

webapp/public/locales/en/main.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@
127127
"global.offset": "Offset",
128128
"global.parameters": "Parameters",
129129
"global.home": "Home",
130+
"global.columns": "Columns",
131+
"global.retry": "Retry",
130132
"date": {
131133
"monday": "Monday",
132134
"tuesday": "Tuesday",
@@ -728,6 +730,13 @@
728730
"study.tableModes.create.error": "Failed to create the table: {{name}}",
729731
"study.tableModes.update.error": "Failed to update the table: {{name}}",
730732
"study.tableModes.delete.error": "Failed to delete the table: {{name}}",
733+
"study.tableModes.migration.title": "Migration of tables from browser to database",
734+
"study.tableModes.migration.error_one": "{{count}} table failed to migrate",
735+
"study.tableModes.migration.error_other": "{{count}} tables failed to migrate",
736+
"study.tableModes.migration.success_one": "{{count}} table migrated successfully",
737+
"study.tableModes.migration.success_other": "{{count}} tables migrated successfully",
738+
"study.tableModes.migration.deleteConfirm": "Tables that have not been migrated will be permanently deleted and cannot be recovered.",
739+
"study.tableModes.migration.closeConfirm": "The migration of tables that have not been migrated will be resumed the next time the page is opened.",
731740
"study.outputs": "Outputs",
732741
"study.outputs.mc": "Monte-Carlo",
733742
"study.outputs.monteCarlo": "Monte-Carlo",

webapp/public/locales/fr/main.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@
127127
"global.offset": "Décalage",
128128
"global.parameters": "Paramètres",
129129
"global.home": "Accueil",
130+
"global.columns": "Colonnes",
131+
"global.retry": "Réessayer",
130132
"date": {
131133
"monday": "Lundi",
132134
"tuesday": "Mardi",
@@ -728,6 +730,13 @@
728730
"study.tableModes.create.error": "Échec de la création de la table : {{name}}",
729731
"study.tableModes.update.error": "Échec de la mise à jour de la table : {{name}}",
730732
"study.tableModes.delete.error": "Échec de la suppression de la table : {{name}}",
733+
"study.tableModes.migration.title": "Migration des tables du navigateur vers la base de données",
734+
"study.tableModes.migration.error_one": "{{count}} table n'a pas pu être migrée",
735+
"study.tableModes.migration.error_other": "{{count}} tables n'ont pas pu être migrées",
736+
"study.tableModes.migration.success_one": "{{count}} table a été migrée avec succès",
737+
"study.tableModes.migration.success_other": "{{count}} tables ont été migrées avec succès",
738+
"study.tableModes.migration.deleteConfirm": "Les tables qui n'ont pas été migrées, seront supprimées définitivement et ne pourront plus être récupérées.",
739+
"study.tableModes.migration.closeConfirm": "La migration des tables qui n'ont pas été migrées, sera relancée lors de la prochaine ouverture de la page.",
731740
"study.outputs": "Sorties",
732741
"study.outputs.mc": "Monte-Carlo",
733742
"study.outputs.monteCarlo": "Monte-Carlo",

webapp/src/components/Fieldset.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function Fieldset({
9191
<>
9292
<Stack
9393
component="legend"
94-
gap={0.5}
94+
spacing={0.5}
9595
{...(canCollapse &&
9696
isLegendString && { onClick: handleToggleCollapse, sx: { cursor: "pointer" } })}
9797
>

webapp/src/components/ListPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function ListPanel<TItem extends ListPanelItem>({
9191
<Stack direction="column">
9292
{hasActions && (
9393
<Stack
94-
gap={1}
94+
spacing={1}
9595
sx={{
9696
p: 1,
9797
pb: 0,

webapp/src/components/page/TabsView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function TabsView({
121121
/>
122122
)}
123123
{primaryActions && (
124-
<Stack gap={1} sx={{ px: 1 }}>
124+
<Stack spacing={1} sx={{ px: 1 }}>
125125
{primaryActions}
126126
</Stack>
127127
)}
@@ -143,7 +143,7 @@ function TabsView({
143143
))}
144144
</TabList>
145145
{secondaryActions && (
146-
<Stack gap={1} sx={{ pr: 1 }}>
146+
<Stack spacing={1} sx={{ pr: 1 }}>
147147
{secondaryActions}
148148
</Stack>
149149
)}

webapp/src/routes/_authenticated/studies/$studyId/-components/InformationView/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function InformationView({ variantTree }: Props) {
8484

8585
return (
8686
<ViewWrapper flex={{ gap: 2 }}>
87-
<Stack flex={1} gap={1} sx={{ overflow: "auto" }}>
87+
<Stack flex={1} spacing={1} sx={{ overflow: "auto" }}>
8888
<LauncherHistory />
8989
{!study.archived && (
9090
<Stack direction="column" sx={{ height: 1, flex: 1, overflow: "hidden" }}>
@@ -99,8 +99,8 @@ function InformationView({ variantTree }: Props) {
9999

100100
<Box>
101101
<CustomScrollbar>
102-
<Stack gap={1} justifyContent="space-between">
103-
<Stack gap={1}>
102+
<Stack spacing={1} justifyContent="space-between">
103+
<Stack spacing={1}>
104104
{!study.archived && (
105105
<>
106106
<RouterButton

webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/needs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function ReservesNeeds() {
7070
}
7171

7272
return (
73-
<Stack direction="column" gap={1} sx={{ height: 1 }}>
73+
<Stack direction="column" spacing={1} sx={{ height: 1 }}>
7474
<SelectFE
7575
label={t("study.modeling.reserves.needs.select")}
7676
value={selectedReserveId}

webapp/src/routes/_authenticated/studies/$studyId/explore/outputs/index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ function Outputs() {
134134

135135
const renderCreationDate = (job: Job) => {
136136
return (
137-
<Stack gap={1}>
137+
<Stack spacing={1}>
138138
<CalendarTodayIcon fontSize="extra-small" />
139139
{convertUTCToLocalTime(job.creationDate)}
140140
</Stack>
@@ -143,7 +143,7 @@ function Outputs() {
143143

144144
const renderCompletionDate = (output: OutputWithJob) => {
145145
return (
146-
<Stack gap={1}>
146+
<Stack spacing={1}>
147147
<EventAvailableIcon fontSize="extra-small" />
148148
{
149149
// An output should always have a completed job — this is just a TS limitation caused
@@ -162,7 +162,7 @@ function Outputs() {
162162
const isDisabled = output.archived || isOutputOptimistic;
163163

164164
return (
165-
<Stack gap={2}>
165+
<Stack spacing={2}>
166166
{isOutputOptimistic && <CircularProgress size="1rem" color="inherit" />}
167167
<RouterLink
168168
color="inherit"
@@ -221,14 +221,14 @@ function Outputs() {
221221
runningJobs.map((job) => (
222222
<TableRow key={job.id}>
223223
<TableCell component="th" scope="row">
224-
<Stack gap={2}>
224+
<Stack spacing={2}>
225225
<CircularProgress size="1rem" color="inherit" />
226226
<Typography>{job.id}</Typography>
227227
</Stack>
228228
</TableCell>
229229
<TableCell align="right">{job.owner?.name}</TableCell>
230230
<TableCell align="right">
231-
<Stack gap={2} justifyContent="flex-end">
231+
<Stack spacing={2} justifyContent="flex-end">
232232
{renderCreationDate(job)}
233233
</Stack>
234234
</TableCell>
@@ -249,7 +249,7 @@ function Outputs() {
249249
</TableCell>
250250
<TableCell align="right">{output.job?.owner?.name}</TableCell>
251251
<TableCell align="right">
252-
<Stack gap={2} justifyContent="flex-end">
252+
<Stack spacing={2} justifyContent="flex-end">
253253
{output.job && renderCreationDate(output.job)}
254254
{renderCompletionDate(output)}
255255
</Stack>
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* Copyright (c) 2026, RTE (https://www.rte-france.com)
3+
*
4+
* See AUTHORS.txt
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public
7+
* License, v. 2.0. If a copy of the MPL was not distributed with this
8+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
*
10+
* SPDX-License-Identifier: MPL-2.0
11+
*
12+
* This file is part of the Antares project.
13+
*/
14+
15+
import LinearProgressWithLabel from "@/components/LinearProgressWithLabel";
16+
import useDialog from "@/hooks/useDialog";
17+
import { tableModeCreationSchema } from "@/services/api/tablemode/schemas";
18+
import type { TableModeCreation } from "@/services/api/tablemode/types";
19+
import storage from "@/services/utils/localStorage";
20+
import { appendColon } from "@/utils/i18nUtils";
21+
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
22+
import {
23+
Backdrop,
24+
Box,
25+
Button,
26+
IconButton,
27+
List,
28+
ListItem,
29+
ListItemText,
30+
Paper,
31+
Stack,
32+
Tooltip,
33+
Typography,
34+
} from "@mui/material";
35+
import axios, { AxiosError } from "axios";
36+
import { useState } from "react";
37+
import { useTranslation } from "react-i18next";
38+
import { useMount } from "react-use";
39+
import useCreateTableMode from "../../-hooks/useCreateTableMode";
40+
41+
interface MigrationState {
42+
status: "disabled" | "loading" | "success" | "error";
43+
migrated: number;
44+
failed: Array<[TableModeCreation, AxiosError | undefined]>;
45+
total: number;
46+
}
47+
48+
const DEFAULT_MIGRATION_STATE: MigrationState = {
49+
status: "disabled",
50+
migrated: 0,
51+
failed: [],
52+
total: 0,
53+
} as const;
54+
55+
const LOCAL_STORAGE_KEY = "studies.model.tableMode.templates" as const;
56+
57+
/**
58+
* Workaround to migrate table modes stored in localStorage to the new backend system (since v2.32.0).
59+
* It can be removed after a certain period of time when we are confident that most users have migrated.
60+
*/
61+
62+
function MigrationTableModes() {
63+
const createTableMode = useCreateTableMode({ notifyError: false });
64+
const { t } = useTranslation();
65+
const [state, setState] = useState<MigrationState>(DEFAULT_MIGRATION_STATE);
66+
const { confirm } = useDialog();
67+
68+
useMount(migrateTableModes);
69+
70+
////////////////////////////////////////////////////////////////
71+
// Functions
72+
////////////////////////////////////////////////////////////////
73+
74+
async function migrateTableModes() {
75+
const oldTableModes = storage.getItem(LOCAL_STORAGE_KEY);
76+
77+
// Invalid data or no data to migrate
78+
if (!Array.isArray(oldTableModes) || oldTableModes.length === 0) {
79+
storage.removeItem(LOCAL_STORAGE_KEY);
80+
return;
81+
}
82+
83+
setState({
84+
...DEFAULT_MIGRATION_STATE,
85+
status: "loading",
86+
total: oldTableModes.length,
87+
});
88+
89+
const failed: MigrationState["failed"] = [];
90+
91+
for (const value of oldTableModes) {
92+
// Ignore invalid table modes
93+
if (!tableModeCreationSchema.safeParse(value).success) {
94+
continue;
95+
}
96+
97+
try {
98+
await createTableMode.mutateAsync(value);
99+
setState((prev) => ({ ...prev, migrated: prev.migrated + 1 }));
100+
} catch (err) {
101+
failed.push([value, axios.isAxiosError(err) ? err : undefined]);
102+
}
103+
}
104+
105+
// If all table modes have been migrated successfully, we can remove the localStorage key
106+
if (failed.length === 0) {
107+
storage.removeItem(LOCAL_STORAGE_KEY);
108+
}
109+
// If some table modes failed to migrate, we keep them in localStorage to retry later
110+
else {
111+
storage.setItem(
112+
LOCAL_STORAGE_KEY,
113+
failed.map(([tableModeCreation]) => tableModeCreation),
114+
);
115+
}
116+
117+
setState({
118+
status: failed.length > 0 ? "error" : "success",
119+
migrated: oldTableModes.length - failed.length,
120+
failed,
121+
total: oldTableModes.length,
122+
});
123+
}
124+
125+
////////////////////////////////////////////////////////////////
126+
// Event Handlers
127+
////////////////////////////////////////////////////////////////
128+
129+
const handleDelete = async () => {
130+
const isConfirmed = await confirm({
131+
content: t("study.tableModes.migration.deleteConfirm"),
132+
alert: "error",
133+
});
134+
135+
if (isConfirmed) {
136+
storage.removeItem(LOCAL_STORAGE_KEY);
137+
setState(DEFAULT_MIGRATION_STATE);
138+
}
139+
};
140+
141+
const handleClose = async () => {
142+
if (state.status === "success") {
143+
setState(DEFAULT_MIGRATION_STATE);
144+
return;
145+
}
146+
147+
const isConfirmed = await confirm({
148+
content: t("study.tableModes.migration.closeConfirm"),
149+
alert: "warning",
150+
});
151+
152+
if (isConfirmed) {
153+
setState(DEFAULT_MIGRATION_STATE);
154+
}
155+
};
156+
157+
////////////////////////////////////////////////////////////////
158+
// JSX
159+
////////////////////////////////////////////////////////////////
160+
161+
if (state.status === "disabled") {
162+
return null;
163+
}
164+
165+
return (
166+
<Backdrop open sx={{ position: "absolute" }}>
167+
<Paper
168+
sx={{
169+
display: "flex",
170+
flexDirection: "column",
171+
gap: 1,
172+
}}
173+
>
174+
<Box sx={{ p: 2 }}>
175+
<Typography variant="h6">{t("study.tableModes.migration.title")}</Typography>
176+
{/* Loading */}
177+
{state.status === "loading" && (
178+
<LinearProgressWithLabel value={(state.migrated / state.total) * 100} />
179+
)}
180+
{/* Error */}
181+
{state.status === "error" && (
182+
<>
183+
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
184+
{t("study.tableModes.migration.error", { count: state.failed.length })}
185+
</Typography>
186+
<List dense sx={{ overflow: "auto" }}>
187+
{state.failed.map(([tableModeCreation, error]) => (
188+
<ListItem
189+
key={tableModeCreation.name}
190+
secondaryAction={
191+
<Tooltip
192+
title={`${appendColon(t("global.columns"))} ${tableModeCreation.columns.join(", ")}`}
193+
>
194+
<IconButton edge="end">
195+
<InfoOutlinedIcon />
196+
</IconButton>
197+
</Tooltip>
198+
}
199+
>
200+
<ListItemText primary={tableModeCreation.name} secondary={error?.message} />
201+
</ListItem>
202+
))}
203+
</List>
204+
</>
205+
)}
206+
{/* Success */}
207+
{state.status === "success" && (
208+
<Typography variant="body2" color="success.main" sx={{ mt: 1 }}>
209+
{t("study.tableModes.migration.success", { count: state.migrated })}
210+
</Typography>
211+
)}
212+
</Box>
213+
<Stack justifyContent="flex-end" spacing={1} sx={{ p: 1 }}>
214+
{state.status === "error" && (
215+
<>
216+
<Button variant="contained" color="secondary" onClick={migrateTableModes}>
217+
{t("global.retry")}
218+
</Button>
219+
<Button variant="contained" color="error" onClick={handleDelete}>
220+
{t("global.delete")}
221+
</Button>
222+
</>
223+
)}
224+
<Button disabled={state.status === "loading"} onClick={handleClose}>
225+
{t("global.close")}
226+
</Button>
227+
</Stack>
228+
</Paper>
229+
</Backdrop>
230+
);
231+
}
232+
233+
export default MigrationTableModes;

0 commit comments

Comments
 (0)