1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +00:00

feat: Add minimum version metadata to extensions

fixes #52
This commit is contained in:
Ash Keel 2023-02-15 10:44:44 +01:00
parent 2dce3076ee
commit d931690802
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
13 changed files with 155 additions and 25 deletions

14
app.go
View file

@ -2,6 +2,7 @@ package main
import (
"context"
"runtime/debug"
"strconv"
"github.com/strimertul/strimertul/docs"
@ -143,3 +144,16 @@ func (a *App) GetLastLogs() []LogEntry {
func (a *App) GetDocumentation() map[string]docs.KeyObject {
return docs.Keys
}
type VersionInfo struct {
Release string `json:"release"`
BuildInfo *debug.BuildInfo `json:"build"`
}
func (a *App) GetAppVersion() VersionInfo {
info, _ := debug.ReadBuildInfo()
return VersionInfo{
Release: appVersion,
BuildInfo: info,
}
}

View file

@ -12,6 +12,7 @@ export const blankTemplate = (slug: string) => `// ==Extension==
// @version 1.0
// @author Put your name here!
// @description A new extension for strimertul
// @apiversion 3.1.0
// ==/Extension==
`;

View file

@ -6,10 +6,11 @@
// ==/Extension==
interface ExtensionMetadata {
name: string;
version: string;
author: string;
description: string;
name?: string;
version?: string;
author?: string;
description?: string;
apiversion: string;
}
export function parseExtensionMetadata(
@ -40,6 +41,7 @@ export function parseExtensionMetadata(
version: metadata.version,
author: metadata.author,
description: metadata.description,
apiversion: metadata.apiversion ?? '3.1.0',
};
}

View file

@ -332,7 +332,9 @@
"error": "Error encountered",
"terminated": "Stopped"
},
"error-alert": "Error details for {{name}}"
"error-alert": "Error details for {{name}}",
"incompatible-body": "This extension requires {{APPNAME}} version {{version}} and up, you are currently running {{appversion}}, which may be too old and miss required features",
"incompatible-warning": "This extension is not compatible"
}
},
"form-actions": {

View file

@ -5,12 +5,14 @@ import thunkMiddleware from 'redux-thunk';
import apiReducer from './api/reducer';
import loggingReducer from './logging/reducer';
import extensionsReducer from './extensions/reducer';
import serverReducer from './server/reducer';
const store = configureStore({
reducer: {
api: apiReducer.reducer,
logging: loggingReducer.reducer,
extensions: extensionsReducer.reducer,
server: serverReducer.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({

View file

@ -0,0 +1,31 @@
/* eslint-disable no-param-reassign */
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { GetAppVersion } from '@wailsapp/go/main/App';
import { main } from '@wailsapp/go/models';
interface ServerState {
version: main.VersionInfo;
}
const initialState: ServerState = {
version: null,
};
const serverReducer = createSlice({
name: 'server',
initialState,
reducers: {
loadedVersionData(state, { payload }: PayloadAction<main.VersionInfo>) {
state.version = payload;
},
},
});
export const initializeServerInfo = createAsyncThunk(
'server/init-info',
async (_: void, { dispatch }) => {
dispatch(serverReducer.actions.loadedVersionData(await GetAppVersion()));
},
);
export default serverReducer;

View file

@ -26,6 +26,7 @@ import { createWSClient, useAuthBypass } from '~/store/api/reducer';
import { ConnectionStatus } from '~/store/api/types';
import loggingReducer from '~/store/logging/reducer';
import { initializeExtensions } from '~/store/extensions/reducer';
import { initializeServerInfo } from '~/store/server/reducer';
import LogViewer from './components/LogViewer';
import Sidebar, { RouteSection } from './components/Sidebar';
@ -165,6 +166,11 @@ export default function App(): JSX.Element {
);
};
// Fill application info
useEffect(() => {
void dispatch(initializeServerInfo());
});
// Get application logs
useEffect(() => {
void GetLastLogs().then((logs) => {

View file

@ -1,13 +1,12 @@
import { styled } from '@stitches/react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
// @ts-expect-error Asset import
import logo from '~/assets/icon-logo.svg';
import { RootState } from '~/store';
import { useAppSelector } from '~/store';
import { APPNAME, APPREPO } from '../theme';
import BrowserLink from './BrowserLink';
import Scrollbar from './utils/Scrollbar';
@ -160,18 +159,12 @@ export default function Sidebar({
const { t } = useTranslation();
const resolved = useResolvedPath('/about');
const matchApp = useMatch({ path: resolved.pathname, end: true });
const client = useSelector((state: RootState) => state.api.client);
const [version, setVersion] = useState<string>(null);
const version = useAppSelector((state) => state.server.version?.release);
const [lastVersion, setLastVersion] = useState<{ url: string; name: string }>(
null,
);
const dev = version && version.startsWith('v0.0.0');
async function fetchVersion() {
const versionString = await client.getKey('stul-meta/version');
setVersion(versionString);
}
async function fetchLastVersion() {
try {
const req = await fetch(
@ -197,12 +190,6 @@ export default function Sidebar({
void fetchLastVersion();
}, []);
useEffect(() => {
if (client) {
void fetchVersion();
}
}, [client]);
return (
<Container>
<Scrollbar vertical={true} viewport={{ maxHeight: '100vh' }}>
@ -219,11 +206,14 @@ export default function Sidebar({
{version && !dev ? version : t('debug.dev-build')}
</VersionLabel>
</AppLink>
{!dev && lastVersion && !version.startsWith(lastVersion.name) && (
<UpdateButton href={lastVersion.url}>
{t('menu.messages.update-available')}
</UpdateButton>
)}
{!dev &&
version &&
lastVersion &&
!version.startsWith(lastVersion.name) && (
<UpdateButton href={lastVersion.url}>
{t('menu.messages.update-available')}
</UpdateButton>
)}
</Header>
{sections.map(({ title: sectionTitle, links }) => (
<MenuSection key={sectionTitle}>

View file

@ -145,6 +145,11 @@ type ExtensionListItemProps = {
function ExtensionListItem(props: ExtensionListItemProps) {
const { t } = useTranslation();
const metadata = parseExtensionMetadata(props.entry.source);
const version = useAppSelector((state) => state.server.version?.release);
const isDev = version && version.startsWith('v0.0.0');
const showIncompatibleWarning =
!isDev && version && version < `v${metadata.apiversion}`;
return (
<ExtensionRow
status={props.enabled && isRunning(props.status) ? 'enabled' : 'disabled'}
@ -180,6 +185,24 @@ function ExtensionListItem(props: ExtensionListItemProps) {
<ExtensionStatusNote color={colorByStatus(props.status)}>
{t(`pages.extensions.statuses.${props.status}`)}
</ExtensionStatusNote>
{showIncompatibleWarning ? (
<Alert>
<AlertTrigger asChild>
<Button variation="warning" size="small">
<ExclamationTriangleIcon />
</Button>
</AlertTrigger>
<AlertContent
title={t('pages.extensions.incompatible-warning')}
showCancel={false}
>
{t('pages.extensions.incompatible-body', {
version: metadata.apiversion,
appversion: version,
})}
</AlertContent>
</Alert>
) : null}
{props.error ? (
<Alert>
<AlertTrigger asChild>

View file

@ -231,6 +231,19 @@ const button = {
},
},
},
warning: {
border: '1px solid $yellow6',
backgroundColor: '$yellow4',
'&:not(:disabled)': {
'&:hover': {
backgroundColor: '$yellow5',
borderColor: '$yellow8',
},
'&:active': {
background: '$yellow6',
},
},
},
danger: {
border: '1px solid $red6',
backgroundColor: '$red4',

View file

@ -1,10 +1,15 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
import {map[string]docs} from '../models';
import {helix} from '../models';
export function AuthenticateKVClient(arg1:string):Promise<void>;
export function GetAppVersion():Promise<main.VersionInfo>;
export function GetDocumentation():Promise<map[string]docs.KeyObject>;
export function GetKilovoltBind():Promise<string>;
export function GetLastLogs():Promise<Array<main.LogEntry>>;

View file

@ -6,6 +6,14 @@ export function AuthenticateKVClient(arg1) {
return window['go']['main']['App']['AuthenticateKVClient'](arg1);
}
export function GetAppVersion() {
return window['go']['main']['App']['GetAppVersion']();
}
export function GetDocumentation() {
return window['go']['main']['App']['GetDocumentation']();
}
export function GetKilovoltBind() {
return window['go']['main']['App']['GetKilovoltBind']();
}

View file

@ -76,6 +76,39 @@ export namespace main {
this.data = source["data"];
}
}
export class VersionInfo {
release: string;
// Go type: debug.BuildInfo
build?: any;
static createFrom(source: any = {}) {
return new VersionInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.release = source["release"];
this.build = this.convertValues(source["build"], null);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}