Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,7 @@
"testformat": "prettier \"src/**/*.{ts,tsx}\" --list-different"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/plugin-proposal-decorators": "^7.29.0",
"@eslint/js": "^10.0.0",
"@rolldown/plugin-babel": "^0.2.2",
"@types/node": "^25.9.3",
"@types/notifyjs": "^3.0.5",
"@types/react": "^19.1.9",
Expand Down
22 changes: 15 additions & 7 deletions ui/src/CurrentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ import axios, {AxiosError, AxiosResponse} from 'axios';
import * as config from './config';
import {detect} from 'detect-browser';
import {SnackReporter} from './snack/SnackManager';
import {observable, runInAction, action} from 'mobx';
import {makeObservable, observable, runInAction, action} from 'mobx';
import {ICurrentUser} from './types';

export class CurrentUser {
private reconnectTimeoutId: number | null = null;
private reconnectTime = 7500;
@observable accessor loggedIn = false;
@observable accessor refreshKey = 0;
@observable accessor authenticating = true;
@observable accessor user: ICurrentUser = {
public loggedIn = false;
public refreshKey = 0;
public authenticating = true;
public user: ICurrentUser = {
name: 'unknown',
admin: false,
id: -1,
createdAt: '',
};
@observable accessor connectionErrorMessage: string | null = null;
public connectionErrorMessage: string | null = null;

public constructor(private readonly snack: SnackReporter) {}
public constructor(private readonly snack: SnackReporter) {
makeObservable(this, {
loggedIn: observable,
refreshKey: observable,
authenticating: observable,
user: observable,
connectionErrorMessage: observable,
});
}

public register = async (name: string, pass: string): Promise<boolean> =>
axios
Expand Down
15 changes: 10 additions & 5 deletions ui/src/ElevateStore.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import axios from 'axios';
import {action, observable, runInAction} from 'mobx';
import {action, makeObservable, observable, runInAction} from 'mobx';
import * as config from './config';
import {SnackReporter} from './snack/SnackManager';
import {CurrentUser} from './CurrentUser';

export class ElevateStore {
@observable accessor elevated = false;
@observable accessor oidcElevatePending = false;
public elevated = false;
public oidcElevatePending = false;
private oidcPollIntervalId: number | undefined = undefined;
private oidcPopup: Window | null = null;

public constructor(
private readonly snack: SnackReporter,
private readonly currentUser: CurrentUser
) {}
) {
makeObservable(this, {
elevated: observable,
oidcElevatePending: observable,
refreshElevated: action,
});
}

@action
public refreshElevated = (): number => {
const elevatedUntil = this.currentUser.user.elevatedUntil;
if (!elevatedUntil) {
Expand Down
12 changes: 7 additions & 5 deletions ui/src/application/AppStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from 'axios';
import {generateKeyBetween} from 'fractional-indexing';
import {action, runInAction} from 'mobx';
import {action, makeObservable, runInAction} from 'mobx';
import {BaseStore} from '../common/BaseStore';
import * as config from '../config';
import {SnackReporter} from '../snack/SnackManager';
Expand All @@ -12,6 +12,12 @@ export class AppStore extends BaseStore<IApplication> {

public constructor(private readonly snack: SnackReporter) {
super();
makeObservable(this, {
uploadImage: action,
reorder: action,
update: action,
create: action,
});
}

protected requestItems = (): Promise<IApplication[]> =>
Expand All @@ -25,7 +31,6 @@ export class AppStore extends BaseStore<IApplication> {
return this.snack('Application deleted');
});

@action
public uploadImage = async (id: number, file: Blob): Promise<void> => {
const formData = new FormData();
formData.append('file', file);
Expand Down Expand Up @@ -57,7 +62,6 @@ export class AppStore extends BaseStore<IApplication> {
}
}

@action
public reorder = async (fromId: number, toId: number): Promise<void> => {
const fromIndex = this.items.findIndex((app) => app.id === fromId);
const toIndex = this.items.findIndex((app) => app.id === toId);
Expand All @@ -80,7 +84,6 @@ export class AppStore extends BaseStore<IApplication> {
await this.update({...toUpdate, sortKey: newSortKey});
};

@action
public update = async ({
id,
...app
Expand All @@ -93,7 +96,6 @@ export class AppStore extends BaseStore<IApplication> {
this.snack('Application updated');
};

@action
public create = async (
name: string,
description: string,
Expand Down
12 changes: 7 additions & 5 deletions ui/src/client/ClientStore.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import {BaseStore} from '../common/BaseStore';
import axios from 'axios';
import * as config from '../config';
import {action} from 'mobx';
import {action, makeObservable} from 'mobx';
import {SnackReporter} from '../snack/SnackManager';
import {IClient} from '../types';

export class ClientStore extends BaseStore<IClient> {
public constructor(private readonly snack: SnackReporter) {
super();
makeObservable(this, {
update: action,
createNoNotifcation: action,
create: action,
elevate: action,
});
}

protected requestItems = (): Promise<IClient[]> =>
Expand All @@ -19,7 +25,6 @@ export class ClientStore extends BaseStore<IClient> {
.then(() => this.snack('Client deleted'));
}

@action
public update = async (
id: number,
name: string,
Expand All @@ -33,7 +38,6 @@ export class ClientStore extends BaseStore<IClient> {
this.snack('Client updated');
};

@action
public createNoNotifcation = async (
name: string,
expiresAfterInactivitySeconds = 0
Expand All @@ -46,14 +50,12 @@ export class ClientStore extends BaseStore<IClient> {
return client.data;
};

@action
public create = async (name: string, expiresAfterInactivitySeconds = 0): Promise<string> => {
const client = await this.createNoNotifcation(name, expiresAfterInactivitySeconds);
this.snack('Client added');
return client.token;
};

@action
public elevate = async (id: number, durationSeconds: number): Promise<void> => {
await axios.post(`${config.get('url')}client/${id}/elevate`, {durationSeconds});
await this.refresh();
Expand Down
18 changes: 12 additions & 6 deletions ui/src/common/BaseStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {action, observable} from 'mobx';
import {action, makeObservable, observable} from 'mobx';

interface HasID {
id: number;
Expand All @@ -12,27 +12,34 @@ export interface IClearable {
* Base implementation for handling items with ids.
*/
export abstract class BaseStore<T extends HasID> implements IClearable {
@observable protected accessor items: T[] = [];
public items: T[] = [];

protected constructor() {
makeObservable(this, {
items: observable,
remove: action,
refresh: action,
refreshIfMissing: action,
clear: action,
});
}

protected abstract requestItems(): Promise<T[]>;

protected abstract requestDelete(id: number): Promise<void>;

@action
public remove = async (id: number): Promise<void> => {
await this.requestDelete(id);
await this.refresh();
};

@action
public refresh = (): Promise<void> =>
this.requestItems().then(
action((items) => {
this.items = items || [];
})
);

@action
public refreshIfMissing = async (id: number): Promise<void> => {
if (this.getByIDOrUndefined(id) === undefined) {
await this.refresh();
Expand All @@ -52,7 +59,6 @@ export abstract class BaseStore<T extends HasID> implements IClearable {

public getItems = (): T[] => this.items;

@action
public clear = (): void => {
this.items = [];
};
Expand Down
35 changes: 21 additions & 14 deletions ui/src/message/MessagesStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {BaseStore} from '../common/BaseStore';
import {action, IObservableArray, observable, reaction, runInAction} from 'mobx';
import {action, IObservableArray, makeObservable, observable, reaction, runInAction} from 'mobx';
import axios, {AxiosResponse} from 'axios';
import * as config from '../config';
import {createTransformer} from 'mobx-utils';
Expand All @@ -22,15 +22,33 @@ interface PendingDelete {
}

export class MessagesStore {
@observable private accessor state: Record<string, MessagesState> = {};
@observable private accessor pendingDeletes: Map<number, PendingDelete> = observable.map();
private state: Record<string, MessagesState> = {};
private pendingDeletes: Map<number, PendingDelete> = observable.map();

private loading = false;

public constructor(
private readonly appStore: BaseStore<IApplication>,
private readonly snack: SnackReporter
) {
makeObservable<MessagesStore, 'state' | 'pendingDeletes' | 'removeFromList' | 'clear'>(
this,
{
state: observable,
pendingDeletes: observable,
loadMore: action,
publishSingleMessage: action,
removeByApp: action,
addPendingDelete: action,
cancelPendingDelete: action,
executePendingDeletes: action,
removeSingle: action,
clearAll: action,
refreshByApp: action,
removeFromList: action,
clear: action,
}
);
reaction(() => appStore.getItems(), this.createEmptyStatesForApps);
}

Expand All @@ -45,7 +63,6 @@ export class MessagesStore {

public canLoadMore = (appId: number) => this.stateOf(appId, /*create*/ false).hasMore;

@action
public loadMore = async (appId: number) => {
const state = this.stateOf(appId);
if (!state.hasMore || this.loading) {
Expand All @@ -70,7 +87,6 @@ export class MessagesStore {
return Promise.resolve();
};

@action
public publishSingleMessage = (message: IMessage) => {
if (this.exists(AllMessages)) {
this.stateOf(AllMessages).messages.unshift(message);
Expand All @@ -80,7 +96,6 @@ export class MessagesStore {
}
};

@action
public removeByApp = async (appId: number) => {
if (appId === AllMessages) {
await axios.delete(config.get('url') + 'message');
Expand All @@ -95,11 +110,9 @@ export class MessagesStore {
await this.loadMore(appId);
};

@action
public addPendingDelete = (pending: PendingDelete) =>
this.pendingDeletes.set(pending.message.id, pending);

@action
public cancelPendingDelete = (message: IMessage): boolean => {
const pending = this.pendingDeletes.get(message.id);
if (pending) {
Expand All @@ -109,13 +122,11 @@ export class MessagesStore {
return !!pending;
};

@action
public executePendingDeletes = () =>
Array.from(this.pendingDeletes.values()).forEach(({message}) => this.removeSingle(message));

public visible = (message: number): boolean => !this.pendingDeletes.has(message);

@action
public removeSingle = async (message: IMessage) => {
if (!this.pendingDeletes.has(message.id)) {
return;
Expand Down Expand Up @@ -152,21 +163,18 @@ export class MessagesStore {
this.snack(`Message sent to ${app.name}`);
};

@action
public clearAll = () => {
this.state = {};
this.createEmptyStatesForApps(this.appStore.getItems());
};

@action
public refreshByApp = async (appId: number) => {
this.clearAll();
this.loadMore(appId);
};

public exists = (id: number) => this.stateOf(id).loaded;

@action
private removeFromList(messages: IMessage[], messageToDelete: IMessage): false | number {
if (messages) {
const index = messages.findIndex((message) => message.id === messageToDelete.id);
Expand All @@ -178,7 +186,6 @@ export class MessagesStore {
return false;
}

@action
private clear = (appId: number) => (this.state[appId] = this.emptyState());

private fetchMessages = (
Expand Down
8 changes: 5 additions & 3 deletions ui/src/plugin/PluginStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios';
import {action} from 'mobx';
import {action, makeObservable} from 'mobx';
import {BaseStore} from '../common/BaseStore';
import * as config from '../config';
import {SnackReporter} from '../snack/SnackManager';
Expand All @@ -10,6 +10,10 @@ export class PluginStore extends BaseStore<IPlugin> {

public constructor(private readonly snack: SnackReporter) {
super();
makeObservable(this, {
changeConfig: action,
changeEnabledState: action,
});
}

public requestConfig = (id: number): Promise<string> =>
Expand All @@ -31,7 +35,6 @@ export class PluginStore extends BaseStore<IPlugin> {
return id === -1 ? 'All Plugins' : plugin !== undefined ? plugin.name : 'unknown';
};

@action
public changeConfig = async (id: number, newConfig: string): Promise<void> => {
await axios.post(`${config.get('url')}plugin/${id}/config`, newConfig, {
headers: {'content-type': 'application/x-yaml'},
Expand All @@ -40,7 +43,6 @@ export class PluginStore extends BaseStore<IPlugin> {
await this.refresh();
};

@action
public changeEnabledState = async (id: number, enabled: boolean): Promise<void> => {
await axios.post(`${config.get('url')}plugin/${id}/${enabled ? 'enable' : 'disable'}`);
this.snack(`Plugin ${enabled ? 'enabled' : 'disabled'}`);
Expand Down
Loading
Loading