Skip to content

Commit 5632a40

Browse files
TrySoundkof
andauthored
feat: allow restore from published backup (#5320)
Ref #5276 --------- Co-authored-by: Oleg Isonen <[email protected]>
1 parent a57783e commit 5632a40

File tree

9 files changed

+437
-102
lines changed

9 files changed

+437
-102
lines changed

apps/builder/app/builder/features/project-settings/project-settings.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { FunctionComponent } from "react";
12
import { useStore } from "@nanostores/react";
23
import {
34
Dialog,
@@ -11,22 +12,24 @@ import {
1112
ListItem,
1213
Text,
1314
} from "@webstudio-is/design-system";
14-
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
15+
import {
16+
$openProjectSettings,
17+
type SectionName,
18+
} from "~/shared/nano-states/project-settings";
19+
import { $isDesignMode } from "~/shared/nano-states";
20+
import { leftPanelWidth, rightPanelWidth } from "./utils";
1521
import { SectionGeneral } from "./section-general";
1622
import { SectionRedirects } from "./section-redirects";
1723
import { SectionPublish } from "./section-publish";
1824
import { SectionMarketplace } from "./section-marketplace";
19-
import { leftPanelWidth, rightPanelWidth } from "./utils";
20-
import type { FunctionComponent } from "react";
21-
import { $isDesignMode } from "~/shared/nano-states";
22-
23-
type SectionName = "general" | "redirects" | "publish" | "marketplace";
25+
import { SectionBackups } from "./section-backups";
2426

2527
const sections = new Map<SectionName, FunctionComponent>([
2628
["general", SectionGeneral],
2729
["redirects", SectionRedirects],
2830
["publish", SectionPublish],
2931
["marketplace", SectionMarketplace],
32+
["backups", SectionBackups],
3033
] as const);
3134

3235
export const ProjectSettingsView = ({
@@ -39,6 +42,9 @@ export const ProjectSettingsView = ({
3942
onOpenChange?: (isOpen: boolean) => void;
4043
}) => {
4144
const isDesignMode = useStore($isDesignMode);
45+
const SectionComponent = currentSection
46+
? sections.get(currentSection)
47+
: undefined;
4248

4349
return (
4450
<Dialog
@@ -100,12 +106,9 @@ export const ProjectSettingsView = ({
100106
})}
101107
</Flex>
102108
</List>
103-
<ScrollArea>
109+
<ScrollArea css={{ width: "100%" }}>
104110
<Grid gap={2} css={{ py: theme.spacing[5] }}>
105-
{currentSection === "general" && <SectionGeneral />}
106-
{currentSection === "redirects" && <SectionRedirects />}
107-
{currentSection === "publish" && <SectionPublish />}
108-
{currentSection === "marketplace" && <SectionMarketplace />}
111+
{SectionComponent && <SectionComponent />}
109112
<div />
110113
</Grid>
111114
</ScrollArea>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { useStore } from "@nanostores/react";
2+
import { useEffect, useState } from "react";
3+
import {
4+
Grid,
5+
Text,
6+
Button,
7+
Select,
8+
Dialog,
9+
DialogTitle,
10+
DialogTrigger,
11+
DialogContent,
12+
DialogClose,
13+
Flex,
14+
theme,
15+
toast,
16+
PanelBanner,
17+
Link,
18+
rawTheme,
19+
} from "@webstudio-is/design-system";
20+
import { UpgradeIcon } from "@webstudio-is/icons";
21+
import { nativeClient, trpcClient } from "~/shared/trpc/trpc-client";
22+
import { $project, $userPlanFeatures } from "~/shared/nano-states";
23+
import { sectionSpacing } from "./utils";
24+
import cmsUpgradeBanner from "../settings-panel/cms-upgrade-banner.svg?url";
25+
26+
const formatPublishDate = (date: string) => {
27+
try {
28+
const formatter = new Intl.DateTimeFormat("en", {
29+
dateStyle: "long",
30+
timeStyle: "short",
31+
});
32+
return formatter.format(new Date(date));
33+
} catch {
34+
return date;
35+
}
36+
};
37+
38+
export const SectionBackups = () => {
39+
const { hasProPlan } = useStore($userPlanFeatures);
40+
const { data, load } = trpcClient.project.publishedBuilds.useQuery();
41+
const projectId = $project.get()?.id ?? "";
42+
useEffect(() => {
43+
load({ projectId });
44+
}, [load, projectId]);
45+
const options = data?.success ? data.data : [];
46+
const [backupBuild = options.at(0), setBackupBuild] = useState<
47+
undefined | (typeof options)[number]
48+
>();
49+
const restore = async () => {
50+
if (!backupBuild?.buildId) {
51+
return;
52+
}
53+
const result = await nativeClient.project.restoreDevelopmentBuild.mutate({
54+
projectId,
55+
fromBuildId: backupBuild.buildId,
56+
});
57+
if (result.success) {
58+
location.reload();
59+
return;
60+
}
61+
toast.error(result.error);
62+
};
63+
64+
return (
65+
<Grid gap={2} css={sectionSpacing}>
66+
<Text variant="titles">Backups</Text>
67+
<Select
68+
placeholder="No backups"
69+
options={options}
70+
getValue={(option) => option.buildId ?? ""}
71+
getLabel={(option) => {
72+
if (!option.createdAt) {
73+
return;
74+
}
75+
let label = formatPublishDate(option.createdAt);
76+
if (option.domains) {
77+
label += ` (${option.domains})`;
78+
}
79+
return label;
80+
}}
81+
value={backupBuild}
82+
onChange={setBackupBuild}
83+
/>
84+
<Dialog>
85+
<DialogTrigger asChild>
86+
<Button
87+
css={{ justifySelf: "start" }}
88+
disabled={!hasProPlan || options.length === 0}
89+
>
90+
Restore
91+
</Button>
92+
</DialogTrigger>
93+
<DialogContent width={320}>
94+
<DialogTitle>Restore published version</DialogTitle>
95+
<Flex
96+
direction="column"
97+
css={{ padding: theme.panel.padding }}
98+
gap={2}
99+
>
100+
<Text>
101+
Are you sure you want to restore the project to its published
102+
version?
103+
</Text>
104+
{backupBuild?.createdAt && (
105+
<Text color="destructive">
106+
All changes made after{" "}
107+
{formatPublishDate(backupBuild.createdAt)} will be lost.
108+
</Text>
109+
)}
110+
<Flex gap="2" justify="end">
111+
<DialogClose>
112+
<Button color="ghost">Cancel</Button>
113+
</DialogClose>
114+
<DialogClose>
115+
<Button color="destructive" onClick={restore}>
116+
Restore
117+
</Button>
118+
</DialogClose>
119+
</Flex>
120+
</Flex>
121+
</DialogContent>
122+
</Dialog>
123+
{!hasProPlan && (
124+
<PanelBanner>
125+
<img
126+
src={cmsUpgradeBanner}
127+
alt="Upgrade for backups"
128+
width={rawTheme.spacing[28]}
129+
style={{ aspectRatio: "4.1" }}
130+
/>
131+
<Text variant="regularBold">Upgrade to restore from backups</Text>
132+
<Flex align="center" gap={1}>
133+
<UpgradeIcon />
134+
<Link
135+
color="inherit"
136+
target="_blank"
137+
href="https://webstudio.is/pricing"
138+
>
139+
Upgrade to Pro
140+
</Link>
141+
</Flex>
142+
</PanelBanner>
143+
)}
144+
</Grid>
145+
);
146+
};

apps/builder/app/shared/help.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
ContentIcon,
3-
DiscordIcon,
4-
LifeBuoyIcon,
5-
YoutubeIcon,
6-
} from "@webstudio-is/icons";
1+
import { ContentIcon, DiscordIcon, YoutubeIcon } from "@webstudio-is/icons";
72

83
export const help = [
94
{
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { atom } from "nanostores";
22

3-
export const $openProjectSettings = atom<
4-
"general" | "redirects" | "publish" | "marketplace" | undefined
5-
>();
3+
export type SectionName =
4+
| "general"
5+
| "redirects"
6+
| "publish"
7+
| "marketplace"
8+
| "backups";
9+
10+
export const $openProjectSettings = atom<SectionName | undefined>();

0 commit comments

Comments
 (0)