Skip to content

Commit

Permalink
SY-1367: Allow Silencing of Version Update Notification (#879)
Browse files Browse the repository at this point in the history
  • Loading branch information
pjdotson authored Oct 19, 2024
1 parent 890e6cf commit 04318df
Show file tree
Hide file tree
Showing 16 changed files with 207 additions and 55 deletions.
2 changes: 1 addition & 1 deletion console/src/cluster/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { Connect, connectWindowLayout } from "@/cluster/Connect";
import { versionOutdatedAdapter } from "@/cluster/notification";
import { Layout } from "@/layout";
import { NotificationAdapter } from "@/notifications/Notifications";
import { type NotificationAdapter } from "@/notifications/Notifications";

export * from "@/cluster/Badges";
export * from "@/cluster/Connect";
Expand Down
6 changes: 3 additions & 3 deletions console/src/cluster/notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
import { Button, Synnax } from "@synnaxlabs/pluto";

import {
NotificationAdapter,
type NotificationAdapter,
SugaredNotification,
} from "@/notifications/Notifications";
import { OpenUpdateDialogAction } from "@/version/Updater";
import { Version } from "@/version";

export const versionOutdatedAdapter: NotificationAdapter = (status) => {
if (status.data == null) return null;
Expand All @@ -32,6 +32,6 @@ export const versionOutdatedAdapter: NotificationAdapter = (status) => {
Update Cluster
</Button.Link>,
];
else nextStatus.actions = [<OpenUpdateDialogAction key="update" />];
else nextStatus.actions = [<Version.OpenUpdateDialogAction key="update" />];
return nextStatus;
};
4 changes: 2 additions & 2 deletions console/src/hardware/device/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import { Version } from "@/version";
import { notificationAdapter } from "@/hardware/device/useListenForChanges";

export * from "@/hardware/device/ontology";
export * from "@/hardware/device/useListenForChanges";

export const NOTIFICATION_ADAPTERS = [Version.notificationAdapter];
export const NOTIFICATION_ADAPTERS = [notificationAdapter];
2 changes: 1 addition & 1 deletion console/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ export type RootAction =
| Cluster.Action
| LinePlot.Action
| Schematic.Action
| Range.Action
| Permissions.Action
| Version.Action
| Workspace.Action;

export type RootStore = Store<RootState, RootAction>;
Expand Down
6 changes: 3 additions & 3 deletions console/src/version/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import { type ReactElement } from "react";

import { Layout } from "@/layout";
import { infoLayout } from "@/version/Info";
import { useSelect } from "@/version/selectors";
import { useSelectVersion } from "@/version/selectors";
import { useCheckForUpdates } from "@/version/Updater";

export const Badge = (): ReactElement => {
const v = useSelect();
const version = useSelectVersion();
const placer = Layout.usePlacer();
const updateAvailable = useCheckForUpdates();
return (
Expand All @@ -28,7 +28,7 @@ export const Badge = (): ReactElement => {
size="medium"
color={updateAvailable ? "var(--pluto-secondary-z)" : "var(--pluto-text-color)"}
>
{"v" + v}
{"v" + version}
</Button.Button>
);
};
4 changes: 2 additions & 2 deletions console/src/version/Info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { check, DownloadEvent } from "@tauri-apps/plugin-updater";
import { useState } from "react";

import { Layout } from "@/layout";
import { useSelect } from "@/version/selectors";
import { useSelectVersion } from "@/version/selectors";

export const infoLayout: Layout.State = {
type: "versionInfo",
Expand All @@ -32,7 +32,7 @@ export const infoLayout: Layout.State = {
};

export const Info: Layout.Renderer = () => {
const version = useSelect();
const version = useSelectVersion();
const updateQuery = useQuery({
queryKey: ["version.update"],
queryFn: async () => {
Expand Down
45 changes: 31 additions & 14 deletions console/src/version/Updater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,44 @@
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import { Icon } from "@synnaxlabs/media";
import { Button, Status, useAsyncEffect } from "@synnaxlabs/pluto";
import { id, TimeSpan } from "@synnaxlabs/x";
import { check } from "@tauri-apps/plugin-updater";
import { useState } from "react";
import { useDispatch } from "react-redux";

import { Layout } from "@/layout";
import { NotificationAdapter } from "@/notifications/Notifications";
import { type NotificationAdapter } from "@/notifications/Notifications";
import { infoLayout } from "@/version/Info";
import { useSelectUpdateNotificationsSilenced } from "@/version/selectors";
import { silenceUpdateNotifications } from "@/version/slice";

export const useCheckForUpdates = (): boolean => {
const addStatus = Status.useAggregator();

const isSilenced = useSelectUpdateNotificationsSilenced();
const [available, setAvailable] = useState(false);

const checkForUpdates = async () => {
const checkForUpdates = async (addNotifications: boolean) => {
const update = await check();
if (update?.available !== true || available) return;
setAvailable(true);
addStatus({
key: `versionUpdate-${id.id()}`,
variant: "info",
message: `Update available`,
});
if (addNotifications)
addStatus({
key: `versionUpdate-${id.id()}`,
variant: "info",
message: `Update available`,
});
};

useAsyncEffect(async () => {
await checkForUpdates();
const i = setInterval(checkForUpdates, TimeSpan.seconds(30).milliseconds);
await checkForUpdates(!isSilenced);
const i = setInterval(
() => checkForUpdates(!isSilenced),
TimeSpan.seconds(30).milliseconds,
);
return () => clearInterval(i);
}, []);
}, [isSilenced]);

return available;
};
Expand All @@ -45,15 +53,24 @@ export const notificationAdapter: NotificationAdapter = (status) => {
if (!status.key.startsWith("versionUpdate")) return null;
return {
...status,
actions: [<OpenUpdateDialogAction key="update" />],
actions: [<OpenUpdateDialogAction key="update" />, <SilenceAction key="silence" />],
};
};

export const OpenUpdateDialogAction = () => {
const place = Layout.usePlacer();
const placer = Layout.usePlacer();
return (
<Button.Button variant="outlined" size="small" onClick={() => place(infoLayout)}>
<Button.Button variant="outlined" size="small" onClick={() => placer(infoLayout)}>
Update
</Button.Button>
);
};

const SilenceAction = () => {
const dispatch = useDispatch();
return (
<Button.Icon variant="text" onClick={() => dispatch(silenceUpdateNotifications())}>
<Icon.Snooze />
</Button.Icon>
);
};
4 changes: 2 additions & 2 deletions console/src/version/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import { Layout } from "@/layout";
import { NotificationAdapter } from "@/notifications/Notifications";
import { type Layout } from "@/layout";
import { type NotificationAdapter } from "@/notifications/Notifications";
import { Info, infoLayout } from "@/version/Info";
import { notificationAdapter } from "@/version/Updater";

Expand Down
36 changes: 36 additions & 0 deletions console/src/version/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2024 Synnax Labs, Inc.
//
// Use of this software is governed by the Business Source License included in the file
// licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with the Business Source
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import { migrate } from "@synnaxlabs/x";

import * as v0 from "@/version/migrations/v0";
import * as v1 from "@/version/migrations/v1";

export type SliceState = v1.SliceState;
export type AnySliceState = v0.SliceState | v1.SliceState;
export const ZERO_SLICE_STATE = v1.ZERO_SLICE_STATE;

export const SLICE_MIGRATIONS: migrate.Migrations = {};

// Because the v0 state had a key called version, the usual migration pattern from X
// does not work. Instead, we need to check if the state is a v0 state, manually convert
// it to a v1 state, and then run the internal migrator.

const internalMigrator = migrate.migrator<AnySliceState, SliceState>({
name: "version.slice",
migrations: SLICE_MIGRATIONS,
def: ZERO_SLICE_STATE,
});

const v0StrictSliceStateZ = v0.sliceStateZ.strict();

export const migrateSlice: (v: AnySliceState) => SliceState = (v) => {
const state = v0StrictSliceStateZ.safeParse(v).success ? v1.migrate(v) : v;
return internalMigrator(state);
};
42 changes: 42 additions & 0 deletions console/src/version/migrations/migrations.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2024 Synnax Labs, Inc.
//
// Use of this software is governed by the Business Source License included in
// the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with the Business
// Source License, use of this software will be governed by the Apache License,
// Version 2.0, included in the file licenses/APL.txt.

import { describe, expect, it } from "vitest";

import { migrateSlice, ZERO_SLICE_STATE } from "@/version/migrations";
import * as v0 from "@/version/migrations/v0";
import * as v1 from "@/version/migrations/v1";

describe("migrations", () => {
describe("slice", () => {
const STATES = [v0.ZERO_SLICE_STATE, v1.ZERO_SLICE_STATE];
STATES.forEach((state) =>
it(`should migrate slice from ${state.version} to latest`, () => {
const migrated = migrateSlice(state);
expect(migrated).toEqual(ZERO_SLICE_STATE);
}),
);
});
describe("slice with a version", () => {
const consoleVersion = "0.27.0";
const V0_STATE: v0.SliceState = {
version: consoleVersion,
};
const V1_STATE: v1.SliceState = {
version: "1.0.0",
consoleVersion,
updateNotificationsSilenced: false,
};
it(`should migrate slice from ${V0_STATE.version} to latest`, () => {
const migrated = migrateSlice(V0_STATE);
console.log(migrated);
expect(migrated).toEqual(migrateSlice(V1_STATE));
});
});
});
20 changes: 20 additions & 0 deletions console/src/version/migrations/v0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2024 Synnax Labs, Inc.
//
// Use of this software is governed by the Business Source License included in the file
// licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with the Business Source
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import { z } from "zod";

export const sliceStateZ = z.object({
version: z.string(),
});

export type SliceState = z.infer<typeof sliceStateZ>;

export const ZERO_SLICE_STATE: SliceState = {
version: "0.0.0",
};
31 changes: 31 additions & 0 deletions console/src/version/migrations/v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2024 Synnax Labs, Inc.
//
// Use of this software is governed by the Business Source License included in the file
// licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with the Business Source
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import { z } from "zod";

import * as v0 from "@/version/migrations/v0";

export const sliceStateZ = z.object({
version: z.literal("1.0.0"),
consoleVersion: z.string(),
updateNotificationsSilenced: z.boolean(),
});
export type SliceState = z.infer<typeof sliceStateZ>;

export const ZERO_SLICE_STATE: SliceState = {
version: "1.0.0",
consoleVersion: "0.0.0",
updateNotificationsSilenced: false,
};

export const migrate: (state: v0.SliceState) => SliceState = (state) => ({
version: "1.0.0",
consoleVersion: state.version,
updateNotificationsSilenced: false,
});
18 changes: 15 additions & 3 deletions console/src/version/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@
// included in the file licenses/APL.txt.

import { useMemoSelect } from "@/hooks";
import { type StoreState } from "@/version/slice";
import { SLICE_NAME, type SliceState, type StoreState } from "@/version/slice";

export const select = (state: StoreState): string => state.version.version;
export const selectSliceState = (state: StoreState): SliceState => state[SLICE_NAME];

export const useSelect = (): string => useMemoSelect(select, []);
export const useSelectSliceState = (): SliceState =>
useMemoSelect((state: StoreState) => selectSliceState(state), []);

export const selectVersion = (state: StoreState): string =>
selectSliceState(state).consoleVersion;

export const useSelectVersion = (): string => useMemoSelect(selectVersion, []);

export const selectUpdateNotificationsSilenced = (state: StoreState): boolean =>
selectSliceState(state).updateNotificationsSilenced;

export const useSelectUpdateNotificationsSilenced = (): boolean =>
useMemoSelect(selectUpdateNotificationsSilenced, []);
Loading

0 comments on commit 04318df

Please sign in to comment.