This commit is contained in:
Anbraten
2025-12-03 23:49:11 +01:00
parent 01765d50f3
commit f761d166d4
14 changed files with 468 additions and 94 deletions

View File

@@ -1,17 +1,19 @@
import { onBeforeUnmount, onMounted, ref } from 'vue';
export function useInterval(fn: () => void | Promise<void>, ms: number): void {
const id = ref<number>();
export function useInterval(fn: () => void | Promise<void>, ms: number, options?: { immediate?: boolean }): void {
const id = ref<number | null>(null);
onMounted(async () => {
await fn(); // run once immediately
if ((options?.immediate ?? true) === true) {
await fn(); // run once immediately
}
id.value = window.setInterval(() => {
void fn();
}, ms);
});
onBeforeUnmount(() => {
if (id.value != null) {
if (id.value !== null) {
window.clearInterval(id.value);
}
});

View File

@@ -342,12 +342,12 @@ export default class WoodpeckerClient extends ApiClient {
return this._post(`/api/orgs/${orgId}/agents`, agent) as Promise<Agent>;
}
async updateOrgAgent(orgId: number, agentId: number, agent: Partial<Agent>): Promise<Agent> {
return this._patch(`/api/orgs/${orgId}/agents/${agentId}`, agent) as Promise<Agent>;
async updateOrgAgent(orgId: number, agent: Agent): Promise<Agent> {
return this._patch(`/api/orgs/${orgId}/agents/${agent.id}`, agent) as Promise<Agent>;
}
async deleteOrgAgent(orgId: number, agentId: number): Promise<unknown> {
return this._delete(`/api/orgs/${orgId}/agents/${agentId}`);
async deleteOrgAgent(orgId: number, agent: Agent): Promise<unknown> {
return this._delete(`/api/orgs/${orgId}/agents/${agent.id}`);
}
async getForges(opts?: PaginationOptions): Promise<Forge[] | null> {

View File

@@ -213,9 +213,24 @@ const routes: RouteRecordRaw[] = [
},
{
path: 'agents',
name: 'org-settings-agents',
component: (): Component => import('~/views/org/settings/OrgAgents.vue'),
props: true,
component: (): Component => import('~/components/layout/RouteWrapper.vue'),
children: [
{
path: '',
name: 'org-settings-agents',
component: (): Component => import('~/views/org/settings/agents/OrgAgents.vue'),
},
{
path: ':agentId',
name: 'org-settings-agent',
component: (): Component => import('~/views/org/settings/agents/OrgAgent.vue'),
},
{
path: 'create',
name: 'org-settings-agent-create',
component: (): Component => import('~/views/org/settings/agents/OrgAgentCreate.vue'),
},
],
},
],
},
@@ -277,7 +292,7 @@ const routes: RouteRecordRaw[] = [
},
{
path: 'create',
name: 'admin-settings-agents-create',
name: 'admin-settings-agent-create',
component: (): Component => import('~/views/admin/agents/AdminAgentCreate.vue'),
},
],
@@ -344,9 +359,24 @@ const routes: RouteRecordRaw[] = [
},
{
path: 'agents',
name: 'user-agents',
component: (): Component => import('~/views/user/UserAgents.vue'),
props: true,
component: (): Component => import('~/components/layout/RouteWrapper.vue'),
children: [
{
path: '',
name: 'user-agents',
component: (): Component => import('~/views/user/agents/UserAgents.vue'),
},
{
path: ':agentId',
name: 'user-agent',
component: (): Component => import('~/views/user/agents/UserAgent.vue'),
},
{
path: 'create',
name: 'user-agent-create',
component: (): Component => import('~/views/user/agents/UserAgentCreate.vue'),
},
],
},
],
},

View File

@@ -1,7 +1,7 @@
<template>
<Settings :title="$t('admin.settings.agents.agents')" :description="$t('admin.settings.agents.desc')">
<template #headerActions>
<Button :text="$t('admin.settings.agents.show')" start-icon="back" @click="$router.back()" />
<Button :text="$t('admin.settings.agents.show')" start-icon="back" :to="{ name: 'admin-settings-agents' }" />
</template>
<AgentForm
@@ -10,26 +10,28 @@
is-editing
:is-saving="isSaving"
@save="saveAgent"
@cancel="$router.back()"
@cancel="$router.replace({ name: 'admin-settings-agents' })"
/>
<div v-else class="flex">
<div v-else class="flex justify-center">
<Icon name="spinner" class="animate-spin" />
</div>
</Settings>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import AgentForm from '~/components/agent/AgentForm.vue';
import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue';
import Settings from '~/components/layout/Settings.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction, useAsyncData } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const notifications = useNotifications();
const { t } = useI18n();
@@ -38,7 +40,17 @@ const route = useRoute();
const agentId = computed(() => Number.parseInt(route.params.agentId.toString(), 10));
const { data: agent, refetch: reloadAgent } = useAsyncData(computed(() => () => apiClient.getAgent(agentId.value)));
const agent = ref<Agent | null>(null);
const { data: dbAgent, refetch: reloadAgent } = useAsyncData(computed(() => () => apiClient.getAgent(agentId.value)));
watch(
dbAgent,
(newAgent) => {
agent.value = newAgent;
},
{ immediate: true },
);
const { doSubmit: saveAgent, isLoading: isSaving } = useAsyncAction(async () => {
if (!agent.value) {

View File

@@ -4,7 +4,12 @@
<Button :text="$t('admin.settings.agents.show')" start-icon="back" :to="{ name: 'admin-settings-agents' }" />
</template>
<AgentForm v-model="agent" :is-saving="isSaving" @save="createAgent" @cancel="$router.back()" />
<AgentForm
v-model="agent"
:is-saving="isSaving"
@save="createAgent"
@cancel="$router.replace({ name: 'admin-settings-agents' })"
/>
</Settings>
</template>
@@ -44,7 +49,7 @@ const { doSubmit: createAgent, isLoading: isSaving } = useAsyncAction(async () =
type: 'success',
});
await router.push({ name: 'admin-settings-agent', params: { agentId: createdAgent.id } });
await router.replace({ name: 'admin-settings-agent', params: { agentId: createdAgent.id } });
});
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('admin.settings.settings'), t('create')]));

View File

@@ -1,11 +1,7 @@
<template>
<Settings :title="$t('admin.settings.agents.agents')" :description="$t('admin.settings.agents.desc')">
<template #headerActions>
<Button
:text="$t('admin.settings.agents.add')"
start-icon="plus"
@click="$router.push({ name: 'admin-settings-agent-create' })"
/>
<Button :text="$t('admin.settings.agents.add')" start-icon="plus" :to="{ name: 'admin-settings-agent-create' }" />
</template>
<AgentList
@@ -51,7 +47,7 @@ const { doSubmit: deleteAgent, isLoading: isDeleting } = useAsyncAction(async (a
await resetPage();
});
useInterval(resetPage, 5 * 1000); // refresh every 5s
useInterval(resetPage, 5 * 1000, { immediate: false });
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('admin.settings.settings')]));
</script>

View File

@@ -1,31 +0,0 @@
<template>
<AgentManager
:description="$t('org.settings.agents.desc')"
:load-agents="loadAgents"
:create-agent="createAgent"
:update-agent="updateAgent"
:delete-agent="deleteAgent"
/>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import AgentManager from '~/components/agent/AgentManager.vue';
import useApiClient from '~/compositions/useApiClient';
import { requiredInject } from '~/compositions/useInjectProvide';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const apiClient = useApiClient();
const org = requiredInject('org');
const loadAgents = (page: number) => apiClient.getOrgAgents(org.value.id, { page });
const createAgent = (agent: Partial<Agent>) => apiClient.createOrgAgent(org.value.id, agent);
const updateAgent = (agent: Agent) => apiClient.updateOrgAgent(org.value.id, agent.id, agent);
const deleteAgent = (agent: Agent) => apiClient.deleteOrgAgent(org.value.id, agent.id);
const { t } = useI18n();
useWPTitle(computed(() => [t('admin.settings.agents.agents'), org.value.name]));
</script>

View File

@@ -0,0 +1,74 @@
<template>
<Settings :title="$t('admin.settings.agents.agents')" :description="$t('admin.settings.agents.desc')">
<template #headerActions>
<Button :text="$t('admin.settings.agents.show')" start-icon="back" :to="{ name: 'org-settings-agents' }" />
</template>
<AgentForm
v-if="agent"
v-model="agent"
is-editing
:is-saving="isSaving"
@save="saveAgent"
@cancel="$router.push({ name: 'org-settings-agents' })"
/>
<div v-else class="flex justify-center">
<Icon name="spinner" class="animate-spin" />
</div>
</Settings>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import AgentForm from '~/components/agent/AgentForm.vue';
import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue';
import Settings from '~/components/layout/Settings.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction, useAsyncData } from '~/compositions/useAsyncAction';
import { requiredInject } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const notifications = useNotifications();
const { t } = useI18n();
const apiClient = useApiClient();
const route = useRoute();
const org = requiredInject('org');
const agentId = computed(() => Number.parseInt(route.params.agentId.toString(), 10));
const agent = ref<Agent | null>(null);
const { data: dbAgent, refetch: reloadAgent } = useAsyncData(computed(() => () => apiClient.getAgent(agentId.value)));
watch(
dbAgent,
(newAgent) => {
agent.value = newAgent;
},
{ immediate: true },
);
const { doSubmit: saveAgent, isLoading: isSaving } = useAsyncAction(async () => {
if (!agent.value) {
throw new Error("Unexpected: Can't get agent");
}
await apiClient.updateOrgAgent(org.value.id, agent.value);
notifications.notify({
title: t('admin.settings.agents.saved'),
type: 'success',
});
await reloadAgent();
});
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('admin.settings.settings'), agent.value?.name ?? '']));
</script>

View File

@@ -0,0 +1,59 @@
<template>
<Settings :title="$t('admin.settings.agents.agents')" :description="$t('admin.settings.agents.desc')">
<template #headerActions>
<Button :text="$t('admin.settings.agents.show')" start-icon="back" :to="{ name: 'org-settings-agents' }" />
</template>
<AgentForm
v-model="agent"
:is-saving="isSaving"
@save="createAgent"
@cancel="$router.replace({ name: 'org-settings-agents' })"
/>
</Settings>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import AgentForm from '~/components/agent/AgentForm.vue';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import { requiredInject } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const notifications = useNotifications();
const { t } = useI18n();
const apiClient = useApiClient();
const router = useRouter();
const agent = ref<Partial<Agent>>({
name: '',
no_schedule: false,
});
const org = requiredInject('org');
const { doSubmit: createAgent, isLoading: isSaving } = useAsyncAction(async () => {
if (!agent.value) {
throw new Error("Unexpected: Can't get agent");
}
const createdAgent = await apiClient.createOrgAgent(org.value.id, agent.value);
notifications.notify({
title: t('admin.settings.agents.created'),
type: 'success',
});
await router.replace({ name: 'org-settings-agent', params: { agentId: createdAgent.id } });
});
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('admin.settings.settings'), t('create')]));
</script>

View File

@@ -0,0 +1,60 @@
<template>
<Settings :title="$t('admin.settings.agents.agents')" :description="$t('admin.settings.agents.desc')">
<template #headerActions>
<Button :text="$t('admin.settings.agents.add')" start-icon="plus" :to="{ name: 'org-settings-agent-create' }" />
</template>
<AgentList
:loading="loading"
:agents="agents"
:is-deleting="isDeleting"
is-admin
@edit="$router.push({ name: 'org-settings-agent', params: { agentId: $event.id } })"
@delete="deleteAgent"
/>
</Settings>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import AgentList from '~/components/agent/AgentList.vue';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import { requiredInject } from '~/compositions/useInjectProvide';
import { useInterval } from '~/compositions/useInterval';
import useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const notifications = useNotifications();
const { t } = useI18n();
const apiClient = useApiClient();
const org = requiredInject('org');
const {
resetPage,
data: agents,
loading,
} = usePagination((page: number) => apiClient.getOrgAgents(org.value.id, { page }));
const { doSubmit: deleteAgent, isLoading: isDeleting } = useAsyncAction(async (agent: Agent) => {
// eslint-disable-next-line no-alert
if (!confirm(t('admin.settings.agents.delete_confirm'))) {
return;
}
await apiClient.deleteOrgAgent(org.value.id, agent);
notifications.notify({ title: t('admin.settings.agents.deleted'), type: 'success' });
await resetPage();
});
useInterval(resetPage, 5 * 1000, { immediate: false });
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('admin.settings.settings')]));
</script>

View File

@@ -1,35 +0,0 @@
<template>
<AgentManager
:description="$t('user.settings.agents.desc')"
:load-agents="loadAgents"
:create-agent="createAgent"
:update-agent="updateAgent"
:delete-agent="deleteAgent"
/>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import AgentManager from '~/components/agent/AgentManager.vue';
import useApiClient from '~/compositions/useApiClient';
import useAuthentication from '~/compositions/useAuthentication';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const apiClient = useApiClient();
const { user } = useAuthentication();
if (!user) {
throw new Error('Unexpected: User should be authenticated');
}
const loadAgents = (page: number) => apiClient.getOrgAgents(user.org_id, { page });
const createAgent = (agent: Partial<Agent>) => apiClient.createOrgAgent(user.org_id, agent);
const updateAgent = (agent: Agent) => apiClient.updateOrgAgent(user.org_id, agent.id, agent);
const deleteAgent = (agent: Agent) => apiClient.deleteOrgAgent(user.org_id, agent.id);
const { t } = useI18n();
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('user.settings.settings')]));
</script>

View File

@@ -0,0 +1,77 @@
<template>
<Settings :title="$t('admin.settings.agents.agents')" :description="$t('admin.settings.agents.desc')">
<template #headerActions>
<Button :text="$t('admin.settings.agents.show')" start-icon="back" :to="{ name: 'user-settings-agents' }" />
</template>
<AgentForm
v-if="agent"
v-model="agent"
is-editing
:is-saving="isSaving"
@save="saveAgent"
@cancel="$router.replace({ name: 'user-settings-agents' })"
/>
<div v-else class="flex justify-center">
<Icon name="spinner" class="animate-spin" />
</div>
</Settings>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import AgentForm from '~/components/agent/AgentForm.vue';
import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue';
import Settings from '~/components/layout/Settings.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction, useAsyncData } from '~/compositions/useAsyncAction';
import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const notifications = useNotifications();
const { t } = useI18n();
const apiClient = useApiClient();
const route = useRoute();
const { user } = useAuthentication();
if (!user) {
throw new Error('Unexpected: User should be authenticated');
}
const agentId = computed(() => Number.parseInt(route.params.agentId.toString(), 10));
const agent = ref<Agent | null>(null);
const { data: dbAgent, refetch: reloadAgent } = useAsyncData(computed(() => () => apiClient.getAgent(agentId.value)));
watch(
dbAgent,
(newAgent) => {
agent.value = newAgent;
},
{ immediate: true },
);
const { doSubmit: saveAgent, isLoading: isSaving } = useAsyncAction(async () => {
if (!agent.value) {
throw new Error("Unexpected: Can't get agent");
}
await apiClient.updateOrgAgent(user.org_id, agent.value);
notifications.notify({
title: t('admin.settings.agents.saved'),
type: 'success',
});
await reloadAgent();
});
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('admin.settings.settings'), agent.value?.name ?? '']));
</script>

View File

@@ -0,0 +1,62 @@
<template>
<Settings :title="$t('admin.settings.agents.agents')" :description="$t('admin.settings.agents.desc')">
<template #headerActions>
<Button :text="$t('admin.settings.agents.show')" start-icon="back" :to="{ name: 'user-settings-agents' }" />
</template>
<AgentForm
v-model="agent"
:is-saving="isSaving"
@save="createAgent"
@cancel="$router.replace({ name: 'user-settings-agents' })"
/>
</Settings>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import AgentForm from '~/components/agent/AgentForm.vue';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const notifications = useNotifications();
const { t } = useI18n();
const apiClient = useApiClient();
const router = useRouter();
const { user } = useAuthentication();
if (!user) {
throw new Error('Unexpected: User should be authenticated');
}
const agent = ref<Partial<Agent>>({
name: '',
no_schedule: false,
});
const { doSubmit: createAgent, isLoading: isSaving } = useAsyncAction(async () => {
if (!agent.value) {
throw new Error("Unexpected: Can't get agent");
}
const createdAgent = await apiClient.createOrgAgent(user.org_id, agent.value);
notifications.notify({
title: t('admin.settings.agents.created'),
type: 'success',
});
await router.push({ name: 'user-settings-agent', params: { agentId: createdAgent.id } });
});
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('admin.settings.settings'), t('create')]));
</script>

View File

@@ -0,0 +1,63 @@
<template>
<Settings :title="$t('admin.settings.agents.agents')" :description="$t('admin.settings.agents.desc')">
<template #headerActions>
<Button :text="$t('admin.settings.agents.add')" start-icon="plus" :to="{ name: 'user-settings-agent-create' }" />
</template>
<AgentList
:loading="loading"
:agents="agents"
:is-deleting="isDeleting"
is-admin
@edit="$router.push({ name: 'org-settings-agent', params: { agentId: $event.id } })"
@delete="deleteAgent"
/>
</Settings>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import AgentList from '~/components/agent/AgentList.vue';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useAuthentication from '~/compositions/useAuthentication';
import { useInterval } from '~/compositions/useInterval';
import useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import { useWPTitle } from '~/compositions/useWPTitle';
import type { Agent } from '~/lib/api/types';
const notifications = useNotifications();
const { t } = useI18n();
const apiClient = useApiClient();
const { user } = useAuthentication();
if (!user) {
throw new Error('Unexpected: User should be authenticated');
}
const {
resetPage,
data: agents,
loading,
} = usePagination((page: number) => apiClient.getOrgAgents(user.org_id, { page }));
const { doSubmit: deleteAgent, isLoading: isDeleting } = useAsyncAction(async (agent: Agent) => {
// eslint-disable-next-line no-alert
if (!confirm(t('admin.settings.agents.delete_confirm'))) {
return;
}
await apiClient.deleteOrgAgent(user.org_id, agent);
notifications.notify({ title: t('admin.settings.agents.deleted'), type: 'success' });
await resetPage();
});
useInterval(resetPage, 5 * 1000, { immediate: false });
useWPTitle(computed(() => [t('admin.settings.agents.agents'), t('admin.settings.settings')]));
</script>