feat: show new updates in dashboard (#690)

* feat: show new update banner

* feat(http): add request logger

* refactor: updates checker

* feat: make update check optional

* fix: empty releases

* add toggle switch for update checks

* feat: toggle updates check from settings

* feat: toggle updates check from settings

* feat: check on toggle enabled

---------

Co-authored-by: soup <soup@r4tio.dev>
This commit is contained in:
ze0s 2023-02-05 18:44:11 +01:00 committed by GitHub
parent 3fdd7cf5e4
commit 2917a7d42d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 687 additions and 121 deletions

View file

@ -1,5 +1,6 @@
import { baseUrl, sseBaseUrl } from "../utils";
import { AuthContext } from "../utils/Context";
import { GithubRelease } from "../types/Update";
interface ConfigType {
body?: BodyInit | Record<string, unknown> | unknown;
@ -80,7 +81,8 @@ export const APIClient = {
delete: (key: string) => appClient.Delete(`api/keys/${key}`)
},
config: {
get: () => appClient.Get<Config>("api/config")
get: () => appClient.Get<Config>("api/config"),
update: (config: ConfigUpdate) => appClient.Patch("api/config", config)
},
download_clients: {
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
@ -180,5 +182,9 @@ export const APIClient = {
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
delete: () => appClient.Delete("api/release/all")
},
updates: {
check: () => appClient.Get("api/updates/check"),
getLatestRelease: () => appClient.Get<GithubRelease|undefined>("api/updates/latest")
}
};

View file

@ -2,11 +2,13 @@ import { Fragment } from "react";
import { Link, NavLink, Outlet } from "react-router-dom";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { BookOpenIcon, UserIcon } from "@heroicons/react/24/solid";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline";
import { AuthContext } from "../utils/Context";
import logo from "../logo.png";
import { useQuery } from "react-query";
import { APIClient } from "../api/APIClient";
interface NavItem {
name: string;
@ -27,6 +29,17 @@ export default function Base() {
{ name: "Logs", path: "/logs" }
];
const { data } = useQuery(
["updates"],
() => APIClient.updates.getLatestRelease(),
{
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
}
);
return (
<div className="min-h-screen">
<Disclosure
@ -185,6 +198,16 @@ export default function Base() {
</div>
</div>
</div>
{data && data.html_url && (
<a href={data.html_url} target="_blank">
<div className="flex mt-4 py-2 bg-blue-500 rounded justify-center">
<MegaphoneIcon className="h-6 w-6 text-blue-100"/>
<span className="text-blue-100 font-medium mx-3">New update available!</span>
<span className="inline-flex items-center rounded-md bg-blue-100 px-2.5 py-0.5 text-sm font-medium text-blue-800">{data?.name}</span>
</div>
</a>
)}
</div>
<Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">

View file

@ -1,12 +1,17 @@
import { useQuery } from "react-query";
import { useMutation, useQuery } from "react-query";
import { APIClient } from "../../api/APIClient";
import { Checkbox } from "../../components/Checkbox";
import { SettingsContext } from "../../utils/Context";
import { GithubRelease } from "../../types/Update";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { queryClient } from "../../App";
interface RowItemProps {
label: string;
value?: string;
title?: string;
newUpdate?: GithubRelease;
}
const RowItem = ({ label, value, title }: RowItemProps) => {
@ -23,6 +28,25 @@ const RowItem = ({ label, value, title }: RowItemProps) => {
);
};
const RowItemVersion = ({ label, value, title, newUpdate }: RowItemProps) => {
if (!value)
return null;
return (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
<dt className="font-medium text-gray-500 dark:text-white" title={title}>{label}:</dt>
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2 break-all">
{value}
{newUpdate && newUpdate.html_url && (
<span>
<a href={newUpdate.html_url} target="_blank"><span className="ml-2 inline-flex items-center rounded-md bg-green-100 px-2.5 py-0.5 text-sm font-medium text-green-800">{newUpdate.name} available!</span></a>
</span>
)}
</dd>
</div>
);
};
function ApplicationSettings() {
const [settings, setSettings] = SettingsContext.use();
@ -36,6 +60,38 @@ function ApplicationSettings() {
}
);
const { data: updateData } = useQuery(
["updates"],
() => APIClient.updates.getLatestRelease(),
{
retry: false,
refetchOnWindowFocus: false,
onError: err => console.log(err)
}
);
const checkUpdateMutation = useMutation(
() => APIClient.updates.check(),
{
onSuccess: () => {
queryClient.invalidateQueries(["updates"]);
}
}
);
const toggleCheckUpdateMutation = useMutation(
(value: boolean) => APIClient.config.update({ check_for_updates: value }),
{
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
queryClient.invalidateQueries(["config"]);
checkUpdateMutation.mutate();
}
}
);
return (
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
<div className="py-6 px-4 sm:p-6 lg:pb-8">
@ -98,7 +154,7 @@ function ApplicationSettings() {
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="px-4 py-5 sm:p-0">
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
<RowItem label="Version" value={data?.version} />
<RowItemVersion label="Version" value={data?.version} newUpdate={updateData ?? undefined} />
<RowItem label="Commit" value={data?.commit} />
<RowItem label="Build date" value={data?.date} />
<RowItem label="Log path" value={data?.log_path} title="Set in config.toml" />
@ -117,6 +173,16 @@ function ApplicationSettings() {
})}
/>
</div>
<div className="px-4 sm:px-6 py-1">
<Checkbox
label="Check for updates"
description="Get notified of new updates."
value={data?.check_for_updates ?? true}
setValue={(newValue: boolean) => {
toggleCheckUpdateMutation.mutate(newValue);
}}
/>
</div>
<div className="px-4 sm:px-6 py-1">
<Checkbox
label="Dark theme"

20
web/src/types/Config.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
interface Config {
host: string;
port: number;
log_level: string;
log_path: string;
base_url: string;
check_for_updates: boolean;
version: string;
commit: string;
date: string;
}
interface ConfigUpdate {
host?: string;
port?: number;
log_level?: string;
log_path?: string;
base_url?: string;
check_for_updates: boolean;
}

View file

@ -67,14 +67,3 @@ interface IrcAuth {
account?: string; // optional
password?: string; // optional
}
interface Config {
host: string;
port: number;
log_level: string;
log_path: string;
base_url: string;
version: string;
commit: string;
date: string;
}

46
web/src/types/Update.d.ts vendored Normal file
View file

@ -0,0 +1,46 @@
interface UpdateAvailableResponse {
id: number;
name: string;
}
export interface GithubRelease {
id: number;
node_id: string;
url: string;
html_url: string;
tag_name: string;
target_commitish: string;
name: string;
body: string;
created_at: Date;
published_at: Date;
author: GithubAuthor;
assets: GitHubReleaseAsset[];
}
export interface GitHubReleaseAsset {
url: string;
id: number;
node_id: string;
name: string;
label: string;
uploader: GithubAuthor;
content_type: string;
state: string;
size: number;
download_count: number;
created_at: Date;
updated_at: Date;
browser_download_url: string;
}
export interface GithubAuthor {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
type: string;
}

View file

@ -45,6 +45,7 @@ export const AuthContext = newRidgeState<AuthInfo>(
interface SettingsType {
debug: boolean;
checkForUpdates: boolean;
darkTheme: boolean;
scrollOnNewLog: boolean;
indentLogLines: boolean;
@ -54,6 +55,7 @@ interface SettingsType {
export const SettingsContext = newRidgeState<SettingsType>(
{
debug: false,
checkForUpdates: true,
darkTheme: true,
scrollOnNewLog: false,
indentLogLines: false,