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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:total-count="announcementCount"
:loading
:items-per-page-options="[10, 20, 50, 100]"
table-name="adminAnnouncements"
data-test="announcement-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/admin/src/components/Device/DeviceList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:loading
:total-count="devicesCount"
:items-per-page-options="[10, 20, 50, 100]"
table-name="adminDevices"
data-test="devices-list"
@update:sort="sortByItem"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:loading
:total-count="firewallRulesCount"
:items-per-page-options="[10, 20, 50, 100]"
table-name="adminFirewallRules"
data-test="firewall-rules-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/admin/src/components/Namespace/NamespaceList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:loading="loading"
:total-count="namespaceCount"
:items-per-page-options="[10, 20, 50, 100]"
table-name="adminNamespaces"
data-test="namespaces-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/admin/src/components/Sessions/SessionList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:items-per-page-options="[10, 20, 50, 100]"
:loading
:total-count="sessionCount"
table-name="adminSessions"
data-test="session-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/admin/src/components/User/UserList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:loading
:items-per-page-options="[10, 20, 50, 100]"
:total-count="userCount"
table-name="adminUsers"
data-test="users-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/Connector/ConnectorList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:total-count="connectorCount"
:loading
:items-per-page-options="[10, 20, 50, 100]"
table-name="connectors"
data-test="connector-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/PublicKeys/PublicKeysList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:total-count="publicKeyCount"
:loading
:items-per-page-options="[10, 20, 50, 100]"
table-name="publicKeys"
data-test="public-keys-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/Sessions/SessionList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:total-count="sessionCount"
:loading
:items-per-page-options="[10, 20, 50, 100]"
table-name="sessions"
data-test="sessions-list"
>
<template #rows>
Expand Down
16 changes: 15 additions & 1 deletion ui/src/components/Tables/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, ref, watch, onMounted } from "vue";
import { useTablePreference, type TableName } from "@/composables/useTablePreference";

type Header = {
text: string;
Expand All @@ -159,6 +160,7 @@ const props = defineProps<{
totalCount: number;
loading: boolean;
itemsPerPageOptions?: number[];
tableName?: TableName;
}>();

defineEmits(["update:sort"]);
Expand All @@ -176,6 +178,18 @@ const itemsPerPage = defineModel<number>("itemsPerPage", {
const itemsPerPageError = ref<string | null>(null);
const pageQuantity = computed(() => Math.ceil(props.totalCount / itemsPerPage.value) || 1);

const { getItemsPerPage, setItemsPerPage } = useTablePreference();

onMounted(() => {
if (!props.tableName) return;
const storedValue = getItemsPerPage(props.tableName);
if (storedValue !== itemsPerPage.value) itemsPerPage.value = storedValue;
});

watch(itemsPerPage, (newValue, oldValue) => {
if (props.tableName && newValue !== oldValue && oldValue !== undefined) setItemsPerPage(props.tableName, newValue);
}, { flush: "post" });

const blockNonNumeric = (e: KeyboardEvent) => {
const allowedKeys = [
"Backspace",
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/Tables/DeviceTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:total-count="deviceCount"
:loading
:items-per-page-options="[10, 20, 50, 100]"
table-name="devices"
data-test="items-list"
@update:sort="sortByItem"
>
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/Tags/TagList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:loading
:total-count="tagCount"
:items-per-page-options="[10, 20, 50, 100]"
table-name="tags"
data-test="tag-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/Team/ApiKeys/ApiKeyList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:total-count="apiKeysCount"
:loading
:items-per-page-options="[10, 20, 50, 100]"
table-name="apiKeys"
data-test="api-key-list"
@update:sort="sortByItem"
>
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/Team/Invitation/InvitationList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:total-count="invitationCount"
:loading
:items-per-page-options="[10, 20, 50, 100]"
table-name="invitations"
data-test="invitations-list"
>
<template #rows>
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/WebEndpoints/WebEndpointList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:total-count="totalCount"
:loading="loading"
:items-per-page-options="[10, 20, 50]"
table-name="webEndpoints"
data-test="web-endpoints-table"
@update:sort="sortByItem"
>
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/firewall/FirewallRuleList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:total-count="firewallRuleCount"
:loading
:items-per-page-options="[10, 20, 50, 100]"
table-name="firewallRules"
data-test="firewallRules-list"
>
<template #rows>
Expand Down
38 changes: 38 additions & 0 deletions ui/src/composables/useTablePreference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import handleError from "@/utils/handleError";

// eslint-disable-next-line vue/max-len
export type TableName = "sessions" | "devices" | "containers" | "firewallRules" | "publicKeys" | "apiKeys" | "invitations" | "tags" | "connectors" | "webEndpoints" | "adminSessions" | "adminDevices" | "adminNamespaces" | "adminUsers" | "adminFirewallRules" | "adminAnnouncements";

const STORAGE_KEY = "tablePreferences";
const DEFAULT_ITEMS_PER_PAGE = 10;

export function useTablePreference() {
const getItemsPerPage = (tableName: TableName): number => {
try {
const preferencesItem = localStorage.getItem(STORAGE_KEY);
if (!preferencesItem) return DEFAULT_ITEMS_PER_PAGE;

const preferencesObject = JSON.parse(preferencesItem) as Record<TableName, number>;
return preferencesObject[tableName] ?? DEFAULT_ITEMS_PER_PAGE;
} catch {
return DEFAULT_ITEMS_PER_PAGE;
}
};

const setItemsPerPage = (tableName: TableName, value: number): void => {
try {
const preferencesItem = localStorage.getItem(STORAGE_KEY);
const preferencesObject = preferencesItem ? (JSON.parse(preferencesItem) as Record<string, number>) : {};

preferencesObject[tableName] = value;
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferencesObject));
} catch (error) {
handleError(error);
}
};

return {
getItemsPerPage,
setItemsPerPage,
};
}
71 changes: 71 additions & 0 deletions ui/tests/components/Tables/DataTable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createVuetify } from "vuetify";
import { mount, VueWrapper, flushPromises } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import DataTable from "@/components/Tables/DataTable.vue";
import * as useTablePreferenceModule from "@/composables/useTablePreference";

type DataTableWrapper = VueWrapper<InstanceType<typeof DataTable>>;

Expand Down Expand Up @@ -185,4 +186,74 @@ describe("DataTable", () => {
expect(text).toContain("1 of 2");
});
});

describe("persistence with tableName prop", () => {
let mockGetItemsPerPage: ReturnType<typeof vi.fn>;
let mockSetItemsPerPage: ReturnType<typeof vi.fn>;

beforeEach(() => {
mockGetItemsPerPage = vi.fn().mockReturnValue(10);
mockSetItemsPerPage = vi.fn();

vi.spyOn(useTablePreferenceModule, "useTablePreference").mockReturnValue({
getItemsPerPage: mockGetItemsPerPage,
setItemsPerPage: mockSetItemsPerPage,
});
});

it("should load initial itemsPerPage from localStorage when tableName is provided", async () => {
mockGetItemsPerPage.mockReturnValue(50);

const localWrapper = mountWrapper({ tableName: "sessions", itemsPerPage: 10 });
await uiTick();

expect(mockGetItemsPerPage).toHaveBeenCalledWith("sessions");

const ippEvents = localWrapper.emitted("update:itemsPerPage");
expect(ippEvents).toBeTruthy();
expect(ippEvents && ippEvents[0]).toEqual([50]);

localWrapper.unmount();
});

it("should not call persistence functions when tableName is not provided", async () => {
const localWrapper = mountWrapper({ itemsPerPage: 10 });
await uiTick();

const combo = localWrapper.findComponent({ name: "VCombobox" });
combo.vm.$emit("update:modelValue", 20);
await uiTick();

expect(mockSetItemsPerPage).not.toHaveBeenCalled();

localWrapper.unmount();
});

it("should save to localStorage when itemsPerPage changes and tableName is provided", async () => {
const localWrapper = mountWrapper({ tableName: "devices", itemsPerPage: 10 });
await uiTick();

const combo = localWrapper.findComponent({ name: "VCombobox" });
combo.vm.$emit("update:modelValue", 25);
await uiTick();

expect(mockSetItemsPerPage).toHaveBeenCalledWith("devices", 25);

localWrapper.unmount();
});

it("should handle localStorage failures gracefully without breaking functionality", async () => {
vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
throw new Error("localStorage error");
});

const localWrapper = mountWrapper({ tableName: "sessions", itemsPerPage: 10 });
await uiTick();

expect(localWrapper.vm).toBeTruthy();
expect(localWrapper.find('[data-test="datatable"]').exists()).toBe(true);

localWrapper.unmount();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`Device Table > Renders the component 1`] = `
"<div data-v-787abe29="">
<data-table-stub data-v-787abe29="" page="1" itemsperpage="10" headers="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" items="[object Object],[object Object]" totalcount="2" itemsperpageoptions="10,20,50,100" loading="false" data-test="items-list"></data-table-stub>
<data-table-stub data-v-787abe29="" page="1" itemsperpage="10" headers="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" items="[object Object],[object Object]" totalcount="2" itemsperpageoptions="10,20,50,100" tablename="devices" loading="false" data-test="items-list"></data-table-stub>
<!--v-if-->
</div>"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ exports[`InvitationList > Renders the component 1`] = `
<div class="v-input v-input--horizontal v-input--hide-spin-buttons v-input--density-default v-theme--light v-locale--is-ltr v-input--dirty v-text-field v-input--plain-underlined v-combobox v-combobox--single mb-4 mr-1 w-100" data-test="ipp-combo">
<!---->
<div class="v-input__control">
<div class="v-field v-field--active v-field--appended v-field--center-affix v-field--dirty v-field--no-label v-field--variant-underlined v-theme--light v-locale--is-ltr" role="combobox" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-v-7" aria-owns="menu-v-7">
<div class="v-field v-field--active v-field--appended v-field--dirty v-field--no-label v-field--variant-underlined v-theme--light v-locale--is-ltr" role="combobox" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-v-7" aria-owns="menu-v-7">
<div class="v-field__overlay"></div>
<div class="v-field__loader">
<div class="v-progress-linear v-theme--light v-locale--is-ltr" style="top: 0px; height: 0px; --v-progress-linear-height: 2px;" role="progressbar" aria-hidden="true" aria-valuemin="0" aria-valuemax="100">
Expand All @@ -75,14 +75,13 @@ exports[`InvitationList > Renders the component 1`] = `
<div class="v-field__input" data-no-activator="">
<!---->
<!---->
<div class="v-combobox__selection"><span class="v-combobox__selection-text">10<!----></span></div><input size="1" role="combobox" type="number" id="input-v-9" aria-expanded="false" aria-controls="menu-v-7" outlined="" value="10">
<div class="v-combobox__selection"><span class="v-combobox__selection-text">10<!----></span></div><input size="1" role="combobox" type="number" aria-labelledby="input-v-9-label" id="input-v-9" aria-expanded="false" aria-controls="menu-v-7" value="10">
</div>
<!---->
</div>
<!---->
<div class="v-field__append-inner">
<!----><i class="mdi-menu-down mdi v-icon notranslate v-theme--light v-icon--size-default v-icon--clickable v-combobox__menu-icon" role="button" aria-hidden="false" tabindex="-1" aria-label="Open" title="Open"></i>
<!---->
<!----><i class="mdi-menu-down mdi v-icon notranslate v-theme--light v-icon--size-default v-icon--clickable v-combobox__menu-icon" role="button" aria-hidden="true" tabindex="-1"></i>
</div>
<div class="v-field__outline">
<!---->
Expand Down
Loading