2022-05-23 10:09:09 +00:00
|
|
|
<script lang="ts" setup>
|
|
|
|
import {
|
2024-03-26 10:48:07 +00:00
|
|
|
Dialog,
|
2022-05-23 10:09:09 +00:00
|
|
|
IconAddCircle,
|
|
|
|
IconPlug,
|
2022-10-24 03:18:16 +00:00
|
|
|
IconRefreshLine,
|
2022-06-14 07:56:55 +00:00
|
|
|
VButton,
|
|
|
|
VCard,
|
2024-03-26 10:48:07 +00:00
|
|
|
VDropdown,
|
|
|
|
VDropdownItem,
|
2022-08-22 08:49:07 +00:00
|
|
|
VEmpty,
|
2024-03-26 10:48:07 +00:00
|
|
|
VLoading,
|
2022-06-14 07:56:55 +00:00
|
|
|
VPageHeader,
|
|
|
|
VSpace,
|
|
|
|
} from "@halo-dev/components";
|
2022-07-27 08:36:04 +00:00
|
|
|
import PluginListItem from "./components/PluginListItem.vue";
|
2023-08-25 15:28:12 +00:00
|
|
|
import PluginInstallationModal from "./components/PluginInstallationModal.vue";
|
2024-03-26 10:48:07 +00:00
|
|
|
import type { Ref } from "vue";
|
|
|
|
import { computed, onMounted, provide, ref, watch } from "vue";
|
2022-09-22 08:46:32 +00:00
|
|
|
import { apiClient } from "@/utils/api-client";
|
2022-10-10 07:29:05 +00:00
|
|
|
import { usePermission } from "@/utils/permission";
|
2023-02-22 08:00:12 +00:00
|
|
|
import { useQuery } from "@tanstack/vue-query";
|
|
|
|
import type { Plugin } from "@halo-dev/api-client";
|
2023-03-23 08:54:33 +00:00
|
|
|
import { useI18n } from "vue-i18n";
|
2023-05-26 14:54:16 +00:00
|
|
|
import { useRouteQuery } from "@vueuse/router";
|
2023-08-25 16:00:13 +00:00
|
|
|
import { usePluginBatchOperations } from "./composables/use-plugin";
|
2023-03-23 08:54:33 +00:00
|
|
|
|
|
|
|
const { t } = useI18n();
|
2023-02-22 08:00:12 +00:00
|
|
|
const { currentUserHasPermission } = usePermission();
|
|
|
|
|
2024-03-26 10:48:07 +00:00
|
|
|
const pluginInstallationModalVisible = ref(false);
|
2023-02-22 08:00:12 +00:00
|
|
|
|
|
|
|
const keyword = ref("");
|
|
|
|
|
2023-07-07 08:23:06 +00:00
|
|
|
const selectedEnabledValue = ref();
|
|
|
|
const selectedSortValue = ref();
|
2022-11-23 04:59:28 +00:00
|
|
|
|
|
|
|
const hasFilters = computed(() => {
|
2023-07-07 08:23:06 +00:00
|
|
|
return selectedEnabledValue.value !== undefined || selectedSortValue.value;
|
2022-11-23 04:59:28 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
function handleClearFilters() {
|
2023-07-07 08:23:06 +00:00
|
|
|
selectedSortValue.value = undefined;
|
|
|
|
selectedEnabledValue.value = undefined;
|
2022-09-30 03:12:18 +00:00
|
|
|
}
|
2023-02-22 08:00:12 +00:00
|
|
|
|
2023-08-28 03:50:13 +00:00
|
|
|
const total = ref(0);
|
|
|
|
|
2023-02-22 08:00:12 +00:00
|
|
|
const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
2023-08-25 15:42:14 +00:00
|
|
|
queryKey: ["plugins", keyword, selectedEnabledValue, selectedSortValue],
|
2023-02-22 08:00:12 +00:00
|
|
|
queryFn: async () => {
|
|
|
|
const { data } = await apiClient.plugin.listPlugins({
|
2023-08-25 15:42:14 +00:00
|
|
|
page: 0,
|
|
|
|
size: 0,
|
2023-02-22 08:00:12 +00:00
|
|
|
keyword: keyword.value,
|
2023-07-07 08:23:06 +00:00
|
|
|
enabled: selectedEnabledValue.value,
|
|
|
|
sort: [selectedSortValue.value].filter(Boolean) as string[],
|
2023-02-22 08:00:12 +00:00
|
|
|
});
|
2023-08-28 03:50:13 +00:00
|
|
|
|
|
|
|
total.value = data.total;
|
|
|
|
|
2023-02-22 08:00:12 +00:00
|
|
|
return data.items;
|
|
|
|
},
|
|
|
|
keepPreviousData: true,
|
|
|
|
refetchInterval: (data) => {
|
|
|
|
const deletingPlugins = data?.filter(
|
|
|
|
(plugin) => !!plugin.metadata.deletionTimestamp
|
|
|
|
);
|
|
|
|
|
2023-07-19 03:44:12 +00:00
|
|
|
return deletingPlugins?.length ? 2000 : false;
|
2023-02-22 08:00:12 +00:00
|
|
|
},
|
|
|
|
});
|
2023-05-26 14:54:16 +00:00
|
|
|
|
2023-08-25 16:00:13 +00:00
|
|
|
// selection
|
|
|
|
const selectedNames = ref<string[]>([]);
|
|
|
|
provide<Ref<string[]>>("selectedNames", selectedNames);
|
|
|
|
const checkedAll = ref(false);
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => selectedNames.value,
|
|
|
|
(value) => {
|
|
|
|
checkedAll.value = value.length === data.value?.length;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const handleCheckAllChange = (e: Event) => {
|
|
|
|
const { checked } = e.target as HTMLInputElement;
|
|
|
|
if (checked) {
|
|
|
|
selectedNames.value =
|
|
|
|
data.value?.map((plugin) => {
|
|
|
|
return plugin.metadata.name;
|
|
|
|
}) || [];
|
|
|
|
} else {
|
|
|
|
selectedNames.value.length = 0;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const { handleChangeStatusInBatch, handleUninstallInBatch } =
|
|
|
|
usePluginBatchOperations(selectedNames);
|
|
|
|
|
2023-05-26 14:54:16 +00:00
|
|
|
// handle remote download url from route
|
|
|
|
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
|
|
|
|
"remote-download-url"
|
|
|
|
);
|
|
|
|
onMounted(() => {
|
|
|
|
if (routeRemoteDownloadUrl.value) {
|
|
|
|
Dialog.warning({
|
|
|
|
title: t("core.plugin.operations.remote_download.title"),
|
|
|
|
description: t("core.plugin.operations.remote_download.description", {
|
|
|
|
url: routeRemoteDownloadUrl.value,
|
|
|
|
}),
|
|
|
|
confirmText: t("core.common.buttons.download"),
|
|
|
|
cancelText: t("core.common.buttons.cancel"),
|
|
|
|
onConfirm() {
|
2024-03-26 10:48:07 +00:00
|
|
|
pluginInstallationModalVisible.value = true;
|
2023-05-26 14:54:16 +00:00
|
|
|
},
|
|
|
|
onCancel() {
|
|
|
|
routeRemoteDownloadUrl.value = null;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2022-05-23 10:09:09 +00:00
|
|
|
</script>
|
|
|
|
<template>
|
2023-08-25 15:28:12 +00:00
|
|
|
<PluginInstallationModal
|
2024-03-26 10:48:07 +00:00
|
|
|
v-if="
|
|
|
|
pluginInstallationModalVisible &&
|
|
|
|
currentUserHasPermission(['system:plugins:manage'])
|
|
|
|
"
|
|
|
|
@close="pluginInstallationModalVisible = false"
|
2022-07-23 12:42:13 +00:00
|
|
|
/>
|
|
|
|
|
2023-03-23 08:54:33 +00:00
|
|
|
<VPageHeader :title="$t('core.plugin.title')">
|
2022-05-23 10:09:09 +00:00
|
|
|
<template #icon>
|
2022-05-29 15:55:06 +00:00
|
|
|
<IconPlug class="mr-2 self-center" />
|
2022-05-23 10:09:09 +00:00
|
|
|
</template>
|
|
|
|
<template #actions>
|
2022-07-23 12:42:13 +00:00
|
|
|
<VButton
|
|
|
|
v-permission="['system:plugins:manage']"
|
|
|
|
type="secondary"
|
2024-03-26 10:48:07 +00:00
|
|
|
@click="pluginInstallationModalVisible = true"
|
2022-07-23 12:42:13 +00:00
|
|
|
>
|
2022-05-23 10:09:09 +00:00
|
|
|
<template #icon>
|
2022-05-29 15:55:06 +00:00
|
|
|
<IconAddCircle class="h-full w-full" />
|
2022-05-23 10:09:09 +00:00
|
|
|
</template>
|
2023-03-23 08:54:33 +00:00
|
|
|
{{ $t("core.common.buttons.install") }}
|
2022-05-23 10:09:09 +00:00
|
|
|
</VButton>
|
|
|
|
</template>
|
|
|
|
</VPageHeader>
|
|
|
|
|
|
|
|
<div class="m-0 md:m-4">
|
|
|
|
<VCard :body-class="['!p-0']">
|
|
|
|
<template #header>
|
2022-05-29 15:55:06 +00:00
|
|
|
<div class="block w-full bg-gray-50 px-4 py-3">
|
2022-05-23 10:09:09 +00:00
|
|
|
<div
|
2023-09-14 16:14:14 +00:00
|
|
|
class="relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center"
|
2022-05-23 10:09:09 +00:00
|
|
|
>
|
2023-08-25 16:00:13 +00:00
|
|
|
<div
|
|
|
|
v-permission="['system:posts:manage']"
|
2023-09-14 16:14:14 +00:00
|
|
|
class="hidden items-center sm:flex"
|
2023-08-25 16:00:13 +00:00
|
|
|
>
|
|
|
|
<input
|
|
|
|
v-model="checkedAll"
|
|
|
|
type="checkbox"
|
|
|
|
@change="handleCheckAllChange"
|
|
|
|
/>
|
|
|
|
</div>
|
2022-09-30 03:12:18 +00:00
|
|
|
<div class="flex w-full flex-1 items-center gap-2 sm:w-auto">
|
2023-08-25 16:00:13 +00:00
|
|
|
<SearchInput v-if="!selectedNames.length" v-model="keyword" />
|
|
|
|
<VSpace v-else>
|
|
|
|
<VButton @click="handleChangeStatusInBatch(true)">
|
|
|
|
{{ $t("core.common.buttons.activate") }}
|
|
|
|
</VButton>
|
|
|
|
<VButton @click="handleChangeStatusInBatch(false)">
|
|
|
|
{{ $t("core.common.buttons.inactivate") }}
|
|
|
|
</VButton>
|
|
|
|
<VDropdown>
|
|
|
|
<VButton type="danger">
|
|
|
|
{{ $t("core.common.buttons.uninstall") }}
|
|
|
|
</VButton>
|
|
|
|
<template #popper>
|
|
|
|
<VDropdownItem
|
|
|
|
type="danger"
|
|
|
|
@click="handleUninstallInBatch(false)"
|
|
|
|
>
|
|
|
|
{{ $t("core.common.buttons.uninstall") }}
|
|
|
|
</VDropdownItem>
|
|
|
|
<VDropdownItem
|
|
|
|
type="danger"
|
|
|
|
@click="handleUninstallInBatch(true)"
|
|
|
|
>
|
|
|
|
{{
|
|
|
|
$t(
|
|
|
|
"core.plugin.operations.uninstall_and_delete_config.button"
|
|
|
|
)
|
|
|
|
}}
|
|
|
|
</VDropdownItem>
|
|
|
|
</template>
|
|
|
|
</VDropdown>
|
|
|
|
</VSpace>
|
2022-05-23 10:09:09 +00:00
|
|
|
</div>
|
2023-09-14 16:14:14 +00:00
|
|
|
<VSpace spacing="lg" class="flex-wrap">
|
|
|
|
<FilterCleanButton
|
|
|
|
v-if="hasFilters"
|
|
|
|
@click="handleClearFilters"
|
|
|
|
/>
|
|
|
|
<FilterDropdown
|
|
|
|
v-model="selectedEnabledValue"
|
|
|
|
:label="$t('core.common.filters.labels.status')"
|
|
|
|
:items="[
|
|
|
|
{
|
|
|
|
label: t('core.common.filters.item_labels.all'),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: t('core.plugin.filters.status.items.active'),
|
|
|
|
value: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: t('core.plugin.filters.status.items.inactive'),
|
|
|
|
value: false,
|
|
|
|
},
|
|
|
|
]"
|
|
|
|
/>
|
|
|
|
<FilterDropdown
|
|
|
|
v-model="selectedSortValue"
|
|
|
|
:label="$t('core.common.filters.labels.sort')"
|
|
|
|
:items="[
|
|
|
|
{
|
|
|
|
label: t('core.common.filters.item_labels.default'),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: t('core.plugin.filters.sort.items.create_time_desc'),
|
|
|
|
value: 'creationTimestamp,desc',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: t('core.plugin.filters.sort.items.create_time_asc'),
|
|
|
|
value: 'creationTimestamp,asc',
|
|
|
|
},
|
|
|
|
]"
|
|
|
|
/>
|
|
|
|
<div class="flex flex-row gap-2">
|
|
|
|
<div
|
|
|
|
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
|
|
|
@click="refetch()"
|
|
|
|
>
|
|
|
|
<IconRefreshLine
|
|
|
|
v-tooltip="$t('core.common.buttons.refresh')"
|
|
|
|
:class="{ 'animate-spin text-gray-900': isFetching }"
|
|
|
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
|
|
/>
|
2022-10-24 03:18:16 +00:00
|
|
|
</div>
|
2023-09-14 16:14:14 +00:00
|
|
|
</div>
|
|
|
|
</VSpace>
|
2022-05-23 10:09:09 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
2022-08-22 08:49:07 +00:00
|
|
|
|
2023-02-22 08:00:12 +00:00
|
|
|
<VLoading v-if="isLoading" />
|
2022-11-24 03:46:10 +00:00
|
|
|
|
2023-02-22 08:00:12 +00:00
|
|
|
<Transition v-else-if="!data?.length" appear name="fade">
|
2022-11-24 03:46:10 +00:00
|
|
|
<VEmpty
|
2023-03-23 08:54:33 +00:00
|
|
|
:message="$t('core.plugin.empty.message')"
|
|
|
|
:title="$t('core.plugin.empty.title')"
|
2022-11-24 03:46:10 +00:00
|
|
|
>
|
|
|
|
<template #actions>
|
|
|
|
<VSpace>
|
2023-03-23 08:54:33 +00:00
|
|
|
<VButton :loading="isFetching" @click="refetch()">
|
|
|
|
{{ $t("core.common.buttons.refresh") }}
|
|
|
|
</VButton>
|
2022-11-24 03:46:10 +00:00
|
|
|
<VButton
|
|
|
|
v-permission="['system:plugins:manage']"
|
|
|
|
type="secondary"
|
2024-03-26 10:48:07 +00:00
|
|
|
@click="pluginInstallationModalVisible = true"
|
2022-11-24 03:46:10 +00:00
|
|
|
>
|
|
|
|
<template #icon>
|
|
|
|
<IconAddCircle class="h-full w-full" />
|
|
|
|
</template>
|
2023-03-23 08:54:33 +00:00
|
|
|
{{ $t("core.plugin.empty.actions.install") }}
|
2022-11-24 03:46:10 +00:00
|
|
|
</VButton>
|
|
|
|
</VSpace>
|
|
|
|
</template>
|
|
|
|
</VEmpty>
|
|
|
|
</Transition>
|
|
|
|
|
|
|
|
<Transition v-else appear name="fade">
|
|
|
|
<ul
|
|
|
|
class="box-border h-full w-full divide-y divide-gray-100"
|
|
|
|
role="list"
|
|
|
|
>
|
2023-05-03 15:56:33 +00:00
|
|
|
<li v-for="plugin in data" :key="plugin.metadata.name">
|
2023-08-22 08:12:12 +00:00
|
|
|
<PluginListItem
|
|
|
|
:plugin="plugin"
|
2023-08-25 16:00:13 +00:00
|
|
|
:is-selected="selectedNames.includes(plugin.metadata.name)"
|
2023-08-22 08:12:12 +00:00
|
|
|
/>
|
2022-11-24 03:46:10 +00:00
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</Transition>
|
2023-08-28 03:50:13 +00:00
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
<div class="flex h-8 items-center">
|
|
|
|
<span class="text-sm text-gray-500">
|
|
|
|
{{ $t("core.components.pagination.total_label", { total: total }) }}
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</template>
|
2022-05-23 10:09:09 +00:00
|
|
|
</VCard>
|
|
|
|
</div>
|
|
|
|
</template>
|