Skip to content

Commit

Permalink
Merge pull request #18645 from davelopez/improve_user_types
Browse files Browse the repository at this point in the history
Improve types around User in schema and client
  • Loading branch information
mvdbeek authored Aug 6, 2024
2 parents db9ff9e + 85dd962 commit 4f20fbf
Show file tree
Hide file tree
Showing 28 changed files with 168 additions and 163 deletions.
12 changes: 4 additions & 8 deletions client/src/api/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { getFakeRegisteredUser } from "@tests/test-data";

import {
type AnonymousUser,
type AnyHistory,
type HistorySummary,
type HistorySummaryExtended,
isRegisteredUser,
type User,
userOwnsHistory,
} from ".";

const REGISTERED_USER_ID = "fake-user-id";
const ANOTHER_USER_ID = "another-fake-user-id";
const ANONYMOUS_USER_ID = null;

const REGISTERED_USER: User = {
id: REGISTERED_USER_ID,
email: "[email protected]",
tags_used: [],
isAnonymous: false,
total_disk_usage: 0,
};
const REGISTERED_USER = getFakeRegisteredUser({ id: REGISTERED_USER_ID });

const ANONYMOUS_USER: AnonymousUser = {
isAnonymous: true,
total_disk_usage: 0,
nice_total_disk_usage: "0.0 bytes",
};

const SESSIONLESS_USER = null;
Expand Down
33 changes: 14 additions & 19 deletions client/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,36 +215,31 @@ export function isHistoryItem(item: object): item is HistoryItemSummary {
return item && "history_content_type" in item;
}

type QuotaUsageResponse = components["schemas"]["UserQuotaUsage"];
type RegisteredUserModel = components["schemas"]["DetailedUserModel"];
type AnonymousUserModel = components["schemas"]["AnonUserModel"];
type UserModel = RegisteredUserModel | AnonymousUserModel;

/** Represents a registered user.**/
export interface User extends QuotaUsageResponse {
id: string;
email: string;
tags_used: string[];
export interface RegisteredUser extends RegisteredUserModel {
isAnonymous: false;
is_admin?: boolean;
username?: string;
}

export interface AnonymousUser extends QuotaUsageResponse {
id?: string;
export interface AnonymousUser extends AnonymousUserModel {
isAnonymous: true;
is_admin?: false;
username?: string;
}

export type GenericUser = User | AnonymousUser;

/** Represents any user, including anonymous users or session-less (null) users.**/
export type AnyUser = GenericUser | null;
export type AnyUser = RegisteredUser | AnonymousUser | null;

export function isRegisteredUser(user: AnyUser | UserModel): user is RegisteredUser {
return user !== null && "email" in user;
}

export function isRegisteredUser(user: AnyUser): user is User {
return user !== null && !user?.isAnonymous;
export function isAnonymousUser(user: AnyUser | UserModel): user is AnonymousUser {
return user !== null && !isRegisteredUser(user);
}

export function isAnonymousUser(user: AnyUser): user is AnonymousUser {
return user !== null && user.isAnonymous;
export function isAdminUser(user: AnyUser | UserModel): user is RegisteredUser {
return isRegisteredUser(user) && user.is_admin;
}

export function userOwnsHistory(user: AnyUser, history: AnyHistory) {
Expand Down
6 changes: 3 additions & 3 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2187,7 +2187,7 @@ export interface components {
* Quota percent
* @description Percentage of the storage quota applicable to the user.
*/
quota_percent?: unknown;
quota_percent?: number | null;
/**
* Total disk usage
* @description Size of all non-purged, unique datasets of the user in bytes.
Expand Down Expand Up @@ -4613,12 +4613,12 @@ export interface components {
* Quota in bytes
* @description Quota applicable to the user in bytes.
*/
quota_bytes: unknown;
quota_bytes?: number | null;
/**
* Quota percent
* @description Percentage of the storage quota applicable to the user.
*/
quota_percent?: unknown;
quota_percent?: number | null;
/**
* Tags used
* @description Tags used by the user
Expand Down
13 changes: 13 additions & 0 deletions client/src/api/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,16 @@ export const invocationCountsFetcher = fetcher.path("/api/workflows/{workflow_id

export const sharing = fetcher.path("/api/workflows/{workflow_id}/sharing").method("get").create();
export const enableLink = fetcher.path("/api/workflows/{workflow_id}/enable_link_access").method("put").create();

//TODO: replace with generated schema model when available
export interface WorkflowSummary {
name: string;
owner: string;
[key: string]: unknown;
update_time: string;
license?: string;
tags?: string[];
creator?: {
[key: string]: unknown;
}[];
}
9 changes: 2 additions & 7 deletions client/src/components/DatasetInformation/DatasetError.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getFakeRegisteredUser } from "@tests/test-data";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { createPinia } from "pinia";
Expand Down Expand Up @@ -57,13 +58,7 @@ async function montDatasetError(has_duplicate_inputs = true, has_empty_inputs =
});

const userStore = useUserStore();
userStore.currentUser = {
email: user_email || "email",
id: "user_id",
tags_used: [],
isAnonymous: false,
total_disk_usage: 0,
};
userStore.currentUser = getFakeRegisteredUser({ email: user_email });

await flushPromises();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert } from "bootstrap-vue";
import { computed, ref } from "vue";
import { type GenericUser, type HistorySummary, userOwnsHistory } from "@/api";
import { type AnyUser, type HistorySummary, userOwnsHistory } from "@/api";
import localize from "@/utils/localization";
library.add(faArchive, faBurn, faTrash);
interface Props {
history: HistorySummary;
currentUser: GenericUser | null;
currentUser: AnyUser;
}
const props = defineProps<Props>();
Expand Down
6 changes: 2 additions & 4 deletions client/src/components/History/HistoryView.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getFakeRegisteredUser } from "@tests/test-data";
import { mount } from "@vue/test-utils";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
Expand Down Expand Up @@ -76,10 +77,7 @@ async function createWrapper(localVue, currentUserId, history) {
pinia,
});
const userStore = useUserStore();
const userData = {
id: currentUserId,
};
userStore.currentUser = { ...userStore.currentUser, ...userData };
userStore.currentUser = getFakeRegisteredUser({ id: currentUserId });
await flushPromises();
return wrapper;
}
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/History/Multiple/MultipleView.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getFakeRegisteredUser } from "@tests/test-data";
import { mount } from "@vue/test-utils";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
Expand All @@ -20,7 +21,8 @@ const getFakeHistorySummaries = (num, selectedIndex) => {
update_time: new Date().toISOString(),
}));
};
const currentUser = { id: USER_ID };

const currentUser = getFakeRegisteredUser({ id: USER_ID });

describe("MultipleView", () => {
let axiosMock;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getFakeRegisteredUser } from "@tests/test-data";
import { shallowMount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { createPinia } from "pinia";
Expand Down Expand Up @@ -31,14 +32,7 @@ async function mountJobDestinationParams() {
});

const userStore = useUserStore();
userStore.currentUser = {
email: "admin@email",
id: "1",
tags_used: [],
isAnonymous: false,
total_disk_usage: 1048576,
is_admin: true,
};
userStore.currentUser = getFakeRegisteredUser({ is_admin: true });

await flushPromises();

Expand Down
8 changes: 8 additions & 0 deletions client/src/components/Masthead/Masthead.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createTestingPinia } from "@pinia/testing";
import { getFakeRegisteredUser } from "@tests/test-data";
import { mount } from "@vue/test-utils";
import { WindowManager } from "layout/window-manager";
import { PiniaVuePlugin } from "pinia";
import { getLocalVue } from "tests/jest/helpers";

import { mockFetcher } from "@/api/schema/__mocks__";
import { useUserStore } from "@/stores/userStore";

import { loadWebhookMenuItems } from "./_webhooks";

Expand All @@ -18,6 +20,8 @@ jest.mock("vue-router/composables", () => ({
}));
jest.mock("@/api/schema");

const currentUser = getFakeRegisteredUser();

describe("Masthead.vue", () => {
let wrapper;
let localVue;
Expand All @@ -42,6 +46,10 @@ describe("Masthead.vue", () => {

windowManager = new WindowManager({});
const windowTab = windowManager.getTab();

const userStore = useUserStore();
userStore.currentUser = currentUser;

wrapper = mount(Masthead, {
propsData: {
windowTab,
Expand Down
30 changes: 13 additions & 17 deletions client/src/components/Masthead/QuotaMeter.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createTestingPinia } from "@pinia/testing";
import { getFakeRegisteredUser } from "@tests/test-data";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { getLocalVue } from "tests/jest/helpers";

import { type RegisteredUser } from "@/api";
import { useUserStore } from "@/stores/userStore";

import QuotaMeter from "./QuotaMeter.vue";
Expand All @@ -20,11 +22,11 @@ jest.mock("@/composables/config", () => ({

const localVue = getLocalVue();

async function createQuotaMeterWrapper(config: any, userData: any) {
async function createQuotaMeterWrapper(config: any, user: RegisteredUser) {
configValues = { ...config };
const pinia = createTestingPinia();
const userStore = useUserStore();
userStore.currentUser = { ...userStore.currentUser, ...userData };
userStore.currentUser = user;
const wrapper = mount(QuotaMeter, {
localVue,
pinia,
Expand All @@ -33,55 +35,49 @@ async function createQuotaMeterWrapper(config: any, userData: any) {
return wrapper;
}

const FAKE_USER = getFakeRegisteredUser({ quota: "100 MB", total_disk_usage: 5120, quota_percent: 50 });

describe("QuotaMeter.vue", () => {
it("shows a percentage usage", async () => {
const user = {
total_disk_usage: 5120,
quota_percent: 50,
quota: "100 MB",
};
const config = { enable_quotas: true };
const wrapper = await createQuotaMeterWrapper(config, user);
const wrapper = await createQuotaMeterWrapper(config, FAKE_USER);
expect(wrapper.find(".quota-progress > span").text()).toBe("Using 50% of 100 MB");
});

it("changes appearance depending on usage", async () => {
const config = { enable_quotas: true };
{
const user = { quota_percent: 30 };
const user = { ...FAKE_USER, quota_percent: 30 };
const wrapper = await createQuotaMeterWrapper(config, user);
expect(wrapper.find(".quota-progress .progress-bar").classes()).toContain("bg-success");
}
{
const user = { quota_percent: 80 };
const user = { ...FAKE_USER, quota_percent: 80 };
const wrapper = await createQuotaMeterWrapper(config, user);
expect(wrapper.find(".quota-progress .progress-bar").classes()).toContain("bg-warning");
}
{
const user = { quota_percent: 95 };
const user = { ...FAKE_USER, quota_percent: 95 };
const wrapper = await createQuotaMeterWrapper(config, user);
expect(wrapper.find(".quota-progress .progress-bar").classes()).toContain("bg-danger");
}
});

it("displays tooltip", async () => {
const config = { enable_quotas: true };
const wrapper = await createQuotaMeterWrapper(config, {});
const wrapper = await createQuotaMeterWrapper(config, FAKE_USER);
expect(wrapper.attributes("title")).toContain("Storage");
});

it("shows total usage when there is no quota", async () => {
{
const user = { total_disk_usage: 7168 };
const user = { ...FAKE_USER, total_disk_usage: 7168 };
const config = { enable_quotas: false };
const wrapper = await createQuotaMeterWrapper(config, user);
expect(wrapper.find("span").text()).toBe("Using 7 KB");
}
{
const user = {
total_disk_usage: 21504,
quota: "unlimited",
};
const user = { ...FAKE_USER, total_disk_usage: 21504, quota: "unlimited" };
const config = { enable_quotas: true };
const wrapper = await createQuotaMeterWrapper(config, user);
expect(wrapper.find("span").text()).toBe("Using 21 KB");
Expand Down
9 changes: 5 additions & 4 deletions client/src/components/Masthead/QuotaMeter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ import { BLink, BProgress, BProgressBar } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed } from "vue";
import { isRegisteredUser } from "@/api";
import { useConfig } from "@/composables/config";
import { useUserStore } from "@/stores/userStore";
import { bytesToString } from "@/utils/utils";
const { config } = useConfig();
const { currentUser, isAnonymous } = storeToRefs(useUserStore());
const hasQuota = computed(() => {
const hasQuota = computed<boolean>(() => {
const quotasEnabled = config.value.enable_quotas ?? false;
const quotaLimited = currentUser.value?.quota !== "unlimited" ?? false;
const quotaLimited = (isRegisteredUser(currentUser.value) && currentUser.value.quota !== "unlimited") ?? false;
return quotasEnabled && quotaLimited;
});
const quotaLimit = computed(() => currentUser.value?.quota ?? 0);
const quotaLimit = computed(() => (isRegisteredUser(currentUser.value) && currentUser.value.quota) ?? null);
const totalUsageString = computed(() => {
const total = currentUser.value?.total_disk_usage ?? 0;
Expand Down Expand Up @@ -56,7 +57,7 @@ const variant = computed(() => {
<span v-localize>Using</span>
<span v-if="hasQuota">
<span>{{ usage.toFixed(0) }}%</span>
<span v-if="quotaLimit !== 0">of {{ quotaLimit }}</span>
<span v-if="quotaLimit !== null">of {{ quotaLimit }}</span>
</span>
<span v-else>{{ totalUsageString }}</span>
</span>
Expand Down
10 changes: 3 additions & 7 deletions client/src/components/User/DiskUsage/DiskUsageSummary.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getFakeRegisteredUser } from "@tests/test-data";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { createPinia } from "pinia";
Expand All @@ -19,16 +20,11 @@ const localVue = getLocalVue();
const quotaUsageClassSelector = ".quota-usage";
const basicDiskUsageSummaryId = "#basic-disk-usage-summary";

const fakeUserWithQuota = {
id: "fakeUser",
email: "fakeUserEmail",
tags_used: [],
isAnonymous: false,
const fakeUserWithQuota = getFakeRegisteredUser({
total_disk_usage: 1048576,
quota_bytes: 104857600,
quota_percent: 1,
quota_source_label: "Default",
};
});

// TODO: Replace this with a mockFetcher when #16608 is merged
const mockGetCurrentUser = getCurrentUser as jest.Mock;
Expand Down
Loading

0 comments on commit 4f20fbf

Please sign in to comment.