From 5a3d8cbdeeda2193a6eeb058fc405b6f8f607aba Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:22:22 +0200 Subject: [PATCH 1/4] Improve quota field types in schema --- client/src/api/schema/schema.ts | 6 +++--- lib/galaxy/schema/schema.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index fd0b318f8b0b..2720918058d5 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -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. @@ -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 diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index 3653890f4968..e782cdb82092 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -362,7 +362,7 @@ class CreatedUserModel(UserModel, DiskUsageUserModel): class AnonUserModel(DiskUsageUserModel): - quota_percent: Any = QuotaPercentField + quota_percent: Optional[float] = QuotaPercentField class DetailedUserModel(BaseUserModel, AnonUserModel): @@ -371,7 +371,9 @@ class DetailedUserModel(BaseUserModel, AnonUserModel): preferences: Dict[Any, Any] = Field(default=..., title="Preferences", description="Preferences of the user") preferred_object_store_id: Optional[str] = PreferredObjectStoreIdField quota: str = Field(default=..., title="Quota", description="Quota applicable to the user") - quota_bytes: Any = Field(default=..., title="Quota in bytes", description="Quota applicable to the user in bytes.") + quota_bytes: Optional[int] = Field( + default=None, title="Quota in bytes", description="Quota applicable to the user in bytes." + ) tags_used: List[str] = Field(default=..., title="Tags used", description="Tags used by the user") From a99d4e576a4baeaa7b90098c9877e0c6039b8729 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 5 Aug 2024 10:21:02 +0200 Subject: [PATCH 2/4] Improve client types around user + refactoring Refactor userStore method to use matchesCurrentUsername for ownership checks. --- client/src/api/index.ts | 28 ++++++++----------- client/src/api/workflows.ts | 13 +++++++++ client/src/components/Masthead/QuotaMeter.vue | 9 +++--- .../Workflow/List/WorkflowActions.vue | 7 ++--- .../Workflow/List/WorkflowActionsExtend.vue | 7 ++--- .../components/Workflow/List/WorkflowCard.vue | 8 ++---- .../Workflow/List/WorkflowIndicators.vue | 11 ++++---- .../components/Workflow/List/WorkflowList.vue | 2 +- .../Published/WorkflowInformation.vue | 20 ++----------- .../Workflow/Published/WorkflowPublished.vue | 19 ++++--------- .../components/Workflow/Run/WorkflowRun.vue | 5 ++-- .../components/Workflow/workflows.services.ts | 4 +-- client/src/stores/userStore.ts | 17 +++++++---- 13 files changed, 67 insertions(+), 83 deletions(-) diff --git a/client/src/api/index.ts b/client/src/api/index.ts index b27d9746a0f3..e22be0702139 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -215,36 +215,32 @@ 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"]; -/** 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; +export type GenericUser = RegisteredUser | AnonymousUser; /** Represents any user, including anonymous users or session-less (null) users.**/ export type AnyUser = GenericUser | null; -export function isRegisteredUser(user: AnyUser): user is User { - return user !== null && !user?.isAnonymous; +export function isRegisteredUser(user: AnyUser): user is RegisteredUser { + return user !== null && "email" in user; } export function isAnonymousUser(user: AnyUser): user is AnonymousUser { - return user !== null && user.isAnonymous; + return user !== null && !isRegisteredUser(user); +} + +export function isAdminUser(user: AnyUser): user is RegisteredUser { + return isRegisteredUser(user) && user.is_admin; } export function userOwnsHistory(user: AnyUser, history: AnyHistory) { diff --git a/client/src/api/workflows.ts b/client/src/api/workflows.ts index 4d676cefcc94..af3fbd325502 100644 --- a/client/src/api/workflows.ts +++ b/client/src/api/workflows.ts @@ -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; + }[]; +} diff --git a/client/src/components/Masthead/QuotaMeter.vue b/client/src/components/Masthead/QuotaMeter.vue index fe6ad9e1fcff..5d3dc01ae72b 100644 --- a/client/src/components/Masthead/QuotaMeter.vue +++ b/client/src/components/Masthead/QuotaMeter.vue @@ -3,6 +3,7 @@ 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"; @@ -10,13 +11,13 @@ import { bytesToString } from "@/utils/utils"; const { config } = useConfig(); const { currentUser, isAnonymous } = storeToRefs(useUserStore()); -const hasQuota = computed(() => { +const hasQuota = computed(() => { 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; @@ -56,7 +57,7 @@ const variant = computed(() => { Using {{ usage.toFixed(0) }}% - of {{ quotaLimit }} + of {{ quotaLimit }} {{ totalUsageString }} diff --git a/client/src/components/Workflow/List/WorkflowActions.vue b/client/src/components/Workflow/List/WorkflowActions.vue index a6acda9a75f6..91c5524c1ac4 100644 --- a/client/src/components/Workflow/List/WorkflowActions.vue +++ b/client/src/components/Workflow/List/WorkflowActions.vue @@ -44,12 +44,9 @@ const { confirm } = useConfirmDialog(); const bookmarkLoading = ref(false); const shared = computed(() => { - if (userStore.currentUser) { - return userStore.currentUser.username !== props.workflow.owner; - } else { - return false; - } + return !userStore.matchesCurrentUsername(props.workflow.owner); }); + const sourceType = computed(() => { if (props.workflow.source_metadata?.url) { return "url"; diff --git a/client/src/components/Workflow/List/WorkflowActionsExtend.vue b/client/src/components/Workflow/List/WorkflowActionsExtend.vue index 6968cc745ef7..ae5878a44e7f 100644 --- a/client/src/components/Workflow/List/WorkflowActionsExtend.vue +++ b/client/src/components/Workflow/List/WorkflowActionsExtend.vue @@ -36,12 +36,9 @@ const { isAnonymous } = storeToRefs(useUserStore()); const downloadUrl = computed(() => { return withPrefix(`/api/workflows/${props.workflow.id}/download?format=json-download`); }); + const shared = computed(() => { - if (userStore.currentUser) { - return userStore.currentUser.username !== props.workflow.owner; - } else { - return false; - } + return !userStore.matchesCurrentUsername(props.workflow.owner); }); async function onCopy() { diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue index 076ec2c271e6..f217a1a76ec2 100644 --- a/client/src/components/Workflow/List/WorkflowCard.vue +++ b/client/src/components/Workflow/List/WorkflowCard.vue @@ -48,13 +48,11 @@ const showRename = ref(false); const showPreview = ref(false); const workflow = computed(() => props.workflow); + const shared = computed(() => { - if (userStore.currentUser) { - return userStore.currentUser.username !== workflow.value.owner; - } else { - return false; - } + return !userStore.matchesCurrentUsername(workflow.value.owner); }); + const description = computed(() => { if (workflow.value.annotations && workflow.value.annotations.length > 0) { return workflow.value.annotations[0].trim(); diff --git a/client/src/components/Workflow/List/WorkflowIndicators.vue b/client/src/components/Workflow/List/WorkflowIndicators.vue index 676e56932a03..92ccac96c4ae 100644 --- a/client/src/components/Workflow/List/WorkflowIndicators.vue +++ b/client/src/components/Workflow/List/WorkflowIndicators.vue @@ -29,19 +29,17 @@ const router = useRouter(); const userStore = useUserStore(); const publishedTitle = computed(() => { - if (userStore.currentUser?.username === props.workflow.owner) { + if (userStore.matchesCurrentUsername(props.workflow.owner)) { return "Published by you. Click to view all published workflows by you"; } else { return `Published by '${props.workflow.owner}'. Click to view all published workflows by '${props.workflow.owner}'`; } }); + const shared = computed(() => { - if (userStore.currentUser) { - return userStore.currentUser.username !== props.workflow.owner; - } else { - return false; - } + return !userStore.matchesCurrentUsername(props.workflow.owner); }); + const sourceType = computed(() => { if (props.workflow.source_metadata?.url) { return "url"; @@ -51,6 +49,7 @@ const sourceType = computed(() => { return ""; } }); + const sourceTitle = computed(() => { if (sourceType.value.includes("trs")) { return `Imported from TRS ID (version: ${props.workflow.source_metadata.trs_version_id}). Click to copy ID`; diff --git a/client/src/components/Workflow/List/WorkflowList.vue b/client/src/components/Workflow/List/WorkflowList.vue index b730a12f002c..70467677c548 100644 --- a/client/src/components/Workflow/List/WorkflowList.vue +++ b/client/src/components/Workflow/List/WorkflowList.vue @@ -145,7 +145,7 @@ async function load(overlayLoading = false, silent = false) { : data; if (props.activeList === "my") { - filteredWorkflows = filter(filteredWorkflows, (w: any) => w.owner === userStore.currentUser?.username); + filteredWorkflows = filter(filteredWorkflows, (w: any) => userStore.matchesCurrentUsername(w.owner)); } workflowsLoaded.value = filteredWorkflows; diff --git a/client/src/components/Workflow/Published/WorkflowInformation.vue b/client/src/components/Workflow/Published/WorkflowInformation.vue index b9d9afb1acb5..dc41bdd27c85 100644 --- a/client/src/components/Workflow/Published/WorkflowInformation.vue +++ b/client/src/components/Workflow/Published/WorkflowInformation.vue @@ -5,6 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { computed } from "vue"; import { RouterLink } from "vue-router"; +import { type WorkflowSummary } from "@/api/workflows"; import { useUserStore } from "@/stores/userStore"; import { getFullAppUrl } from "@/utils/utils"; @@ -16,19 +17,8 @@ import UtcDate from "@/components/UtcDate.vue"; library.add(faBuilding, faUser); -interface WorkflowInformation { - name: string; - [key: string]: unknown; - update_time: string; - license?: string; - tags?: string[]; - creator?: { - [key: string]: unknown; - }[]; -} - interface Props { - workflowInfo: WorkflowInformation; + workflowInfo: WorkflowSummary; embedded?: boolean; } @@ -51,11 +41,7 @@ const fullLink = computed(() => { }); const userOwned = computed(() => { - if (userStore.currentUser) { - return userStore.currentUser.username === props.workflowInfo.owner; - } else { - return false; - } + return userStore.matchesCurrentUsername(props.workflowInfo.owner); }); diff --git a/client/src/components/Workflow/Published/WorkflowPublished.vue b/client/src/components/Workflow/Published/WorkflowPublished.vue index d2c14b7d18b3..524623314ab7 100644 --- a/client/src/components/Workflow/Published/WorkflowPublished.vue +++ b/client/src/components/Workflow/Published/WorkflowPublished.vue @@ -6,6 +6,7 @@ import { type AxiosError } from "axios"; import { BAlert, BButton, BCard } from "bootstrap-vue"; import { computed, onMounted, ref, watch } from "vue"; +import { type WorkflowSummary } from "@/api/workflows"; import { fromSimple } from "@/components/Workflow/Editor/modules/model"; import { getWorkflowFull, getWorkflowInfo } from "@/components/Workflow/workflows.services"; import { useDatatypesMapper } from "@/composables/datatypesMapper"; @@ -22,14 +23,6 @@ import WorkflowInformation from "@/components/Workflow/Published/WorkflowInforma library.add(faBuilding, faDownload, faEdit, faPlay, faSpinner, faUser); -type WorkflowInfo = { - name: string; - [key: string]: unknown; - license?: string; - tags?: string[]; - update_time: string; -}; - interface Props { id: string; zoom?: number; @@ -65,7 +58,7 @@ const { stateStore } = provideScopedWorkflowStores(props.id); const loading = ref(true); const errorMessage = ref(""); -const workflowInfo = ref(); +const workflowInfo = ref(); const workflow = ref(null); const hasError = computed(() => !!errorMessage.value); @@ -80,13 +73,11 @@ const initialPosition = computed(() => ({ })); const viewUrl = computed(() => withPrefix(`/published/workflow?id=${props.id}`)); + const sharedWorkflow = computed(() => { - if (userStore.currentUser) { - return userStore.currentUser.username !== workflowInfo.value?.owner; - } else { - return false; - } + return !userStore.matchesCurrentUsername(workflowInfo.value?.owner); }); + const editButtonTitle = computed(() => { if (userStore.isAnonymous) { return "Log in to edit Workflow"; diff --git a/client/src/components/Workflow/Run/WorkflowRun.vue b/client/src/components/Workflow/Run/WorkflowRun.vue index 5f3a97e7dad0..4190257e3cdd 100644 --- a/client/src/components/Workflow/Run/WorkflowRun.vue +++ b/client/src/components/Workflow/Run/WorkflowRun.vue @@ -1,6 +1,5 @@