mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
16 Commits
renovate/c
...
feat/bun-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7a749efcd | ||
|
|
b54660115e | ||
|
|
97769b25ad | ||
|
|
5b7ce3ea1b | ||
|
|
513b1ef2ff | ||
|
|
ce03244bbe | ||
|
|
a98b8b371e | ||
|
|
aa51a874a3 | ||
|
|
c5c0e48288 | ||
|
|
1ee5353c83 | ||
|
|
cac0039ea2 | ||
|
|
329f021c3d | ||
|
|
ee0d10ef6a | ||
|
|
fce35a1495 | ||
|
|
58b467fa2d | ||
|
|
4971e031a5 |
@@ -24,8 +24,10 @@ COPY tsconfig.json .eslintrc.ts .prettierrc.cjs .npmrc .env.production .env.stag
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN npm i -g bun
|
||||
|
||||
# Install deps
|
||||
RUN npm i
|
||||
RUN npm install
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"name": "unraid-api",
|
||||
"script": "./dist/main.js",
|
||||
"cwd": "/usr/local/unraid-api",
|
||||
"interpreter": "bun",
|
||||
"interpreter_args": "--smol",
|
||||
"exec_mode": "fork",
|
||||
"wait_ready": true,
|
||||
"listen_timeout": 15000,
|
||||
|
||||
44
api/justfile
44
api/justfile
@@ -16,6 +16,50 @@ setup:
|
||||
@deploy:
|
||||
./scripts/deploy-dev.sh
|
||||
|
||||
alias r:= restart
|
||||
|
||||
# restarts the api on {target_server}
|
||||
restart target_server="" :
|
||||
#!/usr/bin/env bash
|
||||
# Path to store the last used server name
|
||||
state_file="$HOME/.deploy_state"
|
||||
|
||||
# Read the last used server name from the state file
|
||||
if [[ -f "$state_file" ]]; then
|
||||
last_server_name=$(cat "$state_file")
|
||||
else
|
||||
last_server_name=""
|
||||
fi
|
||||
|
||||
# Read the server name from the command-line argument or use the last used server name as the default
|
||||
server_name=$([ "{{ target_server }}" = "" ] && echo "$last_server_name" || echo ""{{ target_server }}"")
|
||||
|
||||
# Check if the server name is provided
|
||||
if [[ -z "$server_name" ]]; then
|
||||
echo "Please provide the SSH server name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save the current server name to the state file
|
||||
echo "$server_name" > "$state_file"
|
||||
# Run unraid-api restart on remote host
|
||||
ssh root@"${server_name}" "LOG_LEVEL=trace unraid-api restart"
|
||||
|
||||
# build & deploy
|
||||
bd: build deploy
|
||||
|
||||
# plays an os-specific bell
|
||||
[no-cd]
|
||||
chime:
|
||||
#!/usr/bin/env bash
|
||||
# Play built-in sound based on the operating system
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
afplay /System/Library/Sounds/Glass.aiff
|
||||
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
|
||||
# Linux
|
||||
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||
# Windows
|
||||
powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()"
|
||||
fi
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@reflet/cron": "^1.3.1",
|
||||
"@runonflux/nat-upnp": "^1.0.2",
|
||||
"@vmngr/libvirt": "github:unraid/libvirt",
|
||||
"accesscontrol": "^2.2.1",
|
||||
"bycontract": "^2.0.11",
|
||||
"bytes": "^3.1.2",
|
||||
@@ -167,6 +168,7 @@
|
||||
"nodemon": "^3.1.7",
|
||||
"rollup-plugin-node-externals": "^7.1.3",
|
||||
"standard-version": "^9.5.0",
|
||||
"tsup": "^8.3.6",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"unplugin-swc": "^1.5.1",
|
||||
@@ -176,12 +178,9 @@
|
||||
"vitest": "^2.1.8",
|
||||
"zx": "^8.2.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@vmngr/libvirt": "github:unraid/libvirt"
|
||||
},
|
||||
"overrides": {
|
||||
"eslint": {
|
||||
"jiti": "2"
|
||||
}
|
||||
}
|
||||
"trustedDependencies": [
|
||||
"@nestjs/core",
|
||||
"@swc/core",
|
||||
"@vmngr/libvirt"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -48,21 +48,6 @@ exit_code=$?
|
||||
# Chown the directory
|
||||
ssh root@"${server_name}" "chown -R root:root /usr/local/unraid-api"
|
||||
|
||||
# Run unraid-api restart on remote host
|
||||
ssh root@"${server_name}" "INTROSPECTION=true LOG_LEVEL=trace unraid-api restart"
|
||||
|
||||
# Play built-in sound based on the operating system
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
afplay /System/Library/Sounds/Glass.aiff
|
||||
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
|
||||
# Linux
|
||||
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||
# Windows
|
||||
powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()"
|
||||
fi
|
||||
|
||||
# Exit with the rsync command's exit code
|
||||
exit $exit_code
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import '@app/dotenv';
|
||||
|
||||
|
||||
@@ -80,5 +80,5 @@ export const KEYSERVER_VALIDATION_ENDPOINT = 'https://keys.lime-technology.com/v
|
||||
/** Set the max retries for the GraphQL Client */
|
||||
export const MAX_RETRIES_FOR_LINEAR_BACKOFF = 100;
|
||||
|
||||
export const PM2_PATH = join(import.meta.dirname, '../../', 'node_modules', '.bin', 'pm2');
|
||||
export const PM2_PATH = 'bun x pm2';
|
||||
export const ECOSYSTEM_PATH = join(import.meta.dirname, '../../', 'ecosystem.config.json');
|
||||
|
||||
8
api/src/core/utils/vms/index.ts
Normal file
8
api/src/core/utils/vms/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Created from 'create-ts-index'
|
||||
|
||||
export * from './domain';
|
||||
export * from './filter-devices';
|
||||
export * from './get-hypervisor';
|
||||
export * from './get-pci-devices';
|
||||
export * from './parse-domain';
|
||||
export * from './parse-domains';
|
||||
@@ -1,3 +0,0 @@
|
||||
import { networkInterfaces } from 'systeminformation';
|
||||
|
||||
export const systemNetworkInterfaces = networkInterfaces();
|
||||
@@ -105,6 +105,7 @@ export const viteNodeApp = async () => {
|
||||
},
|
||||
{ wait: 9999 }
|
||||
);
|
||||
await new Promise(() => {});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error, 'API-ERROR');
|
||||
|
||||
@@ -10,13 +10,13 @@ import { AuthActionVerb } from 'nest-authz';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import type { Permission } from '@app/graphql/generated/api/types';
|
||||
import { environment } from '@app/environment';
|
||||
import { ApiKeySchema, ApiKeyWithSecretSchema } from '@app/graphql/generated/api/operations';
|
||||
import {
|
||||
AddPermissionInput,
|
||||
ApiKey,
|
||||
ApiKeyWithSecret,
|
||||
Permission,
|
||||
Resource,
|
||||
Role,
|
||||
} from '@app/graphql/generated/api/types';
|
||||
|
||||
@@ -86,8 +86,11 @@ export class GraphqlAuthGuard
|
||||
|
||||
// parse cookies from raw headers on initial web socket connection request
|
||||
if (fullContext.connectionParams) {
|
||||
console.log(JSON.stringify(fullContext.req, null, 2));
|
||||
const rawHeaders = fullContext.req.extra.request.rawHeaders;
|
||||
const headerIndex = rawHeaders.findIndex((headerOrValue) => headerOrValue === 'Cookie');
|
||||
const headerIndex = rawHeaders.findIndex(
|
||||
(headerOrValue) => headerOrValue.toLowerCase() === 'cookie'
|
||||
);
|
||||
const cookieString = rawHeaders[headerIndex + 1];
|
||||
request.cookies = parseCookies(cookieString);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChoicesFor, Question, QuestionSet, WhenFor } from 'nest-commander';
|
||||
|
||||
import { Permission, Role } from '@app/graphql/generated/api/types';
|
||||
import type { Permission } from '@app/graphql/generated/api/types';
|
||||
import { Role } from '@app/graphql/generated/api/types';
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { AuthActionVerb } from 'nest-authz';
|
||||
import { Command, CommandRunner, InquirerService, Option } from 'nest-commander';
|
||||
|
||||
import { Permission, Resource, Role } from '@app/graphql/generated/api/types';
|
||||
import type { Permission } from '@app/graphql/generated/api/types';
|
||||
import { Resource, Role } from '@app/graphql/generated/api/types';
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service';
|
||||
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service';
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.comman
|
||||
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service';
|
||||
import { LogsCommand } from '@app/unraid-api/cli/logs.command';
|
||||
import { PM2Service } from '@app/unraid-api/cli/pm2.service';
|
||||
import { ReportCommand } from '@app/unraid-api/cli/report.command';
|
||||
import { RestartCommand } from '@app/unraid-api/cli/restart.command';
|
||||
import { AddSSOUserCommand } from '@app/unraid-api/cli/sso/add-sso-user.command';
|
||||
@@ -31,6 +32,7 @@ import { VersionCommand } from '@app/unraid-api/cli/version.command';
|
||||
RemoveSSOUserQuestionSet,
|
||||
ListSSOUserCommand,
|
||||
LogService,
|
||||
PM2Service,
|
||||
StartCommand,
|
||||
StopCommand,
|
||||
RestartCommand,
|
||||
|
||||
47
api/src/unraid-api/cli/pm2.service.ts
Normal file
47
api/src/unraid-api/cli/pm2.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import type { Options, Result } from 'execa';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { LogService } from '@app/unraid-api/cli/log.service';
|
||||
|
||||
type CmdContext = {
|
||||
tag: string;
|
||||
env?: Record<string, string>;
|
||||
/** Default: false.
|
||||
*
|
||||
* When true, results will not be automatically handled and logged.
|
||||
* The caller must handle desired effects.
|
||||
*/
|
||||
raw?: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PM2Service {
|
||||
constructor(private readonly logger: LogService) {}
|
||||
|
||||
/**
|
||||
* Executes a PM2 command with the provided arguments and environment variables.
|
||||
*
|
||||
* @param context - An object containing a tag for logging purposes and optional environment variables (merging with current env).
|
||||
* @param args - Arguments to pass to the PM2 command. Each arguement is escaped.
|
||||
* @returns A promise that resolves to a Result object containing the command's output.
|
||||
* Logs debug information on success and error details on failure.
|
||||
*/
|
||||
async run(context: CmdContext, ...args: string[]) {
|
||||
const { tag, env, raw = false } = context;
|
||||
const runCommand = () => execa('bun', ['x', 'pm2', ...args], { env } satisfies Options);
|
||||
if (raw) {
|
||||
return runCommand();
|
||||
}
|
||||
return runCommand()
|
||||
.then((result) => {
|
||||
this.logger.debug(result.stdout);
|
||||
return result;
|
||||
})
|
||||
.catch((result: Result) => {
|
||||
this.logger.error(`PM2 error occurred from tag "${tag}": ${result.stdout}\n`);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { LogLevel } from '@app/core/log';
|
||||
import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts';
|
||||
import { levels } from '@app/core/log';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service';
|
||||
import { PM2Service } from '@app/unraid-api/cli/pm2.service';
|
||||
|
||||
interface StartCommandOptions {
|
||||
'log-level'?: string;
|
||||
@@ -12,38 +13,38 @@ interface StartCommandOptions {
|
||||
|
||||
@Command({ name: 'start' })
|
||||
export class StartCommand extends CommandRunner {
|
||||
constructor(private readonly logger: LogService) {
|
||||
constructor(
|
||||
private readonly logger: LogService,
|
||||
private readonly pm2: PM2Service
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async cleanupPM2State() {
|
||||
await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH])
|
||||
.then(({ stdout }) => this.logger.debug(stdout))
|
||||
.catch(({ stderr }) => this.logger.error('PM2 Stop Error: ' + stderr));
|
||||
await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH])
|
||||
.then(({ stdout }) => this.logger.debug(stdout))
|
||||
.catch(({ stderr }) => this.logger.error('PM2 Delete API Error: ' + stderr));
|
||||
|
||||
// Update PM2
|
||||
await execa(PM2_PATH, ['update'])
|
||||
.then(({ stdout }) => this.logger.debug(stdout))
|
||||
.catch(({ stderr }) => this.logger.error('PM2 Update Error: ' + stderr));
|
||||
await this.pm2.run({ tag: 'PM2 Stop' }, 'stop', ECOSYSTEM_PATH);
|
||||
await this.pm2.run({ tag: 'PM2 Delete' }, 'delete', ECOSYSTEM_PATH);
|
||||
await this.pm2.run({ tag: 'PM2 Update' }, 'update');
|
||||
}
|
||||
|
||||
async run(_: string[], options: StartCommandOptions): Promise<void> {
|
||||
this.logger.info('Starting the Unraid API');
|
||||
await this.cleanupPM2State();
|
||||
const envLog = options['log-level'] ? `LOG_LEVEL=${options['log-level']}` : '';
|
||||
const { stderr, stdout } = await execa(`${envLog} ${PM2_PATH}`.trim(), [
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
if (options['log-level']) {
|
||||
env.LOG_LEVEL = options['log-level'];
|
||||
}
|
||||
const { stderr, stdout } = await this.pm2.run(
|
||||
{ tag: 'PM2 Start', env, raw: true },
|
||||
'start',
|
||||
ECOSYSTEM_PATH,
|
||||
'--update-env',
|
||||
]);
|
||||
'--update-env'
|
||||
);
|
||||
if (stdout) {
|
||||
this.logger.log(stdout);
|
||||
this.logger.log(stdout.toString());
|
||||
}
|
||||
if (stderr) {
|
||||
this.logger.error(stderr);
|
||||
this.logger.error(stderr.toString());
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
|
||||
@@ -103,10 +103,12 @@ export class NotificationsService {
|
||||
const notification = await this.loadNotificationFile(path, NotificationType[type]);
|
||||
this.increment(notification.importance, NotificationsService.overview[type.toLowerCase()]);
|
||||
|
||||
this.publishOverview();
|
||||
pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_ADDED, {
|
||||
notificationAdded: notification,
|
||||
});
|
||||
if (type === NotificationType.UNREAD) {
|
||||
this.publishOverview();
|
||||
pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_ADDED, {
|
||||
notificationAdded: notification,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<!ENTITY SHA256 "">
|
||||
<!ENTITY API_version "">
|
||||
<!ENTITY API_SHA256 "">
|
||||
<!ENTITY NODEJS_FILENAME "node-v20.18.1-linux-x64.tar.xz">
|
||||
<!ENTITY NODEJS_SHA256 "c6fa75c841cbffac851678a472f2a5bd612fff8308ef39236190e1f8dbb0e567">
|
||||
<!ENTITY NODEJS_TXZ "https://nodejs.org/dist/v20.18.1/node-v20.18.1-linux-x64.tar.xz">
|
||||
<!ENTITY NODEJS_FILENAME "bun-linux-x64-baseline.zip">
|
||||
<!ENTITY NODEJS_SHA256 "cad7756a6ee16f3432a328f8023fc5cd431106822eacfa6d6d3afbad6fdc24db">
|
||||
<!ENTITY NODEJS_TXZ "https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip">
|
||||
<!ENTITY MAIN_TXZ "">
|
||||
<!ENTITY API_TGZ "">
|
||||
]>
|
||||
@@ -128,29 +128,23 @@ sha256check() {
|
||||
<![CDATA[
|
||||
# Check if the Node.js archive exists
|
||||
if [[ ! -f "/boot/config/plugins/dynamix.my.servers/${NODE_FILE}" ]]; then
|
||||
echo "Node.js archive not found at /boot/config/plugins/dynamix.my.servers/${NODE_FILE}"
|
||||
echo "Bun archive not found at /boot/config/plugins/dynamix.my.servers/${NODE_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Perform a dry run to verify the archive is valid
|
||||
if ! tar --strip-components=1 -tf "/boot/config/plugins/dynamix.my.servers/${NODE_FILE}" > /dev/null; then
|
||||
echo "Node.js archive is corrupt or invalid"
|
||||
if ! unzip -t "/boot/config/plugins/dynamix.my.servers/${NODE_FILE}" > /dev/null; then
|
||||
echo "Bun archive is corrupt or invalid"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Define the target directory for Node.js
|
||||
NODE_DIR="/usr/local/node"
|
||||
|
||||
# Create the target directory if it doesn't exist
|
||||
mkdir -p "${NODE_DIR}" || { echo "Failed to create ${NODE_DIR}"; exit 1; }
|
||||
|
||||
# Extract the archive to the target directory
|
||||
if ! tar --strip-components=1 -xf "/boot/config/plugins/dynamix.my.servers/${NODE_FILE}" -C "${NODE_DIR}"; then
|
||||
echo "Failed to extract Node.js archive"
|
||||
# Extract the Bun binary from the archive and move it to /usr/local/bin
|
||||
if ! unzip -j -o "/boot/config/plugins/dynamix.my.servers/${NODE_FILE}" 'bun-linux-x64-baseline/bun' -d /usr/local/bin/; then
|
||||
echo "Failed to extract Bun binary"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Node.js installation successful"
|
||||
echo "Bun installation successful"
|
||||
exit 0
|
||||
]]>
|
||||
</INLINE>
|
||||
|
||||
4
plugin/source/dynamix.unraid.net/etc/profile.d/bun.sh
Executable file
4
plugin/source/dynamix.unraid.net/etc/profile.d/bun.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Add alias for bunx
|
||||
alias bunx='bun x'
|
||||
@@ -1,6 +0,0 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Add Node.js binary path to PATH if not already present
|
||||
if [[ ":$PATH:" != *":/usr/local/node/bin:"* ]]; then
|
||||
export PATH="/usr/local/node/bin:$PATH"
|
||||
fi
|
||||
@@ -131,19 +131,19 @@ const osVersionBranch = 'stable';
|
||||
// };
|
||||
|
||||
export const serverState: Server = {
|
||||
activationCodeData: {
|
||||
code: 'CC2KP3TDRF',
|
||||
partnerName: 'OEM Partner',
|
||||
partnerUrl: 'https://unraid.net/OEM+Partner',
|
||||
sysModel: 'OEM Partner v1',
|
||||
comment: 'OEM Partner NAS',
|
||||
caseIcon: 'case-model.png',
|
||||
header: '#ffffff',
|
||||
headermetacolor: '#eeeeee',
|
||||
background: '#000000',
|
||||
showBannerGradient: 'yes',
|
||||
partnerLogo: true,
|
||||
},
|
||||
// activationCodeData: {
|
||||
// code: 'CC2KP3TDRF',
|
||||
// partnerName: 'OEM Partner',
|
||||
// partnerUrl: 'https://unraid.net/OEM+Partner',
|
||||
// sysModel: 'OEM Partner v1',
|
||||
// comment: 'OEM Partner NAS',
|
||||
// caseIcon: 'case-model.png',
|
||||
// header: '#ffffff',
|
||||
// headermetacolor: '#eeeeee',
|
||||
// background: '#000000',
|
||||
// showBannerGradient: 'yes',
|
||||
// partnerLogo: true,
|
||||
// },
|
||||
avatar: 'https://source.unsplash.com/300x300/?portrait',
|
||||
config: {
|
||||
id: 'config-id',
|
||||
|
||||
@@ -42,6 +42,13 @@ const { onResult: onNotificationAdded } = useSubscription(notificationAddedSubsc
|
||||
onNotificationAdded(({ data }) => {
|
||||
if (!data) return;
|
||||
const notif = useFragment(NOTIFICATION_FRAGMENT, data.notificationAdded);
|
||||
if (notif.type !== NotificationType.Unread) return;
|
||||
|
||||
// probably smart to leave this log outside the if-block for the initial release
|
||||
console.log('incoming notification', notif);
|
||||
if (!globalThis.toast) {
|
||||
return;
|
||||
}
|
||||
|
||||
const funcMapping: Record<Importance, (typeof globalThis)['toast']['info' | 'error' | 'warning']> = {
|
||||
[Importance.Alert]: globalThis.toast.error,
|
||||
@@ -51,10 +58,12 @@ onNotificationAdded(({ data }) => {
|
||||
const toast = funcMapping[notif.importance];
|
||||
const createOpener = () => ({ label: 'Open', onClick: () => location.assign(notif.link as string) });
|
||||
|
||||
toast(notif.title, {
|
||||
description: notif.subject,
|
||||
action: notif.link ? createOpener() : undefined,
|
||||
});
|
||||
requestAnimationFrame(() =>
|
||||
toast(notif.title, {
|
||||
description: notif.subject,
|
||||
action: notif.link ? createOpener() : undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const overview = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user