You've already forked AstralRinth
forked from didirus/AstralRinth
Add notices system to Servers (#3502)
* Servers notices * Refresh on unassign
This commit is contained in:
@@ -89,6 +89,10 @@
|
||||
<InfoIcon class="h-5 w-5" />
|
||||
<span>Details</span>
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
|
||||
<span>Copy ID</span>
|
||||
</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -108,6 +112,7 @@ import {
|
||||
ServerIcon,
|
||||
InfoIcon,
|
||||
MoreVerticalIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { useRouter } from "vue-router";
|
||||
@@ -116,6 +121,8 @@ import { useStorage } from "@vueuse/core";
|
||||
type ServerAction = "start" | "stop" | "restart" | "kill";
|
||||
type ServerState = "stopped" | "starting" | "running" | "stopping" | "restarting";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
|
||||
interface PowerAction {
|
||||
action: ServerAction;
|
||||
nextState: ServerState;
|
||||
@@ -198,8 +205,19 @@ const menuOptions = computed(() => [
|
||||
icon: InfoIcon,
|
||||
action: () => detailsModal.value?.show(),
|
||||
},
|
||||
{
|
||||
id: "copy-id",
|
||||
label: "Copy ID",
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => copyId(),
|
||||
shown: flags.value.developerMode,
|
||||
},
|
||||
]);
|
||||
|
||||
async function copyId() {
|
||||
await navigator.clipboard.writeText(serverId as string);
|
||||
}
|
||||
|
||||
function initiateAction(action: ServerAction) {
|
||||
if (!canTakeAction.value) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modrinth/ui";
|
||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||
import { ref } from "vue";
|
||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
||||
|
||||
const app = useNuxtApp() as unknown as { $notify: any };
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
}>();
|
||||
|
||||
const notice = ref<ServerNoticeType>();
|
||||
|
||||
const assigned = ref<ServerNoticeType["assigned"]>([]);
|
||||
|
||||
const assignedServers = computed(() => assigned.value.filter((n) => n.kind === "server") ?? []);
|
||||
const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === "node") ?? []);
|
||||
|
||||
const inputField = ref("");
|
||||
|
||||
async function refresh() {
|
||||
await usePyroFetch("notices").then((res) => {
|
||||
const notices = res as ServerNoticeType[];
|
||||
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
async function assign(server: boolean = true) {
|
||||
const input = inputField.value.trim();
|
||||
|
||||
if (input !== "" && notice.value) {
|
||||
await usePyroFetch(`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`, {
|
||||
method: "PUT",
|
||||
}).catch((err) => {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error assigning notice",
|
||||
text: err,
|
||||
type: "error",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error assigning notice",
|
||||
text: "No server or node specified",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function unassignDetect() {
|
||||
const input = inputField.value.trim();
|
||||
|
||||
const server = assignedServers.value.some((assigned) => assigned.id === input);
|
||||
const node = assignedNodes.value.some((assigned) => assigned.id === input);
|
||||
|
||||
if (!server && !node) {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error unassigning notice",
|
||||
text: "ID is not an assigned server or node",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await unassign(input, server);
|
||||
}
|
||||
|
||||
async function unassign(id: string, server: boolean = true) {
|
||||
if (notice.value) {
|
||||
await usePyroFetch(`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`, {
|
||||
method: "PUT",
|
||||
}).catch((err) => {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error unassigning notice",
|
||||
text: err,
|
||||
type: "error",
|
||||
});
|
||||
});
|
||||
}
|
||||
await refresh();
|
||||
}
|
||||
|
||||
function show(currentNotice: ServerNoticeType) {
|
||||
notice.value = currentNotice;
|
||||
assigned.value = currentNotice?.assigned ?? [];
|
||||
modal.value?.show();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide();
|
||||
}
|
||||
|
||||
defineExpose({ show, hide });
|
||||
</script>
|
||||
<template>
|
||||
<NewModal ref="modal" :on-hide="() => emit('close')">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
Editing assignments of notice #{{ notice?.id }}
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<ServerNotice
|
||||
v-if="notice"
|
||||
:level="notice.level"
|
||||
:message="notice.message"
|
||||
:dismissable="notice.dismissable"
|
||||
preview
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="server-assign-field" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast"> Assigned servers </span>
|
||||
</label>
|
||||
<Accordion
|
||||
v-if="assignedServers.length > 0"
|
||||
class="mb-2"
|
||||
open-by-default
|
||||
button-class="text-primary m-0 p-0 border-none bg-transparent active:scale-95"
|
||||
>
|
||||
<template #title> {{ assignedServers.length }} servers </template>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<TagItem
|
||||
v-for="server in assignedServers"
|
||||
:key="`server-${server.id}`"
|
||||
:action="() => unassign(server.id, true)"
|
||||
>
|
||||
<XIcon />
|
||||
{{ server.id }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</Accordion>
|
||||
<span v-else class="mb-2"> No servers assigned yet </span>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="server-assign-field" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast"> Assigned nodes </span>
|
||||
</label>
|
||||
<Accordion
|
||||
v-if="assignedNodes.length > 0"
|
||||
class="mb-2"
|
||||
open-by-default
|
||||
button-class="text-primary m-0 p-0 border-none bg-transparent active:scale-95"
|
||||
>
|
||||
<template #title> {{ assignedNodes.length }} nodes </template>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<TagItem
|
||||
v-for="node in assignedNodes"
|
||||
:key="`node-${node.id}`"
|
||||
:action="
|
||||
() => {
|
||||
unassign(node.id, false);
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
{{ node.id }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</Accordion>
|
||||
<span v-else class="mb-2"> No nodes assigned yet </span>
|
||||
</div>
|
||||
<div class="flex w-[45rem] items-center gap-2">
|
||||
<input
|
||||
id="server-assign-field"
|
||||
v-model="inputField"
|
||||
class="w-full"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<ButtonStyled color="green" color-fill="text">
|
||||
<button class="shrink-0" @click="() => assign(true)">
|
||||
<PlusIcon />
|
||||
Add server
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="blue" color-fill="text">
|
||||
<button class="shrink-0" @click="() => assign(false)">
|
||||
<PlusIcon />
|
||||
Add node
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red" color-fill="text">
|
||||
<button class="shrink-0" @click="() => unassignDetect()">
|
||||
<XIcon />
|
||||
Remove
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
|
||||
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
|
||||
import { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||
import {
|
||||
DISMISSABLE,
|
||||
getDismissableMetadata,
|
||||
NOTICE_LEVELS,
|
||||
} from "@modrinth/ui/src/utils/notices.ts";
|
||||
import { useVIntl } from "@vintl/vintl";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
const props = defineProps<{
|
||||
notice: ServerNoticeType;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<div class="col-span-full grid grid-cols-subgrid gap-4 rounded-2xl bg-bg-raised p-4">
|
||||
<div class="col-span-full grid grid-cols-subgrid items-center gap-4">
|
||||
<div>
|
||||
<CopyCode :text="`${notice.id}`" />
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span v-if="notice.announce_at">
|
||||
{{ dayjs(notice.announce_at).format("MMM D, YYYY [at] h:mm A") }} ({{
|
||||
dayjs(notice.announce_at).fromNow()
|
||||
}})
|
||||
</span>
|
||||
<template v-else> Never begins </template>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span
|
||||
v-if="notice.expires"
|
||||
v-tooltip="dayjs(notice.expires).format('MMMM D, YYYY [at] h:mm A')"
|
||||
>
|
||||
{{ dayjs(notice.expires).fromNow() }}
|
||||
</span>
|
||||
<template v-else> Never expires </template>
|
||||
</div>
|
||||
<div
|
||||
:style="
|
||||
NOTICE_LEVELS[notice.level]
|
||||
? {
|
||||
'--_color': NOTICE_LEVELS[notice.level].colors.text,
|
||||
'--_bg-color': NOTICE_LEVELS[notice.level].colors.bg,
|
||||
}
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<TagItem>
|
||||
{{
|
||||
NOTICE_LEVELS[notice.level]
|
||||
? formatMessage(NOTICE_LEVELS[notice.level].name)
|
||||
: notice.level
|
||||
}}
|
||||
</TagItem>
|
||||
</div>
|
||||
<div
|
||||
:style="{
|
||||
'--_color': getDismissableMetadata(notice.dismissable).colors.text,
|
||||
'--_bg-color': getDismissableMetadata(notice.dismissable).colors.bg,
|
||||
}"
|
||||
>
|
||||
<TagItem>
|
||||
{{ formatMessage(getDismissableMetadata(notice.dismissable).name) }}
|
||||
</TagItem>
|
||||
</div>
|
||||
<div class="col-span-2 flex gap-2 md:col-span-1">
|
||||
<ButtonStyled>
|
||||
<button @click="() => startEditing(notice)">
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="() => deleteNotice(notice)">
|
||||
<TrashIcon /> {{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full grid">
|
||||
<ServerNotice
|
||||
:level="notice.level"
|
||||
:message="notice.message"
|
||||
:dismissable="notice.dismissable"
|
||||
preview
|
||||
/>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span v-if="!notice.assigned || notice.assigned.length === 0"
|
||||
>Not assigned to any servers</span
|
||||
>
|
||||
<span v-else-if="!notice.assigned.some((n) => n.kind === 'server')">
|
||||
Assigned to
|
||||
{{ notice.assigned.filter((n) => n.kind === "node").length }} nodes
|
||||
</span>
|
||||
<span v-else-if="!notice.assigned.some((n) => n.kind === 'node')">
|
||||
Assigned to
|
||||
{{ notice.assigned.filter((n) => n.kind === "server").length }} servers
|
||||
</span>
|
||||
<span v-else>
|
||||
Assigned to
|
||||
{{ notice.assigned.filter((n) => n.kind === "server").length }} servers and
|
||||
{{ notice.assigned.filter((n) => n.kind === "node").length }} nodes
|
||||
</span>
|
||||
•
|
||||
<button
|
||||
class="m-0 flex items-center gap-1 border-none bg-transparent p-0 text-blue hover:underline hover:brightness-125 active:scale-95 active:brightness-150"
|
||||
@click="() => startEditing(notice, true)"
|
||||
>
|
||||
<SettingsIcon />
|
||||
Edit assignments
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user