refactor: data list filter components (#4182)

#### What type of PR is this?

/area console
/kind improvement
/milestone 2.8.x

#### What this PR does / why we need it:

重构 Console 端数据列表的筛选项 UI,并提供全局的筛选列表组件和搜索输入框组件。

> 在此 PR 同时重构了插件列表和用户列表用于验证,其他页面后续提 PR 修改。

<img width="1657" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/ffa42184-46e6-4cb0-b39f-bd7b18869697">

重构之后可以获得:

1. 更好的代码组织。
2. 更好的使用体验。
3. UI 更加容易适配移动端。

#### Which issue(s) this PR fixes:

Fixes #4181 

#### Special notes for your reviewer:

需要测试:

1. 测试插件管理和用户管理的数据列表筛选和关键词搜索功能是否正常。

#### Does this PR introduce a user-facing change?

```release-note
重构 Console 端数据列表的筛选项 UI,并提供全局的筛选列表组件和搜索输入框组件。
```
pull/4195/head
Ryan Wang 2023-07-07 16:23:06 +08:00 committed by GitHub
parent c0aae3a63c
commit f622b1787c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 262 additions and 335 deletions

View File

@ -0,0 +1,67 @@
<script lang="ts" setup>
import { IconArrowDown, VDropdown, VDropdownItem } from "@halo-dev/components";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
items: {
label: string;
value?: string | boolean | number;
}[];
label: string;
modelValue?: string | boolean | number;
}>(),
{
modelValue: undefined,
}
);
const emit = defineEmits<{
(
event: "update:modelValue",
modelValue: string | boolean | number | undefined
): void;
}>();
const selectedItem = computed(() => {
return props.items.find((item) => item.value === props.modelValue);
});
function handleSelect(item: {
label: string;
value?: string | boolean | number;
}) {
if (item.value === props.modelValue) {
emit("update:modelValue", undefined);
return;
}
emit("update:modelValue", item.value);
}
</script>
<template>
<VDropdown>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
:class="{ 'font-semibold text-gray-700': modelValue !== undefined }"
>
<span v-if="!selectedItem" class="mr-0.5">
{{ label }}
</span>
<span v-else> {{ label }}{{ selectedItem.label }} </span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<VDropdownItem
v-for="(item, index) in items"
:key="index"
:selected="item.value === modelValue"
@click="handleSelect(item)"
>
{{ item.label }}
</VDropdownItem>
</template>
</VDropdown>
</template>

View File

@ -0,0 +1,55 @@
<script lang="ts" setup>
import { getNode, reset } from "@formkit/core";
import { IconCloseCircle } from "@halo-dev/components";
withDefaults(
defineProps<{
placeholder?: string;
modelValue: string;
}>(),
{
placeholder: undefined,
}
);
const emit = defineEmits<{
(event: "update:modelValue", modelValue: string): void;
}>();
const id = `search-input-${crypto.randomUUID()}`;
function handleReset() {
emit("update:modelValue", "");
reset(id);
}
function onKeywordChange() {
const keywordNode = getNode(id);
if (keywordNode) {
emit("update:modelValue", keywordNode._value as string);
}
}
</script>
<template>
<FormKit
:id="id"
outer-class="!p-0"
:placeholder="placeholder || $t('core.common.placeholder.search')"
type="text"
name="keyword"
:model-value="modelValue"
@keyup.enter="onKeywordChange"
>
<template v-if="modelValue" #suffix>
<div
class="group flex h-full cursor-pointer items-center bg-white px-2 transition-all hover:bg-gray-50"
@click="handleReset"
>
<IconCloseCircle
class="h-4 w-4 text-gray-500 group-hover:text-gray-700"
/>
</div>
</template>
</FormKit>
</template>

View File

@ -748,7 +748,6 @@ core:
filters: filters:
status: status:
items: items:
all: All
active: Active active: Active
inactive: Inactive inactive: Inactive
sort: sort:
@ -1190,6 +1189,9 @@ core:
labels: labels:
sort: Sort sort: Sort
status: Status status: Status
item_labels:
all: All
default: Default
status: status:
deleting: Deleting deleting: Deleting
loading: Loading loading: Loading

View File

@ -748,7 +748,6 @@ core:
filters: filters:
status: status:
items: items:
all: 全部
active: 已启用 active: 已启用
inactive: 未启用 inactive: 未启用
sort: sort:
@ -1190,6 +1189,9 @@ core:
labels: labels:
sort: 排序 sort: 排序
status: 状态 status: 状态
item_labels:
all: 全部
default: 默认
status: status:
deleting: 删除中 deleting: 删除中
loading: 加载中 loading: 加载中

View File

@ -748,7 +748,6 @@ core:
filters: filters:
status: status:
items: items:
all: 全部
active: 已啟用 active: 已啟用
inactive: 未啟用 inactive: 未啟用
sort: sort:
@ -1190,6 +1189,9 @@ core:
labels: labels:
sort: 排序 sort: 排序
status: 狀態 status: 狀態
item_labels:
all: 全部
default: 預設
status: status:
deleting: 刪除中 deleting: 刪除中
loading: 加載中 loading: 加載中

View File

@ -44,7 +44,6 @@ import { useRouteQuery } from "@vueuse/router";
import { useFetchAttachmentGroup } from "./composables/use-attachment-group"; import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import FilterTag from "@/components/filter/FilterTag.vue"; import FilterTag from "@/components/filter/FilterTag.vue";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core"; import { getNode } from "@formkit/core";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";

View File

@ -21,7 +21,6 @@ import type { ListedComment, User } from "@halo-dev/api-client";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import FilterTag from "@/components/filter/FilterTag.vue"; import FilterTag from "@/components/filter/FilterTag.vue";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core"; import { getNode } from "@formkit/core";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";

View File

@ -37,7 +37,6 @@ import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import { singlePageLabels } from "@/constants/labels"; import { singlePageLabels } from "@/constants/labels";
import FilterTag from "@/components/filter/FilterTag.vue"; import FilterTag from "@/components/filter/FilterTag.vue";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core"; import { getNode } from "@formkit/core";
import { useMutation, useQuery } from "@tanstack/vue-query"; import { useMutation, useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";

View File

@ -43,7 +43,6 @@ import { formatDatetime } from "@/utils/date";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import { postLabels } from "@/constants/labels"; import { postLabels } from "@/constants/labels";
import FilterTag from "@/components/filter/FilterTag.vue"; import FilterTag from "@/components/filter/FilterTag.vue";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core"; import { getNode } from "@formkit/core";
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue"; import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
import { useMutation, useQuery } from "@tanstack/vue-query"; import { useMutation, useQuery } from "@tanstack/vue-query";

View File

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
IconAddCircle, IconAddCircle,
IconArrowDown,
IconPlug, IconPlug,
IconRefreshLine, IconRefreshLine,
VButton, VButton,
@ -11,8 +10,6 @@ import {
VPagination, VPagination,
VSpace, VSpace,
VLoading, VLoading,
VDropdown,
VDropdownItem,
Dialog, Dialog,
} from "@halo-dev/components"; } from "@halo-dev/components";
import PluginListItem from "./components/PluginListItem.vue"; import PluginListItem from "./components/PluginListItem.vue";
@ -20,26 +17,14 @@ import PluginUploadModal from "./components/PluginUploadModal.vue";
import { computed, ref, onMounted } from "vue"; import { computed, ref, onMounted } from "vue";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import FilterTag from "@/components/filter/FilterTag.vue";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import type { Plugin } from "@halo-dev/api-client"; import type { Plugin } from "@halo-dev/api-client";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { watch } from "vue";
const { t } = useI18n(); const { t } = useI18n();
interface EnabledItem {
label: string;
value?: boolean;
}
interface SortItem {
label: string;
value: string;
}
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const pluginInstall = ref(false); const pluginInstall = ref(false);
@ -49,90 +34,41 @@ const page = ref(1);
const size = ref(20); const size = ref(20);
const total = ref(0); const total = ref(0);
// Filters const selectedEnabledValue = ref();
const EnabledItems: EnabledItem[] = [ const selectedSortValue = ref();
{
label: t("core.plugin.filters.status.items.all"),
value: undefined,
},
{
label: t("core.plugin.filters.status.items.active"),
value: true,
},
{
label: t("core.plugin.filters.status.items.inactive"),
value: false,
},
];
const SortItems: SortItem[] = [
{
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",
},
];
const selectedEnabledItem = ref<EnabledItem>();
const selectedSortItem = ref<SortItem>();
function handleEnabledItemChange(enabledItem: EnabledItem) {
selectedEnabledItem.value = enabledItem;
page.value = 1;
}
function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem;
page.value = 1;
}
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
page.value = 1;
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
const hasFilters = computed(() => { const hasFilters = computed(() => {
return ( return selectedEnabledValue.value !== undefined || selectedSortValue.value;
selectedEnabledItem.value?.value !== undefined ||
selectedSortItem.value?.value ||
keyword.value
);
}); });
function handleClearFilters() { function handleClearFilters() {
selectedEnabledItem.value = undefined; selectedSortValue.value = undefined;
selectedSortItem.value = undefined; selectedEnabledValue.value = undefined;
keyword.value = "";
page.value = 1;
} }
watch(
() => [selectedEnabledValue.value, selectedSortValue.value, keyword.value],
() => {
page.value = 1;
}
);
const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
queryKey: [ queryKey: [
"plugins", "plugins",
page, page,
size, size,
keyword, keyword,
selectedEnabledItem, selectedEnabledValue,
selectedSortItem, selectedSortValue,
], ],
queryFn: async () => { queryFn: async () => {
const { data } = await apiClient.plugin.listPlugins({ const { data } = await apiClient.plugin.listPlugins({
page: page.value, page: page.value,
size: size.value, size: size.value,
keyword: keyword.value, keyword: keyword.value,
enabled: selectedEnabledItem.value?.value, enabled: selectedEnabledValue.value,
sort: [selectedSortItem.value?.value].filter(Boolean) as string[], sort: [selectedSortValue.value].filter(Boolean) as string[],
}); });
total.value = data.total; total.value = data.total;
@ -204,99 +140,52 @@ onMounted(() => {
class="relative flex flex-col items-start sm:flex-row sm:items-center" class="relative flex flex-col items-start sm:flex-row sm:items-center"
> >
<div class="flex w-full flex-1 items-center gap-2 sm:w-auto"> <div class="flex w-full flex-1 items-center gap-2 sm:w-auto">
<FormKit <SearchInput v-model="keyword" />
id="keywordInput" </div>
outer-class="!p-0" <div class="mt-4 flex sm:mt-0">
:placeholder="$t('core.common.placeholder.search')" <VSpace spacing="lg">
type="text"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<FilterTag v-if="keyword" @close="handleClearKeyword()">
{{
$t("core.common.filters.results.keyword", {
keyword: keyword,
})
}}
</FilterTag>
<FilterTag
v-if="selectedEnabledItem?.value !== undefined"
@close="handleEnabledItemChange(EnabledItems[0])"
>
{{
$t("core.common.filters.results.status", {
status: selectedEnabledItem.label,
})
}}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
@close="handleSortItemChange()"
>
{{
$t("core.common.filters.results.sort", {
sort: selectedSortItem.label,
})
}}
</FilterTag>
<FilterCleanButton <FilterCleanButton
v-if="hasFilters" v-if="hasFilters"
@click="handleClearFilters" @click="handleClearFilters"
/> />
</div> <FilterDropdown
<div class="mt-4 flex sm:mt-0"> v-model="selectedEnabledValue"
<VSpace spacing="lg"> :label="$t('core.common.filters.labels.status')"
<VDropdown> :items="[
<div {
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" label: t('core.common.filters.item_labels.all'),
> },
<span class="mr-0.5"> {
{{ $t("core.common.filters.labels.status") }} label: t('core.plugin.filters.status.items.active'),
</span> value: true,
<span> },
<IconArrowDown /> {
</span> label: t('core.plugin.filters.status.items.inactive'),
</div> value: false,
<template #popper> },
<VDropdownItem ]"
v-for="(enabledItem, index) in EnabledItems" />
:key="index" <FilterDropdown
:selected=" v-model="selectedSortValue"
enabledItem.value === selectedEnabledItem?.value :label="$t('core.common.filters.labels.sort')"
" :items="[
@click="handleEnabledItemChange(enabledItem)" {
> label: t('core.common.filters.item_labels.default'),
{{ enabledItem.label }} },
</VDropdownItem> {
</template> label: t(
</VDropdown> 'core.plugin.filters.sort.items.create_time_desc'
<VDropdown> ),
<div value: 'creationTimestamp,desc',
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" },
> {
<span class="mr-0.5"> label: t(
{{ $t("core.common.filters.labels.sort") }} 'core.plugin.filters.sort.items.create_time_asc'
</span> ),
<span> value: 'creationTimestamp,asc',
<IconArrowDown /> },
</span> ]"
</div> />
<template #popper>
<VDropdownItem
v-for="(sortItem, index) in SortItems"
:key="index"
:selected="sortItem.value === selectedSortItem?.value"
@click="handleSortItemChange(sortItem)"
>
{{ sortItem.label }}
</VDropdownItem>
</template>
</VDropdown>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div <div
class="group cursor-pointer rounded p-1 hover:bg-gray-200" class="group cursor-pointer rounded p-1 hover:bg-gray-200"

View File

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
IconAddCircle, IconAddCircle,
IconArrowDown,
IconUserFollow, IconUserFollow,
IconUserSettings, IconUserSettings,
IconLockPasswordLine, IconLockPasswordLine,
@ -20,7 +19,6 @@ import {
Toast, Toast,
IconRefreshLine, IconRefreshLine,
VEmpty, VEmpty,
VDropdown,
VDropdownItem, VDropdownItem,
} from "@halo-dev/components"; } from "@halo-dev/components";
import UserEditingModal from "./components/UserEditingModal.vue"; import UserEditingModal from "./components/UserEditingModal.vue";
@ -28,16 +26,13 @@ import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
import GrantPermissionModal from "./components/GrantPermissionModal.vue"; import GrantPermissionModal from "./components/GrantPermissionModal.vue";
import { computed, onMounted, ref, watch } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import type { Role, User, ListedUser } from "@halo-dev/api-client"; import type { User, ListedUser } from "@halo-dev/api-client";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { getNode } from "@formkit/core";
import FilterTag from "@/components/filter/FilterTag.vue";
import { useFetchRole } from "../roles/composables/use-role"; import { useFetchRole } from "../roles/composables/use-role";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import UserCreationModal from "./components/UserCreationModal.vue"; import UserCreationModal from "./components/UserCreationModal.vue";
@ -61,61 +56,26 @@ const ANONYMOUSUSER_NAME = "anonymousUser";
const DELETEDUSER_NAME = "ghost"; const DELETEDUSER_NAME = "ghost";
// Filters // Filters
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
page.value = 1;
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
interface SortItem {
label: string;
value: string;
}
const SortItems: SortItem[] = [
{
label: t("core.user.filters.sort.items.create_time_desc"),
value: "creationTimestamp,desc",
},
{
label: t("core.user.filters.sort.items.create_time_asc"),
value: "creationTimestamp,asc",
},
];
const selectedSortItem = ref<SortItem>();
function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem;
page.value = 1;
}
const { roles } = useFetchRole(); const { roles } = useFetchRole();
const selectedRole = ref<Role>(); const selectedRoleValue = ref();
const selectedSortValue = ref();
function handleRoleChange(role?: Role) {
selectedRole.value = role;
page.value = 1;
}
function handleClearFilters() { function handleClearFilters() {
selectedRole.value = undefined; selectedRoleValue.value = undefined;
selectedSortItem.value = undefined; selectedSortValue.value = undefined;
keyword.value = "";
page.value = 1;
} }
const hasFilters = computed(() => { const hasFilters = computed(() => {
return selectedRole.value || selectedSortItem.value || keyword.value; return selectedRoleValue.value || selectedSortValue.value;
}); });
watch(
() => [selectedRoleValue.value, selectedSortValue.value, keyword.value],
() => {
page.value = 1;
}
);
const page = ref(1); const page = ref(1);
const size = ref(20); const size = ref(20);
const total = ref(0); const total = ref(0);
@ -126,7 +86,14 @@ const {
isFetching, isFetching,
refetch, refetch,
} = useQuery<ListedUser[]>({ } = useQuery<ListedUser[]>({
queryKey: ["users", page, size, keyword, selectedSortItem, selectedRole], queryKey: [
"users",
page,
size,
keyword,
selectedSortValue,
selectedRoleValue,
],
queryFn: async () => { queryFn: async () => {
const { data } = await apiClient.user.listUsers({ const { data } = await apiClient.user.listUsers({
page: page.value, page: page.value,
@ -136,10 +103,8 @@ const {
`name!=${ANONYMOUSUSER_NAME}`, `name!=${ANONYMOUSUSER_NAME}`,
`name!=${DELETEDUSER_NAME}`, `name!=${DELETEDUSER_NAME}`,
], ],
sort: [selectedSortItem.value?.value].filter( sort: [selectedSortValue.value].filter(Boolean) as string[],
(item) => !!item role: selectedRoleValue.value,
) as string[],
role: selectedRole.value?.metadata.name,
}); });
total.value = data.total; total.value = data.total;
@ -330,55 +295,7 @@ onMounted(() => {
/> />
</div> </div>
<div class="flex w-full flex-1 items-center sm:w-auto"> <div class="flex w-full flex-1 items-center sm:w-auto">
<div <SearchInput v-if="!selectedUserNames.length" v-model="keyword" />
v-if="!selectedUserNames.length"
class="flex items-center gap-2"
>
<FormKit
id="keywordInput"
outer-class="!p-0"
:model-value="keyword"
name="keyword"
:placeholder="$t('core.common.placeholder.search')"
type="text"
@keyup.enter="handleKeywordChange"
></FormKit>
<FilterTag v-if="keyword" @close="handleClearKeyword()">
{{
$t("core.common.filters.results.keyword", {
keyword: keyword,
})
}}
</FilterTag>
<FilterTag v-if="selectedRole" @close="handleRoleChange()">
{{
$t("core.user.filters.role.result", {
role:
selectedRole.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || selectedRole.metadata.name,
})
}}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
@close="handleSortItemChange()"
>
{{
$t("core.common.filters.results.sort", {
sort: selectedSortItem.label,
})
}}
</FilterTag>
<FilterCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
</div>
<VSpace v-else> <VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch"> <VButton type="danger" @click="handleDeleteInBatch">
{{ $t("core.common.buttons.delete") }} {{ $t("core.common.buttons.delete") }}
@ -387,56 +304,45 @@ onMounted(() => {
</div> </div>
<div class="mt-4 flex sm:mt-0"> <div class="mt-4 flex sm:mt-0">
<VSpace spacing="lg"> <VSpace spacing="lg">
<VDropdown> <FilterCleanButton
<div v-if="hasFilters"
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" @click="handleClearFilters"
> />
<span class="mr-0.5"> <FilterDropdown
{{ $t("core.user.filters.role.label") }} v-model="selectedRoleValue"
</span> :label="$t('core.user.filters.role.label')"
<span> :items="[
<IconArrowDown /> {
</span> label: t('core.common.filters.item_labels.all'),
</div> },
<template #popper> ...roles.map((role) => {
<VDropdownItem return {
v-for="(role, index) in roles" label:
:key="index"
:selected="
selectedRole?.metadata.name === role.metadata.name
"
@click="handleRoleChange(role)"
>
{{
role.metadata.annotations?.[ role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME rbacAnnotations.DISPLAY_NAME
] || role.metadata.name ] || role.metadata.name,
}} value: role.metadata.name,
</VDropdownItem> };
</template> }),
</VDropdown> ]"
<VDropdown> />
<div <FilterDropdown
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" v-model="selectedSortValue"
> :label="$t('core.common.filters.labels.sort')"
<span class="mr-0.5"> :items="[
{{ $t("core.common.filters.labels.sort") }} {
</span> label: t('core.common.filters.item_labels.default'),
<span> },
<IconArrowDown /> {
</span> label: t('core.user.filters.sort.items.create_time_desc'),
</div> value: 'creationTimestamp,desc',
<template #popper> },
<VDropdownItem {
v-for="(sortItem, index) in SortItems" label: t('core.user.filters.sort.items.create_time_asc'),
:key="index" value: 'creationTimestamp,asc',
:selected="selectedSortItem?.value === sortItem.value" },
@click="handleSortItemChange(sortItem)" ]"
> />
{{ sortItem.label }}
</VDropdownItem>
</template>
</VDropdown>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div <div
class="group cursor-pointer rounded p-1 hover:bg-gray-200" class="group cursor-pointer rounded p-1 hover:bg-gray-200"

View File

@ -6,6 +6,9 @@ import "floating-vue/dist/style.css";
import VueGridLayout from "vue-grid-layout"; import VueGridLayout from "vue-grid-layout";
import { defaultConfig, plugin as FormKit } from "@formkit/vue"; import { defaultConfig, plugin as FormKit } from "@formkit/vue";
import FormKitConfig from "@/formkit/formkit.config"; import FormKitConfig from "@/formkit/formkit.config";
import FilterDropdown from "@/components/filter/FilterDropdown.vue";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import SearchInput from "@/components/input/SearchInput.vue";
export function setupComponents(app: App) { export function setupComponents(app: App) {
app.use(VueGridLayout); app.use(VueGridLayout);
@ -25,4 +28,9 @@ export function setupComponents(app: App) {
"VCodemirror", "VCodemirror",
defineAsyncComponent(() => import("@/components/codemirror/Codemirror.vue")) defineAsyncComponent(() => import("@/components/codemirror/Codemirror.vue"))
); );
// Console components
app.component("FilterDropdown", FilterDropdown);
app.component("FilterCleanButton", FilterCleanButton);
app.component("SearchInput", SearchInput);
} }