Skip to content
Merged
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
566 changes: 243 additions & 323 deletions components/servers/ServerAccess.vue

Large diffs are not rendered by default.

585 changes: 236 additions & 349 deletions components/servers/ServerMapRotation.vue

Large diffs are not rendered by default.

388 changes: 144 additions & 244 deletions components/servers/ServerPlugins.vue

Large diffs are not rendered by default.

263 changes: 263 additions & 0 deletions components/servers/ServerSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import gql from "graphql-tag";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import SettingsSideTabs from "~/components/settings/SettingsSideTabs.vue";
import SettingsSaveBar from "~/components/settings/SettingsSaveBar.vue";
import ServerMapRotation from "~/components/servers/ServerMapRotation.vue";
import ServerPlugins from "~/components/servers/ServerPlugins.vue";
import ServerAccess from "~/components/servers/ServerAccess.vue";
import { toast } from "@/components/ui/toast";
import {
tacticalSectionLabelClasses,
tacticalSectionTickClasses,
} from "~/utilities/tacticalClasses";

type Change = { text: string; restart: boolean };

type SettingsPane = {
changes: Array<Change>;
payload: () => unknown;
reset: () => void;
saved: () => Promise<void>;
};

const props = defineProps<{
server: {
id: string;
enabled: boolean;
game_server_node_id: string | null;
};
}>();

const { t } = useI18n();
const nuxtApp = useNuxtApp();
const route = useRoute();
const router = useRouter();

// Raw document: setServerSettings is newer than ~/generated/zeus until codegen
// runs against a migrated database.
const SAVE = gql`
mutation SetServerSettings(
$serverId: uuid!
$mapRotation: ServerMapRotationInput
$plugins: [ServerPluginInput!]
$access: ServerAccessInput
) {
setServerSettings(
server_id: $serverId
map_rotation: $mapRotation
plugins: $plugins
access: $access
) {
success
}
}
`;

// Rotation and plugins reach the server in its pod spec, so an external server
// only has access, which its Player Management plugin enforces.
const hasPod = computed(() => !!props.server.game_server_node_id);

const tabs = computed(() => [
...(hasPod.value
? [
{
key: "rotation",
label: t("pages.dedicated_servers.detail.map_rotation.title"),
},
{
key: "plugins",
label: t("pages.dedicated_servers.detail.plugins.title"),
},
]
: []),
{ key: "access", label: t("pages.dedicated_servers.detail.access.title") },
]);

const activeTab = computed(() => {
const requested = route.query.settings;

return tabs.value.some((tab) => tab.key === requested)
? (requested as string)
: tabs.value[0].key;
});

function tabPath(key: string): string {
return router.resolve({ query: { ...route.query, settings: key } })
.fullPath;
}

const tabGroups = computed(() => [
{
label: t("pages.dedicated_servers.detail.settings.group"),
items: tabs.value.map((tab) => ({ path: tabPath(tab.key), label: tab.label })),
},
]);

const mobileTab = computed({
get: () => activeTab.value,
set: (key: string) => {
void router.replace({ query: { ...route.query, settings: key } });
},
});

const rotation = ref<SettingsPane | null>(null);
const plugins = ref<SettingsPane | null>(null);
const access = ref<SettingsPane | null>(null);
const submitting = ref(false);

const sections = computed(() =>
[rotation.value, plugins.value, access.value].filter(
(section): section is SettingsPane => !!section,
),
);

const changes = computed<Array<Change>>(() =>
sections.value.flatMap((section) => section.changes ?? []),
);

const restarts = computed(() => changes.value.some((change) => change.restart));

const summary = computed(() => {
if (!changes.value.length) {
return "";
}

const list = changes.value.map((change) => change.text).join(", ");

return `${list}. ${t(
restarts.value
? "pages.dedicated_servers.detail.settings.restart_note"
: "pages.dedicated_servers.detail.settings.live_note",
)}`;
});

function dirtyPayload(section: SettingsPane | null) {
return section?.changes.length ? section.payload() : null;
}

async function save() {
if (submitting.value || !changes.value.length) {
return;
}

submitting.value = true;
const restarting = restarts.value;

try {
await nuxtApp.$apollo.defaultClient.mutate({
mutation: SAVE,
variables: {
serverId: props.server.id,
mapRotation: dirtyPayload(rotation.value),
plugins: dirtyPayload(plugins.value),
access: dirtyPayload(access.value),
},
});

toast({
title: t(
!restarting
? "pages.dedicated_servers.detail.settings.saved_live"
: props.server.enabled
? "pages.dedicated_servers.detail.settings.saved_restart"
: "pages.dedicated_servers.detail.settings.saved_disabled",
),
});

await Promise.all(sections.value.map((section) => section.saved()));
} finally {
submitting.value = false;
}
}

function discard() {
for (const section of sections.value) {
section.reset();
}
}
</script>

<template>
<section class="grid gap-4">
<div :class="[tacticalSectionLabelClasses, 'mb-0']">
<span :class="tacticalSectionTickClasses"></span>
{{ $t("pages.dedicated_servers.detail.settings.title") }}
</div>

<div class="flex flex-col gap-6 lg:flex-row lg:gap-8">
<aside class="shrink-0">
<div class="lg:hidden">
<Select v-model="mobileTab">
<SelectTrigger
class="w-full"
:aria-label="
$t('pages.dedicated_servers.detail.settings.select_section')
"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="tab in tabs" :key="tab.key" :value="tab.key">
{{ tab.label }}
</SelectItem>
</SelectContent>
</Select>
</div>
<SettingsSideTabs
:groups="tabGroups"
:active-path="tabPath(activeTab)"
:aria-label="$t('pages.dedicated_servers.detail.settings.title')"
/>
</aside>

<!-- Every section stays mounted, so an edit survives switching tabs and
one save sends all of them. Locked while saving so nothing typed
mid-save is marked saved without having been sent. -->
<div
class="min-w-0 max-w-3xl flex-1"
:inert="submitting || undefined"
:aria-busy="submitting"
>
<ServerMapRotation
v-if="hasPod"
v-show="activeTab === 'rotation'"
ref="rotation"
:server-id="server.id"
/>
<ServerPlugins
v-if="hasPod"
v-show="activeTab === 'plugins'"
ref="plugins"
:server-id="server.id"
/>
<ServerAccess
v-show="activeTab === 'access'"
ref="access"
:server-id="server.id"
/>
</div>
</div>

<SettingsSaveBar
:dirty="changes.length > 0"
:submitting="submitting"
:description="summary"
:action-label="
restarts
? $t('pages.dedicated_servers.detail.save_restart')
: $t('pages.dedicated_servers.detail.settings.save')
"
@save="save"
@discard="discard"
/>
</section>
</template>
47 changes: 32 additions & 15 deletions i18n/locales/ar_SA.json
Original file line number Diff line number Diff line change
Expand Up @@ -3699,15 +3699,12 @@
"continue": "متابعة"
},
"save_restart": "حفظ وإعادة التشغيل",
"restart_hint": "يؤدي الحفظ إلى إعادة تشغيل الخادم وفصل كل من يلعب عليه.",
"map_rotation": {
"title": "تدوير الخرائط",
"description": "الخرائط التي يتنقل بينها هذا الخادم. يبدأ على الخريطة الأولى، أو على خريطة عشوائية عند تفعيل الخلط، ثم تتولى إضافة تدوير الخرائط تصويتات نهاية الخريطة وrock the vote والتدوير التلقائي.",
"maps": "الخرائط",
"shuffle": "خلط",
"shuffle_hint": "اختيار الخريطة التالية عشوائيًا",
"in_order": "بالترتيب",
"in_order_hint": "لعب القائمة من الأعلى إلى الأسفل",
"no_plugin": "لا توجد إضافة مثبتة لتدوير الخرائط، لذا سيبقى الخادم على خريطته الأولى.",
"install_plugin": "تثبيت MapChooser",
"workshop_order_hint": "تفقد MapChooser موضعها في القائمة مع خرائط الورشة وتعود دائمًا إلى الخريطة الثانية. أبقِ الخلط مفعّلًا لخرائط الورشة.",
Expand All @@ -3727,14 +3724,11 @@
"imported_skipped": "تم تخطيها لأنها ليست خرائط CS2: {count}",
"move_up": "نقل لأعلى",
"move_down": "نقل لأسفل",
"remove": "إزالة",
"saved": "تم حفظ تدوير الخرائط. تتم إعادة تشغيل الخادم.",
"saved_disabled": "تم حفظ تدوير الخرائط. سيُطبَّق عند تفعيل الخادم."
"remove": "إزالة"
},
"plugins": {
"title": "الإضافات",
"description": "فعّل إضافة لعبة أو عطّلها لهذا الخادم فقط. الافتراضي يتبع إعدادات التحميل الخاصة بالإضافة.",
"active": "نشطة",
"default": "افتراضي",
"on": "تشغيل",
"off": "إيقاف",
Expand All @@ -3745,35 +3739,58 @@
"forced_on": "مفعّلة لهذا الخادم",
"forced_off": "معطّلة لهذا الخادم",
"for_rotation": "تُحمَّل لتشغيل تدوير الخرائط",
"map_rotation": "تدير تدوير الخرائط",
"from_mode": "من {mode}",
"empty": "لا توجد إضافات لعبة مثبتة بعد.",
"browse": "دليل الإضافات",
"saved": "تم حفظ الإضافات. تتم إعادة تشغيل الخادم.",
"saved_disabled": "تم حفظ الإضافات. ستُطبَّق عند تفعيل الخادم."
"browse": "دليل الإضافات"
},
"access": {
"title": "الوصول",
"description": "اختر من يمكنه الاتصال. يُخفى الخادم المقيّد عن الجميع، وترفضهم إضافة Player Management عند الاتصال.",
"open": "مفتوح",
"restricted": "مقيّد",
"open_hint": "يمكن لأي شخص العثور على هذا الخادم والانضمام إليه.",
"plugin_missing": "لم تتواصل إضافة Player Management من هذا الخادم خلال آخر 3 دقائق، لذا لا يُمنع أحد بعد.",
"staff": "يمكن للمشرفين ومن فوقهم الانضمام دائمًا.",
"min_role": "الحد الأدنى للدور",
"min_role_hint": "يمكن لكل من لديه هذا الدور أو أعلى الانضمام.",
"no_role": "بلا قاعدة دور",
"role_and_up": "{role} فما فوق",
"players": "اللاعبون",
"players_hint": "لاعبون مختارون يمكنهم الانضمام دائمًا.",
"add_player": "إضافة لاعب",
"already_added": "موجود في القائمة بالفعل",
"events": "الفعاليات",
"add_event": "إضافة فعالية",
"events_hint": "يمكن لأعضاء الفعالية الانضمام فقط أثناء انعقادها.",
"no_events": "لم يتم العثور على فعاليات",
"remove": "إزالة",
"remove": "إزالة"
},
"settings": {
"title": "إعدادات الخادم",
"group": "خادم المجتمع",
"select_section": "القسم",
"save": "حفظ",
"save_hint": "يُطبَّق خلال ثوانٍ. يُزال اللاعبون غير المسموح لهم.",
"saved": "تم حفظ الوصول. سيطبقه الخادم خلال ثوانٍ."
"restart_note": "يؤدي الحفظ إلى إعادة تشغيل الخادم مرة واحدة.",
"live_note": "يُطبَّق خلال ثوانٍ.",
"saved_restart": "تم الحفظ. تتم إعادة تشغيل الخادم.",
"saved_live": "تم الحفظ. سيطبقه الخادم خلال ثوانٍ.",
"saved_disabled": "تم الحفظ. سيُطبَّق عند تفعيل الخادم.",
"changes": {
"maps_added": "الخرائط المضافة: {count}",
"maps_removed": "الخرائط المزالة: {count}",
"map_order": "تغيّر ترتيب الخرائط",
"shuffle_on": "الخلط مفعّل",
"shuffle_off": "الخلط معطّل",
"plugin_default": "{plugin}: افتراضي",
"plugin_on": "{plugin}: تشغيل",
"plugin_off": "{plugin}: إيقاف",
"restricted": "مقيّد",
"opened": "مفتوح للجميع",
"min_role": "تغيّر الحد الأدنى للدور",
"players_added": "اللاعبون المضافون: {count}",
"players_removed": "اللاعبون المزالون: {count}",
"events_added": "الفعاليات المضافة: {count}",
"events_removed": "الفعاليات المزالة: {count}"
}
}
}
},
Expand Down
Loading
Loading