mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-23 15:40:25 -06:00
Merge branch '1.23.X' into 1.23.X-merge-to-2.X.X-2
# Conflicts: # docker/debian-base.dockerfile
This commit is contained in:
commit
81c9900e23
17
package-lock.json
generated
17
package-lock.json
generated
@ -32,7 +32,7 @@
|
|||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "~4.1.0",
|
"gamedig": "^4.2.0",
|
||||||
"html-escaper": "^3.0.3",
|
"html-escaper": "^3.0.3",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "~5.0.0",
|
"http-proxy-agent": "~5.0.0",
|
||||||
@ -65,6 +65,7 @@
|
|||||||
"playwright-core": "~1.35.1",
|
"playwright-core": "~1.35.1",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"promisify-child-process": "~4.1.2",
|
||||||
"protobufjs": "~7.2.4",
|
"protobufjs": "~7.2.4",
|
||||||
"qs": "~6.10.4",
|
"qs": "~6.10.4",
|
||||||
"redbean-node": "~0.3.0",
|
"redbean-node": "~0.3.0",
|
||||||
@ -8529,9 +8530,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gamedig": {
|
"node_modules/gamedig": {
|
||||||
"version": "4.1.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gamedig/-/gamedig-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/gamedig/-/gamedig-4.2.0.tgz",
|
||||||
"integrity": "sha512-jvLUEakihJgpiw9t9yQRsbcemeALeTNlnaWY1gvYdwI63ZlkxznTaLqX5K/eluRTTCtAWNW3YceT6NVjyAZIwA==",
|
"integrity": "sha512-UwV9gT1PrOTxjwGzj9/i8XJLx9QqmzFtrRJnRLqN7fZWEIRHjbuUpx2b6Y3v/5QyRDFP+vaB3Pm0hw3Xg4RnOQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.0.0-rc.10",
|
"cheerio": "^1.0.0-rc.10",
|
||||||
"gbxremote": "^0.2.1",
|
"gbxremote": "^0.2.1",
|
||||||
@ -12943,6 +12944,14 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/promisify-child-process": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/promisify-child-process/-/promisify-child-process-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-APnkIgmaHNJpkAn7k+CrJSi9WMuff5ctYFbD0CO2XIPkM8yO7d/ShouU2clywbpHV/DUsyc4bpJCsNgddNtx4g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prompts": {
|
"node_modules/prompts": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "~4.1.0",
|
"gamedig": "^4.2.0",
|
||||||
"html-escaper": "^3.0.3",
|
"html-escaper": "^3.0.3",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "~5.0.0",
|
"http-proxy-agent": "~5.0.0",
|
||||||
@ -129,6 +129,7 @@
|
|||||||
"playwright-core": "~1.35.1",
|
"playwright-core": "~1.35.1",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"promisify-child-process": "~4.1.2",
|
||||||
"protobufjs": "~7.2.4",
|
"protobufjs": "~7.2.4",
|
||||||
"qs": "~6.10.4",
|
"qs": "~6.10.4",
|
||||||
"redbean-node": "~0.3.0",
|
"redbean-node": "~0.3.0",
|
||||||
|
@ -43,6 +43,7 @@ if (process.platform === "win32") {
|
|||||||
"/usr/bin/chromium",
|
"/usr/bin/chromium",
|
||||||
"/usr/bin/chromium-browser",
|
"/usr/bin/chromium-browser",
|
||||||
"/usr/bin/google-chrome",
|
"/usr/bin/google-chrome",
|
||||||
|
"/snap/bin/chromium", // Ubuntu
|
||||||
];
|
];
|
||||||
} else if (process.platform === "darwin") {
|
} else if (process.platform === "darwin") {
|
||||||
allowedList = [
|
allowedList = [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
const { MonitorType } = require("./monitor-type");
|
||||||
const { UP } = require("../../src/util");
|
const { UP } = require("../../src/util");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TailscalePing class extends the MonitorType.
|
* A TailscalePing class extends the MonitorType.
|
||||||
@ -37,12 +37,9 @@ class TailscalePing extends MonitorType {
|
|||||||
*/
|
*/
|
||||||
async runTailscalePing(hostname, interval) {
|
async runTailscalePing(hostname, interval) {
|
||||||
let timeout = interval * 1000 * 0.8;
|
let timeout = interval * 1000 * 0.8;
|
||||||
let res = childProcess.spawnSync("tailscale", [ "ping", hostname ], {
|
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
|
||||||
timeout: timeout
|
timeout: timeout
|
||||||
});
|
});
|
||||||
if (res.error) {
|
|
||||||
throw new Error(`Execution error: ${res.error.message}`);
|
|
||||||
}
|
|
||||||
if (res.stderr && res.stderr.toString()) {
|
if (res.stderr && res.stderr.toString()) {
|
||||||
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
|
|
||||||
class Apprise extends NotificationProvider {
|
class Apprise extends NotificationProvider {
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ class Apprise extends NotificationProvider {
|
|||||||
args.push("-t");
|
args.push("-t");
|
||||||
args.push(notification.title);
|
args.push(notification.title);
|
||||||
}
|
}
|
||||||
const s = childProcess.spawnSync("apprise", args);
|
const s = await childProcessAsync.spawn("apprise", args);
|
||||||
|
|
||||||
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||||
|
|
||||||
|
@ -1335,9 +1335,9 @@ let needSetup = false;
|
|||||||
// Update nscd status
|
// Update nscd status
|
||||||
if (previousNSCDStatus !== data.nscd) {
|
if (previousNSCDStatus !== data.nscd) {
|
||||||
if (data.nscd) {
|
if (data.nscd) {
|
||||||
server.startNSCDServices();
|
await server.startNSCDServices();
|
||||||
} else {
|
} else {
|
||||||
server.stopNSCDServices();
|
await server.stopNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ const util = require("util");
|
|||||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||||
@ -372,7 +372,7 @@ class UptimeKumaServer {
|
|||||||
let enable = await Settings.get("nscd");
|
let enable = await Settings.get("nscd");
|
||||||
|
|
||||||
if (enable || enable === null) {
|
if (enable || enable === null) {
|
||||||
this.startNSCDServices();
|
await this.startNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +384,7 @@ class UptimeKumaServer {
|
|||||||
let enable = await Settings.get("nscd");
|
let enable = await Settings.get("nscd");
|
||||||
|
|
||||||
if (enable || enable === null) {
|
if (enable || enable === null) {
|
||||||
this.stopNSCDServices();
|
await this.stopNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,11 +393,11 @@ class UptimeKumaServer {
|
|||||||
* For now, only used in Docker
|
* For now, only used in Docker
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
startNSCDServices() {
|
async startNSCDServices() {
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
try {
|
try {
|
||||||
log.info("services", "Starting nscd");
|
log.info("services", "Starting nscd");
|
||||||
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
|
await childProcessAsync.exec("sudo service nscd start");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info("services", "Failed to start nscd");
|
log.info("services", "Failed to start nscd");
|
||||||
}
|
}
|
||||||
@ -408,11 +408,11 @@ class UptimeKumaServer {
|
|||||||
* Stop all system services
|
* Stop all system services
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
stopNSCDServices() {
|
async stopNSCDServices() {
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
try {
|
try {
|
||||||
log.info("services", "Stopping nscd");
|
log.info("services", "Stopping nscd");
|
||||||
childProcess.execSync("sudo service nscd stop");
|
await childProcessAsync.exec("sudo service nscd stop");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info("services", "Failed to stop nscd");
|
log.info("services", "Failed to stop nscd");
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="!enabled"
|
:disabled="!enabled"
|
||||||
>
|
>
|
||||||
<a class="btn btn-outline-primary" @click="action()">
|
<button class="btn btn-outline-primary" @click="action()" :aria-label="actionAriaLabel">
|
||||||
<font-awesome-icon :icon="icon" />
|
<font-awesome-icon :icon="icon" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -66,6 +66,13 @@ export default {
|
|||||||
action: {
|
action: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The aria-label of the action button
|
||||||
|
*/
|
||||||
|
actionAriaLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: [ "update:modelValue" ],
|
emits: [ "update:modelValue" ],
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<select ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
|
<select :id="id" ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
|
||||||
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
|
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
<a class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" @click="action()">
|
<button class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" :aria-label="actionAriaLabel" @click="action()">
|
||||||
<font-awesome-icon :icon="icon" />
|
<font-awesome-icon :icon="icon" aria-hidden="true" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -20,6 +20,13 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* The id of the form which will be targeted by a <label for=..
|
||||||
|
*/
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* The value of the select field.
|
* The value of the select field.
|
||||||
*/
|
*/
|
||||||
@ -51,6 +58,13 @@ export default {
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* The aria-label of the action button
|
||||||
|
*/
|
||||||
|
actionAriaLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Whether the action button is disabled.
|
* Whether the action button is disabled.
|
||||||
* @example true
|
* @example true
|
||||||
|
@ -60,13 +60,13 @@
|
|||||||
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
||||||
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
||||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
<span>{{ day }} {{ $tc("day", day) }}</span>
|
||||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
|
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)" :aria-label="$t('Remove the expiry notification')">
|
||||||
<font-awesome-icon class="" icon="times" />
|
<font-awesome-icon icon="times" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-xl-6">
|
<div class="col-12 col-xl-6">
|
||||||
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
|
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||||
|
@ -348,6 +348,8 @@
|
|||||||
"Fingerprint:": "Fingerprint:",
|
"Fingerprint:": "Fingerprint:",
|
||||||
"No status pages": "No status pages",
|
"No status pages": "No status pages",
|
||||||
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
||||||
|
"Add a new expiry notification day": "Add a new expiry notification day",
|
||||||
|
"Remove the expiry notification": "Remove the expiry notification day",
|
||||||
"Proxy": "Proxy",
|
"Proxy": "Proxy",
|
||||||
"Date Created": "Date Created",
|
"Date Created": "Date Created",
|
||||||
"Footer Text": "Footer Text",
|
"Footer Text": "Footer Text",
|
||||||
@ -683,6 +685,10 @@
|
|||||||
"Notify Channel": "Notify Channel",
|
"Notify Channel": "Notify Channel",
|
||||||
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
||||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||||
|
"setup a new monitor group": "setup a new monitor group",
|
||||||
|
"openModalTo": "open modal to {0}",
|
||||||
|
"Add a domain": "Add a domain",
|
||||||
|
"Remove domain": "Remove domain '{0}'",
|
||||||
"Icon Emoji": "Icon Emoji",
|
"Icon Emoji": "Icon Emoji",
|
||||||
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
||||||
"aboutWebhooks": "More info about Webhooks on: {0}",
|
"aboutWebhooks": "More info about Webhooks on: {0}",
|
||||||
|
@ -291,7 +291,9 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
||||||
<ActionSelect
|
<ActionSelect
|
||||||
|
id="docker-host"
|
||||||
v-model="monitor.docker_host"
|
v-model="monitor.docker_host"
|
||||||
|
:action-aria-label="$t('openModalTo', $t('Setup Docker Host'))"
|
||||||
:options="dockerHostOptionsList"
|
:options="dockerHostOptionsList"
|
||||||
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
|
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
@ -501,9 +503,11 @@
|
|||||||
|
|
||||||
<!-- Parent Monitor -->
|
<!-- Parent Monitor -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
|
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
|
||||||
<ActionSelect
|
<ActionSelect
|
||||||
|
id="monitorGroupSelector"
|
||||||
v-model="monitor.parent"
|
v-model="monitor.parent"
|
||||||
|
:action-aria-label="$t('openModalTo', 'setup a new monitor group')"
|
||||||
:options="parentMonitorOptionsList"
|
:options="parentMonitorOptionsList"
|
||||||
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
@ -866,7 +870,7 @@ const monitorDefaults = {
|
|||||||
interval: 60,
|
interval: 60,
|
||||||
retryInterval: 60,
|
retryInterval: 60,
|
||||||
resendInterval: 0,
|
resendInterval: 0,
|
||||||
maxretries: 1,
|
maxretries: 0,
|
||||||
timeout: 48,
|
timeout: 48,
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
ignoreTls: false,
|
ignoreTls: false,
|
||||||
|
@ -69,13 +69,17 @@
|
|||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
{{ $t("Domain Names") }}
|
{{ $t("Domain Names") }}
|
||||||
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
|
<button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" @click="addDomainField">
|
||||||
|
<font-awesome-icon icon="plus-circle" class="action text-primary" />
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<ul class="list-group domain-name-list">
|
<ul class="list-group domain-name-list">
|
||||||
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
||||||
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
||||||
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="removeDomain(index)" />
|
<button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [ domain ])" @click="removeDomain(index)">
|
||||||
|
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user