From c4f6987b8378b6660eaf1f8d217d1f84a429e950 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 11:47:01 -0500 Subject: [PATCH 01/42] Consolidate history grids --- client/src/components/Grid/GridHistory.vue | 42 ++++++++++++++++++++++ client/src/components/Grid/GridList.vue | 5 ++- client/src/entry/analysis/router.js | 16 ++++----- 3 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 client/src/components/Grid/GridHistory.vue diff --git a/client/src/components/Grid/GridHistory.vue b/client/src/components/Grid/GridHistory.vue new file mode 100644 index 000000000000..58201aac4ff4 --- /dev/null +++ b/client/src/components/Grid/GridHistory.vue @@ -0,0 +1,42 @@ + + + diff --git a/client/src/components/Grid/GridList.vue b/client/src/components/Grid/GridList.vue index a8a8bae298f3..4a04eb5133c0 100644 --- a/client/src/components/Grid/GridList.vue +++ b/client/src/components/Grid/GridList.vue @@ -33,12 +33,15 @@ interface Props { gridMessage?: string; // debounce delay delay?: number; + // embedded + embedded?: boolean; // rows per page to be shown limit?: number; } const props = withDefaults(defineProps(), { delay: 5000, + embedded: false, limit: 25, }); @@ -237,7 +240,7 @@ watch(operationMessage, () => { {{ errorMessage }} {{ operationMessage }}
-
+
{{ gridConfig.title }} diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 21ff320e7ce1..bd999611ef2d 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -7,9 +7,6 @@ import DatasetAttributes from "components/DatasetInformation/DatasetAttributes"; import DatasetDetails from "components/DatasetInformation/DatasetDetails"; import DatasetError from "components/DatasetInformation/DatasetError"; import FormGeneric from "components/Form/FormGeneric"; -import historiesGridConfig from "components/Grid/configs/histories"; -import historiesPublishedGridConfig from "components/Grid/configs/historiesPublished"; -import historiesSharedGridConfig from "components/Grid/configs/historiesShared"; import visualizationsGridConfig from "components/Grid/configs/visualizations"; import visualizationsPublishedGridConfig from "components/Grid/configs/visualizationsPublished"; import GridList from "components/Grid/GridList"; @@ -63,6 +60,7 @@ import Vue from "vue"; import VueRouter from "vue-router"; import AvailableDatatypes from "@/components/AvailableDatatypes/AvailableDatatypes"; +import GridHistory from "@/components/Grid/GridHistory"; import { parseBool } from "@/utils/utils"; import { patchRouterPush } from "./router-push"; @@ -283,9 +281,9 @@ export function getRouter(Galaxy) { }, { path: "histories/list_published", - component: GridList, + component: GridHistory, props: { - gridConfig: historiesPublishedGridConfig, + activeList: "published", }, }, { @@ -294,17 +292,17 @@ export function getRouter(Galaxy) { }, { path: "histories/list", - component: GridList, + component: GridHistory, props: { - gridConfig: historiesGridConfig, + activeList: "my", }, redirect: redirectAnon(), }, { path: "histories/list_shared", - component: GridList, + component: GridHistory, props: { - gridConfig: historiesSharedGridConfig, + activeList: "shared", }, redirect: redirectAnon(), }, From 1a341cafa73e4b8bcdb79d68119e60bee43e1b0e Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 11:49:57 -0500 Subject: [PATCH 02/42] Consolidate masthead tabs for history grids --- client/src/entry/analysis/menu.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/client/src/entry/analysis/menu.js b/client/src/entry/analysis/menu.js index 7badb524ee39..2dbb08895733 100644 --- a/client/src/entry/analysis/menu.js +++ b/client/src/entry/analysis/menu.js @@ -54,8 +54,8 @@ export function fetchMenu(options = {}) { }); } else { menu.push({ - id: "shared", - title: _l("Shared Data"), + id: "resources", + title: _l("Data"), url: "javascript:void(0)", tooltip: _l("Access published resources"), menu: [ @@ -66,7 +66,7 @@ export function fetchMenu(options = {}) { }, { title: _l("Histories"), - url: "/histories/list_published", + url: "/histories/list", }, { title: _l("Workflows"), @@ -206,15 +206,6 @@ export function fetchMenu(options = {}) { title: _l("Datasets"), url: "/datasets/list", }, - { - title: _l("Histories"), - url: "/histories/list", - }, - { - title: _l("Histories shared with me"), - url: "/histories/list_shared", - hidden: Galaxy.config.single_user, - }, { title: _l("Archived Histories"), url: "/histories/archived", From 8c7139a0f21f212d9d3d50a8035ab8785b2f9022 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 12:14:10 -0500 Subject: [PATCH 03/42] Consolidate visualizations grids into tab view, adjust masthead user tab --- .../src/components/Grid/GridVisualization.vue | 39 ++++++++++++ client/src/entry/analysis/menu.js | 59 ++++++++----------- client/src/entry/analysis/router.js | 12 ++-- 3 files changed, 70 insertions(+), 40 deletions(-) create mode 100644 client/src/components/Grid/GridVisualization.vue diff --git a/client/src/components/Grid/GridVisualization.vue b/client/src/components/Grid/GridVisualization.vue new file mode 100644 index 000000000000..13661f22a12a --- /dev/null +++ b/client/src/components/Grid/GridVisualization.vue @@ -0,0 +1,39 @@ + + + diff --git a/client/src/entry/analysis/menu.js b/client/src/entry/analysis/menu.js index 2dbb08895733..d71b8ccdb060 100644 --- a/client/src/entry/analysis/menu.js +++ b/client/src/entry/analysis/menu.js @@ -64,21 +64,29 @@ export function fetchMenu(options = {}) { url: "/libraries", target: "_top", }, + { + title: _l("Datasets"), + url: "/datasets/list", + }, { title: _l("Histories"), url: "/histories/list", }, { - title: _l("Workflows"), - url: "/workflows/list_published", + title: _l("Pages"), + url: "/pages/list", + }, + { + title: _l("Published Pages"), + url: "/pages/list_published", }, { title: _l("Visualizations"), - url: "/visualizations/list_published", + url: "/visualizations/list", }, { - title: _l("Pages"), - url: "/pages/list_published", + title: _l("Workflows"), + url: "/workflows/list", }, ], }); @@ -197,48 +205,33 @@ export function fetchMenu(options = {}) { disabled: true, }, { divider: true }, - { - title: _l("Preferences"), - url: "/user", - }, - { divider: true }, - { - title: _l("Datasets"), - url: "/datasets/list", - }, - { - title: _l("Archived Histories"), - url: "/histories/archived", - }, - { - title: _l("Pages"), - url: "/pages/list", - }, - { - title: _l("Workflow Invocations"), - url: "/workflows/invocations", - }, ], }; - if (Galaxy.config.visualizations_visible) { - userTab.menu.push({ - title: _l("Visualizations"), - url: "/visualizations/list", - }); - } if (Galaxy.config.interactivetools_enable) { userTab.menu.push({ - title: _l("Active InteractiveTools"), + title: _l("Active Interactive Tools"), url: "/interactivetool_entry_points/list", }); } + userTab.menu.push({ + title: _l("Archived Histories"), + url: "/histories/archived", + }); if (Galaxy.config.enable_notification_system) { userTab.menu.push({ title: _l("Notifications"), url: "/user/notifications", }); } + userTab.menu.push({ + title: _l("Workflow Invocations"), + url: "/workflows/invocations", + }); userTab.menu.push({ divider: true }); + userTab.menu.push({ + title: _l("Preferences"), + url: "/user", + }); userTab.menu.push({ title: _l("Sign Out"), onclick: userLogout, diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index bd999611ef2d..b927bf6dffaa 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -7,9 +7,6 @@ import DatasetAttributes from "components/DatasetInformation/DatasetAttributes"; import DatasetDetails from "components/DatasetInformation/DatasetDetails"; import DatasetError from "components/DatasetInformation/DatasetError"; import FormGeneric from "components/Form/FormGeneric"; -import visualizationsGridConfig from "components/Grid/configs/visualizations"; -import visualizationsPublishedGridConfig from "components/Grid/configs/visualizationsPublished"; -import GridList from "components/Grid/GridList"; import HistoryExportTasks from "components/History/Export/HistoryExport"; import HistoryPublished from "components/History/HistoryPublished"; import HistoryView from "components/History/HistoryView"; @@ -66,6 +63,7 @@ import { parseBool } from "@/utils/utils"; import { patchRouterPush } from "./router-push"; import AboutGalaxy from "@/components/AboutGalaxy.vue"; +import GridVisualization from "@/components/Grid/GridVisualization.vue"; import HistoryArchive from "@/components/History/Archiving/HistoryArchive.vue"; import HistoryArchiveWizard from "@/components/History/Archiving/HistoryArchiveWizard.vue"; import HistoryDatasetPermissions from "@/components/History/HistoryDatasetPermissions.vue"; @@ -481,16 +479,16 @@ export function getRouter(Galaxy) { }, { path: "visualizations/list", - component: GridList, + component: GridVisualization, props: { - gridConfig: visualizationsGridConfig, + activeList: "my", }, }, { path: "visualizations/list_published", - component: GridList, + component: GridVisualization, props: { - gridConfig: visualizationsPublishedGridConfig, + activeList: "published", }, }, { From 61aff054df6b5ce4531d16a1148e200c42e0cb01 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 14:59:51 -0500 Subject: [PATCH 04/42] Alphabetically sort index endpoint parameters of pages --- lib/galaxy/schema/schema.py | 8 ++++---- lib/galaxy/webapps/galaxy/api/pages.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index 57f34405df0d..a2408772fc04 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -1459,14 +1459,14 @@ class InvocationIndexQueryPayload(Model): class PageIndexQueryPayload(Model): deleted: bool = False + limit: Optional[int] = Field(default=100, lt=1000, title="Limit", description="Maximum number of pages to return.") + offset: Optional[int] = Field(default=0, title="Offset", description="Number of pages to skip.") show_published: Optional[bool] = None show_shared: Optional[bool] = None - user_id: Optional[DecodedDatabaseIdField] = None + search: Optional[str] = Field(default=None, title="Filter text", description="Freetext to search.") sort_by: PageSortByEnum = Field("update_time", title="Sort By", description="Sort pages by this attribute.") sort_desc: Optional[bool] = Field(default=False, title="Sort descending", description="Sort in descending order.") - search: Optional[str] = Field(default=None, title="Filter text", description="Freetext to search.") - limit: Optional[int] = Field(default=100, lt=1000, title="Limit", description="Maximum number of pages to return.") - offset: Optional[int] = Field(default=0, title="Offset", description="Number of pages to skip.") + user_id: Optional[DecodedDatabaseIdField] = None class CreateHistoryPayload(Model): diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index 7606aaa53b47..16077b08591c 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -105,26 +105,26 @@ async def index( response: Response, trans: ProvidesUserContext = DependsOnTrans, deleted: bool = DeletedQueryParam, - user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, + limit: int = LimitQueryParam, + offset: int = OffsetQueryParam, + search: Optional[str] = SearchQueryParam, show_published: bool = ShowPublishedQueryParam, show_shared: bool = ShowSharedQueryParam, sort_by: PageSortByEnum = SortByQueryParam, sort_desc: bool = SortDescQueryParam, - limit: int = LimitQueryParam, - offset: int = OffsetQueryParam, - search: Optional[str] = SearchQueryParam, + user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, ) -> PageSummaryList: """Get a list with summary information of all Pages available to the user.""" payload = PageIndexQueryPayload.model_construct( deleted=deleted, - user_id=user_id, + limit=limit, + offset=offset, + search=search, show_published=show_published, show_shared=show_shared, sort_by=sort_by, sort_desc=sort_desc, - limit=limit, - offset=offset, - search=search, + user_id=user_id, ) pages, total_matches = self.service.index(trans, payload, include_total_count=True) response.headers["total_matches"] = str(total_matches) From 004f09efe63d1bb44221a2b8df1d01179b3d6ac3 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:01:03 -0500 Subject: [PATCH 05/42] Add show_own query parameter in alignment with visualizations index endpoint --- lib/galaxy/schema/schema.py | 1 + lib/galaxy/webapps/galaxy/api/pages.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index a2408772fc04..c598e3943cfd 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -1461,6 +1461,7 @@ class PageIndexQueryPayload(Model): deleted: bool = False limit: Optional[int] = Field(default=100, lt=1000, title="Limit", description="Maximum number of pages to return.") offset: Optional[int] = Field(default=0, title="Offset", description="Number of pages to skip.") + show_own: Optional[bool] = None show_published: Optional[bool] = None show_shared: Optional[bool] = None search: Optional[str] = Field(default=None, title="Filter text", description="Freetext to search.") diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index 16077b08591c..7cea098afdf3 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -52,6 +52,8 @@ title="Encoded user ID to restrict query to, must be own id if not an admin user", ) +ShowOwnQueryParam: bool = Query(default=True, title="Show pages owned by user.", description="") + ShowPublishedQueryParam: bool = Query(default=True, title="Include published pages.", description="") ShowSharedQueryParam: bool = Query(default=False, title="Include pages shared with authenticated user.", description="") @@ -108,6 +110,7 @@ async def index( limit: int = LimitQueryParam, offset: int = OffsetQueryParam, search: Optional[str] = SearchQueryParam, + show_own: bool = ShowOwnQueryParam, show_published: bool = ShowPublishedQueryParam, show_shared: bool = ShowSharedQueryParam, sort_by: PageSortByEnum = SortByQueryParam, @@ -120,6 +123,7 @@ async def index( limit=limit, offset=offset, search=search, + show_own=show_own, show_published=show_published, show_shared=show_shared, sort_by=sort_by, From 5db6d726cfcf95d09e455e0a11f321889fd27814 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:12:22 -0500 Subject: [PATCH 06/42] Align pages manager query to histories, and visualizations endpoint for consistency --- lib/galaxy/managers/pages.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 3157b026a4dd..fef8ed8a245c 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -143,36 +143,30 @@ def index_query( self, trans: ProvidesUserContext, payload: PageIndexQueryPayload, include_total_count: bool = False ) -> Tuple[sqlalchemy.engine.Result, int]: show_deleted = payload.deleted + show_own = payload.show_own + show_published = payload.show_published show_shared = payload.show_shared is_admin = trans.user_is_admin user = trans.user - if show_shared is None: - show_shared = not show_deleted - - if show_shared and show_deleted: - message = "show_shared and show_deleted cannot both be specified as true" + if not user: + message = "Requires user to log in." raise exceptions.RequestParameterInvalidException(message) - stmt = select(Page) - - if not is_admin: - filters = [Page.user == trans.user] - if payload.show_published: - filters.append(Page.published == true()) - if user and show_shared: - filters.append(PageUserShareAssociation.user == user) - stmt = stmt.outerjoin(Page.users_shared_with) - stmt = stmt.where(or_(*filters)) + stmt = select(self.model_class) - if not show_deleted: - stmt = stmt.where(Page.deleted == false()) - elif not is_admin: - # don't let non-admins see other user's deleted pages - stmt = stmt.where(or_(Page.deleted == false(), Page.user == user)) + filters = [] + if show_own or (not show_published and not is_admin): + filters = [self.model_class.user == user] + if show_published: + filters.append(self.model_class.published == true()) + if user and show_shared: + filters.append(self.user_share_model.user == user) + stmt = stmt.outerjoin(self.model_class.users_shared_with) + stmt = stmt.where(or_(*filters)) if payload.user_id: - stmt = stmt.where(Page.user_id == payload.user_id) + stmt = stmt.where(self.model_class.user_id == payload.user_id) if payload.search: search_query = payload.search @@ -222,6 +216,12 @@ def p_tag_filter(term_text: str, quoted: bool): term, ) ) + + if show_published and not is_admin: + show_deleted = False + + stmt = stmt.where(self.model_class.deleted == (true() if show_deleted else false())) + if include_total_count: total_matches = get_count(trans.sa_session, stmt) else: From 14b07038052605a7f6dbf5497761c05ac575f9df Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:17:16 -0500 Subject: [PATCH 07/42] Ensure that visualizations and pages index endpoints do not produce duplicate entries --- lib/galaxy/managers/pages.py | 2 +- lib/galaxy/managers/visualizations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index fef8ed8a245c..2e5bc651f825 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -220,7 +220,7 @@ def p_tag_filter(term_text: str, quoted: bool): if show_published and not is_admin: show_deleted = False - stmt = stmt.where(self.model_class.deleted == (true() if show_deleted else false())) + stmt = stmt.where(self.model_class.deleted == (true() if show_deleted else false())).distinct() if include_total_count: total_matches = get_count(trans.sa_session, stmt) diff --git a/lib/galaxy/managers/visualizations.py b/lib/galaxy/managers/visualizations.py index 9aa2808f147f..c6fe3f365b74 100644 --- a/lib/galaxy/managers/visualizations.py +++ b/lib/galaxy/managers/visualizations.py @@ -155,7 +155,7 @@ def p_tag_filter(term_text: str, quoted: bool): if show_published and not is_admin: show_deleted = False - query = query.filter(self.model_class.deleted == (true() if show_deleted else false())) + query = query.filter(self.model_class.deleted == (true() if show_deleted else false())).distinct() if include_total_count: total_matches = query.count() From 7e55b6aec5f5e381fbe9a01a43b6aad864a25a01 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:22:20 -0500 Subject: [PATCH 08/42] Allow to sort pages by create time --- lib/galaxy/schema/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index c598e3943cfd..7a64cc50c652 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -1454,7 +1454,7 @@ class InvocationIndexQueryPayload(Model): offset: Optional[int] = Field(default=0, description="Number of invocations to skip") -PageSortByEnum = Literal["update_time", "title", "username"] +PageSortByEnum = Literal["create_time", "title", "update_time", "username"] class PageIndexQueryPayload(Model): From 4e6682ba554d292f903df1dc5ab3a06a37e1f743 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:24:11 -0500 Subject: [PATCH 09/42] Update client api schema --- client/src/api/schema/schema.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 75c9b078677c..1eeb526466d7 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -18578,8 +18578,6 @@ export interface operations { */ parameters?: { /** @description Whether to include deleted pages in the result. */ - /** @description Sort page index by this specified attribute on the page model */ - /** @description Sort in descending order? */ /** * @description A mix of free text and GitHub-style tags used to filter the index operation. * @@ -18616,16 +18614,19 @@ export interface operations { * Free text search terms will be searched against the following attributes of the * Pages: `title`, `slug`, `tag`, `user`. */ + /** @description Sort page index by this specified attribute on the page model */ + /** @description Sort in descending order? */ query?: { deleted?: boolean; - user_id?: string | null; - show_published?: boolean; - show_shared?: boolean; - sort_by?: "update_time" | "title" | "username"; - sort_desc?: boolean; limit?: number; offset?: number; search?: string | null; + show_own?: boolean; + show_published?: boolean; + show_shared?: boolean; + sort_by?: "create_time" | "title" | "update_time" | "username"; + sort_desc?: boolean; + user_id?: string | null; }; /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ header?: { From 3dc2f23555082a9c191cc0a839beb85d407ec92e Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:38:01 -0500 Subject: [PATCH 10/42] Add page grid config --- client/src/components/Grid/configs/pages.ts | 186 ++++++++++++++++++++ client/src/entry/analysis/router.js | 7 + 2 files changed, 193 insertions(+) create mode 100644 client/src/components/Grid/configs/pages.ts diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts new file mode 100644 index 000000000000..517a812f78e9 --- /dev/null +++ b/client/src/components/Grid/configs/pages.ts @@ -0,0 +1,186 @@ +import { faEdit, faEye, faPlus, faShareAlt, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { useEventBus } from "@vueuse/core"; + +import { fetcher } from "@/api/schema"; +import { getGalaxyInstance } from "@/app"; +import Filtering, { contains, equals, expandNameTag, toBool, type ValidFilter } from "@/utils/filtering"; +import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error"; + +import type { ActionArray, FieldArray, GridConfig } from "./types"; + +const { emit } = useEventBus("grid-router-push"); + +/** + * Api endpoint handlers + */ +const getPages = fetcher.path("/api/pages").method("get").create(); +const deletePage = fetcher.path("/api/pages/{id}").method("delete").create(); + +/** + * Local types + */ +type SortKeyLiteral = "create_time" | "title" | "update_time" | "username" | undefined; +type PageEntry = Record; + +/** + * Request and return data from server + */ +async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { + // TODO: Avoid using Galaxy instance to identify current user + const Galaxy = getGalaxyInstance(); + const userId = !Galaxy.isAnonymous && Galaxy.user.id; + if (!userId) { + rethrowSimple("Please login to access this page."); + } + const { data, headers } = await getPages({ + limit, + offset, + search, + sort_by: sort_by as SortKeyLiteral, + sort_desc, + show_published: false, + user_id: userId, + }); + const totalMatches = parseInt(headers.get("total_matches") ?? "0"); + return [data, totalMatches]; +} + +/** + * Actions are grid-wide operations + */ +const actions: ActionArray = [ + { + title: "Create", + icon: faPlus, + handler: () => { + emit("/pages/create"); + }, + }, +]; + +/** + * Declare columns to be displayed + */ +const fields: FieldArray = [ + { + title: "Title", + key: "title", + type: "operations", + width: 40, + operations: [ + { + title: "View", + icon: faEye, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/published/page?id=${data.id}`); + }, + }, + { + title: "Edit Content", + icon: faEdit, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/pages/edit?id=${data.id}`); + }, + }, + { + title: "Edit Content", + icon: faEdit, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/pages/editor?id=${data.id}`); + }, + }, + { + title: "Share and Publish", + icon: faShareAlt, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/pages/sharing?id=${data.id}`); + }, + }, + { + title: "Delete", + icon: faTrash, + condition: (data: PageEntry) => !data.deleted, + handler: async (data: PageEntry) => { + try { + await deletePage({ id: String(data.id) }); + return { + status: "success", + message: `'${data.title}' has been deleted.`, + }; + } catch (e) { + return { + status: "danger", + message: `Failed to delete '${data.title}': ${errorMessageAsString(e)}.`, + }; + } + }, + }, + ], + }, + { + key: "create_time", + title: "Created", + type: "date", + }, + { + key: "update_time", + title: "Updated", + type: "date", + }, + { + key: "sharing", + title: "Status", + type: "sharing", + }, +]; + +/** + * Declare filter options + */ +const validFilters: Record> = { + title: { placeholder: "title", type: String, handler: contains("title"), menuItem: true }, + slug: { handler: contains("slug"), menuItem: false }, + published: { + placeholder: "Filter on published pages", + type: Boolean, + boolType: "is", + handler: equals("published", "published", toBool), + menuItem: true, + }, + importable: { + placeholder: "Filter on importable pages", + type: Boolean, + boolType: "is", + handler: equals("importable", "importable", toBool), + menuItem: true, + }, + deleted: { + placeholder: "Filter on deleted pages", + type: Boolean, + boolType: "is", + handler: equals("deleted", "deleted", toBool), + menuItem: true, + }, +}; + +/** + * Grid configuration + */ +const gridConfig: GridConfig = { + id: "pages-grid", + actions: actions, + fields: fields, + filtering: new Filtering(validFilters, undefined, false, false), + getData: getData, + plural: "Pages", + sortBy: "update_time", + sortDesc: true, + sortKeys: ["create_time", "title", "update_time"], + title: "Saved Pages", +}; + +export default gridConfig; diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index b927bf6dffaa..5ce457d5a8b0 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -369,6 +369,13 @@ export function getRouter(Galaxy) { modelClass: "Page", }), }, + { + path: "pages/list", + component: GridList, + props: { + gridConfig: pagesGridConfig, + }, + }, { path: "pages/:actionId", component: PageList, From 1970df134c6d920a6f1da22f686a434e6552b11a Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:42:38 -0500 Subject: [PATCH 11/42] Add grid config for published pages --- .../components/Grid/configs/pagesPublished.ts | 121 ++++++++++++++++++ client/src/entry/analysis/router.js | 10 +- 2 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 client/src/components/Grid/configs/pagesPublished.ts diff --git a/client/src/components/Grid/configs/pagesPublished.ts b/client/src/components/Grid/configs/pagesPublished.ts new file mode 100644 index 000000000000..c0135d39bb9f --- /dev/null +++ b/client/src/components/Grid/configs/pagesPublished.ts @@ -0,0 +1,121 @@ +import { faEye, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useEventBus } from "@vueuse/core"; + +import { fetcher } from "@/api/schema"; +import { getGalaxyInstance } from "@/app"; +import Filtering, { contains, type ValidFilter } from "@/utils/filtering"; +import { rethrowSimple } from "@/utils/simple-error"; + +import type { ActionArray, FieldArray, GridConfig } from "./types"; + +const { emit } = useEventBus("grid-router-push"); + +/** + * Api endpoint handlers + */ +const getPages = fetcher.path("/api/pages").method("get").create(); + +/** + * Local types + */ +type SortKeyLiteral = "create_time" | "title" | "update_time" | "username" | undefined; +type PageEntry = Record; + +/** + * Request and return data from server + */ +async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { + // TODO: Avoid using Galaxy instance to identify current user + const Galaxy = getGalaxyInstance(); + const userId = !Galaxy.isAnonymous && Galaxy.user.id; + if (!userId) { + rethrowSimple("Please login to access this page."); + } + const { data, headers } = await getPages({ + limit, + offset, + search, + sort_by: sort_by as SortKeyLiteral, + sort_desc, + show_published: false, + user_id: userId, + }); + const totalMatches = parseInt(headers.get("total_matches") ?? "0"); + return [data, totalMatches]; +} + +/** + * Actions are grid-wide operations + */ +const actions: ActionArray = [ + { + title: "Create", + icon: faPlus, + handler: () => { + emit("/pages/create"); + }, + }, +]; + +/** + * Declare columns to be displayed + */ +const fields: FieldArray = [ + { + title: "Title", + key: "title", + type: "operations", + width: 40, + operations: [ + { + title: "View", + icon: faEye, + condition: (data: PageEntry) => !data.deleted, + handler: (data: PageEntry) => { + emit(`/published/page?id=${data.id}`); + }, + }, + ], + }, + { + key: "create_time", + title: "Created", + type: "date", + }, + { + key: "update_time", + title: "Updated", + type: "date", + }, + { + key: "owner", + title: "Owner", + type: "text", + }, +]; + +/** + * Declare filter options + */ +const validFilters: Record> = { + title: { placeholder: "title", type: String, handler: contains("title"), menuItem: true }, + slug: { handler: contains("slug"), menuItem: false }, +}; + +/** + * Grid configuration + */ +const gridConfig: GridConfig = { + id: "pages-published-grid", + actions: actions, + fields: fields, + filtering: new Filtering(validFilters, undefined, false, false), + getData: getData, + plural: "Pages", + sortBy: "update_time", + sortDesc: true, + sortKeys: ["create_time", "title", "update_time"], + title: "Published Pages", +}; + +export default gridConfig; diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 5ce457d5a8b0..b0383cb79243 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -377,11 +377,11 @@ export function getRouter(Galaxy) { }, }, { - path: "pages/:actionId", - component: PageList, - props: (route) => ({ - published: route.params.actionId == "list_published" ? true : false, - }), + path: "pages/list_published", + component: GridList, + props: { + gridConfig: pagesPublishedGridConfig, + }, }, { path: "storage/history/:historyId", From 067de0f4b58094b95ce1ba1b6d64b2ad9bef838f Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:43:34 -0500 Subject: [PATCH 12/42] Remove previous custom page grids --- .../src/components/Page/PageDropdown.test.ts | 193 ----------- client/src/components/Page/PageDropdown.vue | 97 ------ .../components/Page/PageIndexActions.test.ts | 32 -- .../src/components/Page/PageIndexActions.vue | 22 -- client/src/components/Page/PageList.test.js | 324 ------------------ client/src/components/Page/PageList.vue | 320 ----------------- 6 files changed, 988 deletions(-) delete mode 100644 client/src/components/Page/PageDropdown.test.ts delete mode 100644 client/src/components/Page/PageDropdown.vue delete mode 100644 client/src/components/Page/PageIndexActions.test.ts delete mode 100644 client/src/components/Page/PageIndexActions.vue delete mode 100644 client/src/components/Page/PageList.test.js delete mode 100644 client/src/components/Page/PageList.vue diff --git a/client/src/components/Page/PageDropdown.test.ts b/client/src/components/Page/PageDropdown.test.ts deleted file mode 100644 index 4a4be6327a33..000000000000 --- a/client/src/components/Page/PageDropdown.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import "jest-location-mock"; - -import { expect, jest } from "@jest/globals"; -import { createTestingPinia } from "@pinia/testing"; -import { createWrapper, mount, shallowMount, Wrapper } from "@vue/test-utils"; -import { createPinia, PiniaVuePlugin } from "pinia"; -import { getLocalVue } from "tests/jest/helpers"; - -import { mockFetcher } from "@/api/schema/__mocks__"; -import { useUserStore } from "@/stores/userStore"; - -import PageDropdown from "./PageDropdown.vue"; - -jest.mock("@/api/schema"); - -const waitRAF = () => new Promise((resolve) => requestAnimationFrame(resolve)); - -const localVue = getLocalVue(true); -localVue.use(PiniaVuePlugin); - -const PAGE_DATA_OWNED = { - id: "page1235", - title: "My Page Title", - description: "A description derived from an annotation.", - shared: false, -}; - -const PAGE_DATA_SHARED = { - id: "page1235", - title: "My Page Title", - description: "A description derived from an annotation.", - shared: true, -}; - -describe("PageDropdown.vue", () => { - let wrapper: Wrapper; - - function pageOptions() { - return wrapper.findAll(".dropdown-menu .dropdown-item"); - } - - describe("navigation on owned pages", () => { - beforeEach(async () => { - const pinia = createPinia(); - const propsData = { - root: "/rootprefix/", - page: PAGE_DATA_OWNED, - }; - wrapper = shallowMount(PageDropdown, { - propsData, - localVue, - pinia: pinia, - stubs: { - RouterLink: true, - }, - }); - const userStore = useUserStore(); - userStore.currentUser = { - email: "my@email", - id: "1", - tags_used: [], - isAnonymous: false, - total_disk_usage: 1048576, - }; - }); - - it("should show page title", async () => { - const titleWrapper = await wrapper.find(".page-title"); - expect(titleWrapper.text()).toBe("My Page Title"); - }); - - it("should decorate dropdown with page ID for automation", async () => { - const linkWrapper = await wrapper.find("[data-page-dropdown='page1235']"); - expect(linkWrapper.exists()).toBeTruthy(); - }); - - it("should have a 'Share' option", async () => { - expect(wrapper.find(".dropdown-menu .dropdown-item-share").exists()).toBeTruthy(); - }); - - it("should provide 5 options", () => { - expect(pageOptions().length).toBe(5); - }); - }); - - describe("navigation on shared pages", () => { - beforeEach(async () => { - const propsData = { - root: "/rootprefixshared/", - page: PAGE_DATA_SHARED, - }; - wrapper = shallowMount(PageDropdown, { - propsData, - localVue, - pinia: createTestingPinia(), - stubs: { - RouterLink: true, - }, - }); - }); - - it("should have the 'View' option", async () => { - expect(wrapper.find(".dropdown-menu .dropdown-item-view").exists()).toBeTruthy(); - }); - - it("should have only single option", () => { - expect(pageOptions().length).toBe(1); - }); - }); - - describe("clicking page deletion on owned page", () => { - const pinia = createPinia(); - - async function mountAndDelete({ confirm = true } = {}) { - const propsData = { - root: "/rootprefixdelete/", - page: PAGE_DATA_OWNED, - }; - wrapper = mount(PageDropdown, { - propsData, - localVue, - pinia: pinia, - stubs: { - transition: false, - RouterLink: true, - }, - }); - const userStore = useUserStore(); - userStore.currentUser = { - email: "my@email", - id: "1", - tags_used: [], - isAnonymous: false, - total_disk_usage: 1048576, - }; - wrapper.find(".page-dropdown").trigger("click"); - await wrapper.vm.$nextTick(); - wrapper.find(".dropdown-item-delete").trigger("click"); - - if (confirm) { - await confirmDeletion(); - } - } - - async function confirmDeletion() { - // this is here because b-modal is lazy loading and portalling - // see https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/modal/modal.spec.js#L233 - await wrapper.vm.$nextTick(); - await waitRAF(); - await wrapper.vm.$nextTick(); - await waitRAF(); - await wrapper.vm.$nextTick(); - await waitRAF(); - const foot: any = document.getElementById("delete-page-modal-page1235___BV_modal_footer_"); - createWrapper(foot).find(".btn-primary").trigger("click"); - await wrapper.vm.$nextTick(); - await waitRAF(); - await wrapper.vm.$nextTick(); - await waitRAF(); - } - - afterEach(() => { - mockFetcher.clearMocks(); - }); - - it("should fire deletion API request upon confirmation", async () => { - mockFetcher.path("/api/pages/{id}").method("delete").mock({ status: 204 }); - await mountAndDelete(); - const emitted = wrapper.emitted(); - expect(emitted["onRemove"]?.[0]?.[0]).toEqual("page1235"); - expect(emitted["onSuccess"]).toBeTruthy(); - }); - - it("should emit an error on API fail", async () => { - mockFetcher - .path("/api/pages/{id}") - .method("delete") - .mock(() => { - throw Error("mock error"); - }); - await mountAndDelete(); - const emitted = wrapper.emitted(); - expect(emitted["onError"]).toBeTruthy(); - }); - - it("should not fire deletion API request if not confirmed", async () => { - await mountAndDelete({ confirm: false }); - const emitted = wrapper.emitted(); - expect(emitted["onRemove"]).toBeFalsy(); - expect(emitted["onSuccess"]).toBeFalsy(); - }); - }); -}); diff --git a/client/src/components/Page/PageDropdown.vue b/client/src/components/Page/PageDropdown.vue deleted file mode 100644 index 270a9168648a..000000000000 --- a/client/src/components/Page/PageDropdown.vue +++ /dev/null @@ -1,97 +0,0 @@ - - diff --git a/client/src/components/Page/PageIndexActions.test.ts b/client/src/components/Page/PageIndexActions.test.ts deleted file mode 100644 index 48bf76b2b2af..000000000000 --- a/client/src/components/Page/PageIndexActions.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import "jest-location-mock"; - -import { shallowMount } from "@vue/test-utils"; -import { getLocalVue } from "tests/jest/helpers"; - -import PageIndexActions from "./PageIndexActions.vue"; - -const localVue = getLocalVue(); - -describe("PageIndexActions.vue", () => { - let wrapper: any; - const mockRouter = { - push: jest.fn(), - }; - - beforeEach(async () => { - wrapper = shallowMount(PageIndexActions, { - mocks: { - $router: mockRouter, - }, - localVue, - }); - }); - - describe("navigation", () => { - it("should create a page when create is clicked", async () => { - await wrapper.find("#page-create").trigger("click"); - expect(mockRouter.push).toHaveBeenCalledTimes(1); - expect(mockRouter.push).toHaveBeenCalledWith("/pages/create"); - }); - }); -}); diff --git a/client/src/components/Page/PageIndexActions.vue b/client/src/components/Page/PageIndexActions.vue deleted file mode 100644 index b4563d38a521..000000000000 --- a/client/src/components/Page/PageIndexActions.vue +++ /dev/null @@ -1,22 +0,0 @@ - - diff --git a/client/src/components/Page/PageList.test.js b/client/src/components/Page/PageList.test.js deleted file mode 100644 index ef61d1f07397..000000000000 --- a/client/src/components/Page/PageList.test.js +++ /dev/null @@ -1,324 +0,0 @@ -import { createTestingPinia } from "@pinia/testing"; -import { mount } from "@vue/test-utils"; -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { formatDistanceToNow, parseISO } from "date-fns"; -import flushPromises from "flush-promises"; -import { PiniaVuePlugin } from "pinia"; -import { useUserStore } from "stores/userStore"; -import { getLocalVue, wait } from "tests/jest/helpers"; - -import PageList from "./PageList.vue"; - -jest.mock("app"); - -const localVue = getLocalVue(); -localVue.use(PiniaVuePlugin); - -const debounceDelay = 500; // in ms - see FilterMenu.props.debounceDelay - -describe("PgeList.vue", () => { - let axiosMock; - let wrapper; - - beforeEach(async () => { - axiosMock = new MockAdapter(axios); - }); - - afterEach(() => { - axiosMock.restore(); - }); - - const personalGridApiParams = { - limit: 20, - offset: 0, - sort_by: "update_time", - sort_desc: true, - show_published: false, - show_shared: true, - }; - const publishedGridApiParams = { - ...personalGridApiParams, - show_published: true, - search: "is:published", - show_shared: false, - }; - const publishedGridApiParamsSortTitleAsc = { - sort_by: "title", - limit: 20, - offset: 0, - search: "is:published", - show_published: true, - show_shared: false, - }; - const publishedGridApiParamsSortTitleDesc = { - ...publishedGridApiParamsSortTitleAsc, - sort_desc: true, - }; - - const propsDataPersonalGrid = { - inputDebounceDelay: 0, - published: false, - }; - const propsDataPublishedGrid = { - ...propsDataPersonalGrid, - published: true, - }; - - const privatePage = { - id: "5f1915bcf9f3561a", - update_time: "2023-05-23T17:51:51.069761", - create_time: "2023-01-04T17:40:58.407793", - deleted: false, - importable: false, - published: false, - slug: "raynor-here", - tags: ["tagThis", "tagIs", "tagJimmy"], - title: "jimmy's page", - username: "jimmyPage", - }; - const publishedPage = { - ...privatePage, - id: "5f1915bcf9f3561b", - published: true, - importable: true, - update_time: "2023-05-25T17:51:51.069761", - }; - const pageA = { - id: "5f1915bcf9f3561c", - update_time: "2023-05-21T17:51:51.069761", - create_time: "2023-01-04T17:40:58.407793", - deleted: false, - importable: true, - published: true, - slug: "a-page", - title: "a page title", - username: "APageUser", - }; - const mockPrivatePageData = [privatePage]; - const mockPublishedPageData = [publishedPage]; - const mockTwoPageData = [privatePage, pageA]; - - function mountGrid(propsData) { - const pinia = createTestingPinia(); - const userStore = useUserStore(); - userStore.currentUser = { username: "jimmyPage", tags_used: [] }; - - wrapper = mount(PageList, { - propsData, - localVue, - pinia, - stubs: { - icon: { template: "
" }, - }, - }); - } - - function mountPersonalGrid() { - mountGrid(propsDataPersonalGrid); - } - - function mountPublishedGrid() { - mountGrid(propsDataPublishedGrid); - } - - describe(" with empty page list", () => { - beforeEach(async () => { - axiosMock.onAny().reply(200, [], { total_matches: "0" }); - mountPersonalGrid(); - }); - - it("title should be shown", async () => { - expect(wrapper.find("#pages-title").text()).toBe("Pages"); - }); - - it("no invocations message should be shown when not loading", async () => { - expect(wrapper.find("#no-pages").exists()).toBe(true); - }); - }); - describe("with server error", () => { - beforeEach(async () => { - axiosMock.onAny().reply(403, { err_msg: "this is a problem" }); - mountPersonalGrid(); - }); - - it("renders error message", async () => { - expect(wrapper.find(".index-grid-message").text()).toContain("this is a problem"); - }); - }); - - describe("with single private page", () => { - function findSearchBar(wrapper) { - return wrapper.find("[data-description='filter text input']"); - } - - beforeEach(async () => { - axiosMock - .onGet("/api/pages", { params: { search: "", ...personalGridApiParams } }) - .reply(200, mockPrivatePageData, { total_matches: "1" }); - jest.spyOn(PageList.methods, "decorateData").mockImplementation((page) => { - page.shared = false; - }); - mountPersonalGrid(); - await flushPromises(); - }); - - it("'no pages' message is gone", async () => { - expect(wrapper.find("#no-pages").exists()).toBe(false); - }); - - it("renders one row with correct sharing options", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - expect(rows.length).toBe(1); - const row = rows[0]; - const columns = row.findAll("td"); - expect(columns.at(0).text()).toContain("jimmy's page"); - expect(columns.at(1).text()).toContain("tagThis"); - expect(columns.at(1).text()).toContain("tagIs"); - expect(columns.at(1).text()).toContain("tagJimmy"); - expect(columns.at(3).text()).toBe( - formatDistanceToNow(parseISO(`${mockPrivatePageData[0].update_time}Z`), { addSuffix: true }) - ); - expect(row.find(".share-this-page").exists()).toBe(true); - expect(row.find(".sharing-indicator-published").exists()).toBe(false); - expect(row.find(".sharing-indicator-importable").exists()).toBe(false); - expect(row.find(".sharing-indicator-shared").exists()).toBe(false); - }); - - it("starts with an empty filter", async () => { - expect(findSearchBar(wrapper).element.value).toBe(""); - }); - - it("fetches filtered results when search filter is used", async () => { - await findSearchBar(wrapper).setValue("mytext"); - expect(findSearchBar(wrapper).element.value).toBe("mytext"); - await wait(debounceDelay); - expect(wrapper.vm.filterText).toBe("mytext"); - }); - - it("updates filter when a tag is clicked", async () => { - const tags = wrapper.findAll("tbody > tr .tag").wrappers; - expect(tags.length).toBe(3); - tags[0].trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("tag:'tagThis'"); - }); - - it("updates filter when a tag is clicked only on the first click", async () => { - const tags = wrapper.findAll("tbody > tr .tag").wrappers; - expect(tags.length).toBe(3); - tags[0].trigger("click"); - tags[0].trigger("click"); - tags[0].trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("tag:'tagThis'"); - }); - }); - describe("with single published and importable page on personal grid", () => { - beforeEach(async () => { - axiosMock - .onGet("/api/pages", { params: { search: "", ...personalGridApiParams } }) - .reply(200, mockPublishedPageData, { total_matches: "1" }); - jest.spyOn(PageList.methods, "decorateData").mockImplementation((page) => { - page.shared = true; - }); - mountPersonalGrid(); - await flushPromises(); - }); - it("updates filter when published icon is clicked", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - const row = rows[0]; - row.find(".sharing-indicator-published").trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("is:published"); - }); - - it("updates filter when shared with me icon is clicked", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - const row = rows[0]; - row.find(".sharing-indicator-shared").trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("is:shared_with_me"); - }); - - it("updates filter when importable icon is clicked", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - const row = rows[0]; - row.find(".sharing-indicator-importable").trigger("click"); - await flushPromises(); - expect(wrapper.vm.filterText).toBe("is:importable"); - }); - }); - describe("with single page on published grid", () => { - beforeEach(async () => { - axiosMock - .onGet("/api/pages", { params: publishedGridApiParams }) - .reply(200, mockPublishedPageData, { total_matches: "1" }); - jest.spyOn(PageList.methods, "decorateData").mockImplementation((page) => { - page.shared = false; - }); - mountPublishedGrid(); - await flushPromises(); - }); - - it("renders one row with correct sharing options", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - expect(rows.length).toBe(1); - const row = rows[0]; - const columns = row.findAll("td"); - expect(columns.at(0).text()).toContain("jimmy's page"); - expect(columns.at(1).text()).toContain("tagThis"); - expect(columns.at(1).text()).toContain("tagIs"); - expect(columns.at(1).text()).toContain("tagJimmy"); - expect(columns.at(2).text()).toBe("jimmyPage"); - expect(columns.at(3).text()).toBe( - formatDistanceToNow(parseISO(`${mockPublishedPageData[0].update_time}Z`), { addSuffix: true }) - ); - expect(row.find(".share-this-page").exists()).toBe(false); - expect(row.find(".sharing-indicator-published").exists()).toBe(false); - expect(row.find(".sharing-indicator-importable").exists()).toBe(false); - expect(row.find(".sharing-indicator-shared").exists()).toBe(false); - }); - }); - describe("with two pages on published grid", () => { - beforeEach(async () => { - axiosMock - .onGet("/api/pages", { params: publishedGridApiParams }) - .reply(200, mockTwoPageData, { total_matches: "2" }); - axiosMock - .onGet("/api/pages", { params: publishedGridApiParamsSortTitleAsc }) - .reply(200, [pageA, privatePage], { total_matches: "2" }); - axiosMock - .onGet("/api/pages", { params: publishedGridApiParamsSortTitleDesc }) - .reply(200, mockTwoPageData, { total_matches: "2" }); - jest.spyOn(PageList.methods, "decorateData").mockImplementation((page) => { - page.shared = false; - }); - mountPublishedGrid(); - await flushPromises(); - }); - - it("should render both rows", async () => { - const rows = wrapper.findAll("tbody > tr").wrappers; - expect(rows.length).toBe(2); - }); - - it("should sort asc/desc when title column is clicked", async () => { - let firstRowColumns = wrapper.findAll("tbody > tr").wrappers[0].findAll("td"); - expect(firstRowColumns.at(0).text()).toContain("jimmy's page"); - const titleColumn = wrapper.findAll("th").wrappers[0]; - // default sort is by update_time - expect(titleColumn.attributes("aria-sort")).toBe("none"); - await titleColumn.trigger("click"); - await flushPromises(); - expect(titleColumn.attributes("aria-sort")).toBe("ascending"); - firstRowColumns = wrapper.findAll("tbody > tr").wrappers[0].findAll("td"); - expect(firstRowColumns.at(0).text()).toContain("a page title"); - await titleColumn.trigger("click"); - await flushPromises(); - expect(titleColumn.attributes("aria-sort")).toBe("descending"); - firstRowColumns = wrapper.findAll("tbody > tr").wrappers[0].findAll("td"); - expect(firstRowColumns.at(0).text()).toContain("jimmy's page"); - }); - }); -}); diff --git a/client/src/components/Page/PageList.vue b/client/src/components/Page/PageList.vue deleted file mode 100644 index 2f941d190b5a..000000000000 --- a/client/src/components/Page/PageList.vue +++ /dev/null @@ -1,320 +0,0 @@ - - From 9fc5c191094038dfb2fd635abbe563f116782767 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 15:45:03 -0500 Subject: [PATCH 13/42] Remove unused import, fix username in published grid config --- client/src/components/Grid/configs/pagesPublished.ts | 2 +- client/src/entry/analysis/router.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/components/Grid/configs/pagesPublished.ts b/client/src/components/Grid/configs/pagesPublished.ts index c0135d39bb9f..3db352182da3 100644 --- a/client/src/components/Grid/configs/pagesPublished.ts +++ b/client/src/components/Grid/configs/pagesPublished.ts @@ -88,7 +88,7 @@ const fields: FieldArray = [ type: "date", }, { - key: "owner", + key: "username", title: "Owner", type: "text", }, diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index b0383cb79243..62dad38f802b 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -17,7 +17,6 @@ import InteractiveTools from "components/InteractiveTools/InteractiveTools"; import JobDetails from "components/JobInformation/JobDetails"; import CarbonEmissionsCalculations from "components/JobMetrics/CarbonEmissions/CarbonEmissionsCalculations"; import NewUserWelcome from "components/NewUserWelcome/NewUserWelcome"; -import PageList from "components/Page/PageList"; import PageDisplay from "components/PageDisplay/PageDisplay"; import PageEditor from "components/PageEditor/PageEditor"; import ToolSuccess from "components/Tool/ToolSuccess"; From e0989ba911fd6fba439531489f4f8941b0ca879f Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 16:01:17 -0500 Subject: [PATCH 14/42] Use grid page to display page grids, remove redundant masthead entry --- client/src/entry/analysis/menu.js | 4 ---- client/src/entry/analysis/router.js | 9 +++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/client/src/entry/analysis/menu.js b/client/src/entry/analysis/menu.js index d71b8ccdb060..13b07b8912d7 100644 --- a/client/src/entry/analysis/menu.js +++ b/client/src/entry/analysis/menu.js @@ -76,10 +76,6 @@ export function fetchMenu(options = {}) { title: _l("Pages"), url: "/pages/list", }, - { - title: _l("Published Pages"), - url: "/pages/list_published", - }, { title: _l("Visualizations"), url: "/visualizations/list", diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 62dad38f802b..e25f0dfcdbf8 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -57,6 +57,7 @@ import VueRouter from "vue-router"; import AvailableDatatypes from "@/components/AvailableDatatypes/AvailableDatatypes"; import GridHistory from "@/components/Grid/GridHistory"; +import GridPage from "@/components/Grid/GridPage"; import { parseBool } from "@/utils/utils"; import { patchRouterPush } from "./router-push"; @@ -370,16 +371,16 @@ export function getRouter(Galaxy) { }, { path: "pages/list", - component: GridList, + component: GridPage, props: { - gridConfig: pagesGridConfig, + activeList: "my", }, }, { path: "pages/list_published", - component: GridList, + component: GridPage, props: { - gridConfig: pagesPublishedGridConfig, + activeList: "published", }, }, { From 45efadb08bf88602486909291daba6dd57257dab Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 16:09:46 -0500 Subject: [PATCH 15/42] Adjust published page query request --- client/src/components/Grid/configs/pagesPublished.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/components/Grid/configs/pagesPublished.ts b/client/src/components/Grid/configs/pagesPublished.ts index 3db352182da3..d1db10997105 100644 --- a/client/src/components/Grid/configs/pagesPublished.ts +++ b/client/src/components/Grid/configs/pagesPublished.ts @@ -37,8 +37,9 @@ async function getData(offset: number, limit: number, search: string, sort_by: s search, sort_by: sort_by as SortKeyLiteral, sort_desc, - show_published: false, - user_id: userId, + show_own: false, + show_published: true, + show_shared: true, }); const totalMatches = parseInt(headers.get("total_matches") ?? "0"); return [data, totalMatches]; From c1ec4972dd81a066b6a8fec93a80fea52a26138a Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 16:17:03 -0500 Subject: [PATCH 16/42] Remove unused import from pages grid --- client/src/components/Grid/configs/pages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts index 517a812f78e9..ca54cd4fa492 100644 --- a/client/src/components/Grid/configs/pages.ts +++ b/client/src/components/Grid/configs/pages.ts @@ -3,7 +3,7 @@ import { useEventBus } from "@vueuse/core"; import { fetcher } from "@/api/schema"; import { getGalaxyInstance } from "@/app"; -import Filtering, { contains, equals, expandNameTag, toBool, type ValidFilter } from "@/utils/filtering"; +import Filtering, { contains, equals, toBool, type ValidFilter } from "@/utils/filtering"; import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error"; import type { ActionArray, FieldArray, GridConfig } from "./types"; From c4e8a5e72071488492ac1b678b746853da9507fe Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 16:19:49 -0500 Subject: [PATCH 17/42] Display grid actions in outline variant for consistency --- client/src/components/Grid/GridList.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Grid/GridList.vue b/client/src/components/Grid/GridList.vue index 4a04eb5133c0..8d82873f6ea7 100644 --- a/client/src/components/Grid/GridList.vue +++ b/client/src/components/Grid/GridList.vue @@ -250,7 +250,7 @@ watch(operationMessage, () => { :key="actionIndex" class="m-1" size="sm" - variant="primary" + variant="outline-primary" :data-description="`grid action ${action.title.toLowerCase()}`" @click="action.handler()"> From ca6147030f88a2a8efc14d235231cfc13b2ae174 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 16:28:25 -0500 Subject: [PATCH 18/42] Add history grid action to import new histories --- client/src/components/Grid/GridHistory.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/components/Grid/GridHistory.vue b/client/src/components/Grid/GridHistory.vue index 58201aac4ff4..f99b2084a516 100644 --- a/client/src/components/Grid/GridHistory.vue +++ b/client/src/components/Grid/GridHistory.vue @@ -1,6 +1,6 @@ + + From f2c973099731c465e844adc1d0ab48bcf12a18b1 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 17 Feb 2024 16:32:22 -0500 Subject: [PATCH 20/42] Add visualization create action to visualization grid, remove unused icons --- client/src/components/Grid/GridHistory.vue | 4 ++-- client/src/components/Grid/GridPage.vue | 4 ++-- client/src/components/Grid/GridVisualization.vue | 10 ++++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/client/src/components/Grid/GridHistory.vue b/client/src/components/Grid/GridHistory.vue index f99b2084a516..0acc7c66653a 100644 --- a/client/src/components/Grid/GridHistory.vue +++ b/client/src/components/Grid/GridHistory.vue @@ -1,6 +1,6 @@ diff --git a/client/src/components/Grid/GridList.vue b/client/src/components/Grid/GridList.vue index 8d82873f6ea7..267cd0ec913f 100644 --- a/client/src/components/Grid/GridList.vue +++ b/client/src/components/Grid/GridList.vue @@ -259,7 +259,7 @@ watch(operationMessage, () => {
(), { diff --git a/client/src/components/Grid/GridVisualization.vue b/client/src/components/Grid/GridVisualization.vue index a34041355fcb..e8ab12a9488c 100644 --- a/client/src/components/Grid/GridVisualization.vue +++ b/client/src/components/Grid/GridVisualization.vue @@ -21,25 +21,23 @@ withDefaults(defineProps(), { From 2f5715815ce153c8196b610ef8d94cf225956bbc Mon Sep 17 00:00:00 2001 From: guerler Date: Sun, 18 Feb 2024 11:28:27 -0500 Subject: [PATCH 23/42] Align published resource dropdown for consistency --- .../Grid/configs/historiesPublished.ts | 1 - .../components/Grid/configs/pagesPublished.ts | 1 - .../Grid/configs/visualizationsPublished.ts | 22 ++++++++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/client/src/components/Grid/configs/historiesPublished.ts b/client/src/components/Grid/configs/historiesPublished.ts index 1c5163c5f2a2..61e80ed3506a 100644 --- a/client/src/components/Grid/configs/historiesPublished.ts +++ b/client/src/components/Grid/configs/historiesPublished.ts @@ -45,7 +45,6 @@ const fields: FieldArray = [ { title: "View", icon: faEye, - condition: (data: HistoryEntry) => !data.deleted, handler: (data: HistoryEntry) => { emit(`/histories/view?id=${data.id}`); }, diff --git a/client/src/components/Grid/configs/pagesPublished.ts b/client/src/components/Grid/configs/pagesPublished.ts index d1db10997105..0b34382ac36a 100644 --- a/client/src/components/Grid/configs/pagesPublished.ts +++ b/client/src/components/Grid/configs/pagesPublished.ts @@ -71,7 +71,6 @@ const fields: FieldArray = [ { title: "View", icon: faEye, - condition: (data: PageEntry) => !data.deleted, handler: (data: PageEntry) => { emit(`/published/page?id=${data.id}`); }, diff --git a/client/src/components/Grid/configs/visualizationsPublished.ts b/client/src/components/Grid/configs/visualizationsPublished.ts index 5c37ee06cdf9..b8888ffeb535 100644 --- a/client/src/components/Grid/configs/visualizationsPublished.ts +++ b/client/src/components/Grid/configs/visualizationsPublished.ts @@ -1,3 +1,5 @@ +import { faEye } from "@fortawesome/free-solid-svg-icons"; + import { fetcher } from "@/api/schema"; import Filtering, { contains, expandNameTag, type ValidFilter } from "@/utils/filtering"; import { withPrefix } from "@/utils/redirect"; @@ -40,11 +42,21 @@ const fields: FieldArray = [ { title: "Title", key: "title", - type: "link", - width: 30, - handler: (data: VisualizationEntry) => { - window.location.href = withPrefix(`/plugins/visualizations/${data.type}/saved?id=${data.id}`); - }, + type: "operations", + width: 40, + operations: [ + { + title: "View", + icon: faEye, + handler: (data: VisualizationEntry) => { + if (data.type === "trackster") { + window.location.href = withPrefix(`/visualization/${data.type}?id=${data.id}`); + } else { + window.location.href = withPrefix(`/plugins/visualizations/${data.type}/saved?id=${data.id}`); + } + }, + }, + ], }, { key: "annotation", From 1eb844bc40ce8aaef4f40c84d8d8face2417c277 Mon Sep 17 00:00:00 2001 From: guerler Date: Sun, 18 Feb 2024 16:10:46 -0500 Subject: [PATCH 24/42] Fix page attribute operation label --- client/src/components/Grid/configs/pages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts index ca54cd4fa492..ced5baa7f09f 100644 --- a/client/src/components/Grid/configs/pages.ts +++ b/client/src/components/Grid/configs/pages.ts @@ -77,7 +77,7 @@ const fields: FieldArray = [ }, }, { - title: "Edit Content", + title: "Edit Attributes", icon: faEdit, condition: (data: PageEntry) => !data.deleted, handler: (data: PageEntry) => { From d70a6dcee7e0b0e9a540101458b936a02037026f Mon Sep 17 00:00:00 2001 From: guerler Date: Sun, 18 Feb 2024 16:23:33 -0500 Subject: [PATCH 25/42] Allow pages to be filtered by deleted entries --- client/src/components/Grid/configs/pages.ts | 1 - lib/galaxy/managers/pages.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts index ced5baa7f09f..aed9025d1380 100644 --- a/client/src/components/Grid/configs/pages.ts +++ b/client/src/components/Grid/configs/pages.ts @@ -71,7 +71,6 @@ const fields: FieldArray = [ { title: "View", icon: faEye, - condition: (data: PageEntry) => !data.deleted, handler: (data: PageEntry) => { emit(`/published/page?id=${data.id}`); }, diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 2e5bc651f825..8438dcff695d 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -192,6 +192,8 @@ def p_tag_filter(term_text: str, quoted: bool): elif key == "user": stmt = append_user_filter(stmt, Page, term) elif key == "is": + if q == "deleted": + show_deleted = True if q == "published": stmt = stmt.where(Page.published == true()) if q == "importable": From 9f50c156eea8c95a96d7b8089f046d79af1e90f8 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 10:00:54 -0500 Subject: [PATCH 26/42] Adjust pages api tests --- lib/galaxy/managers/pages.py | 4 ++++ lib/galaxy_test/api/test_pages.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 8438dcff695d..2264b5679da6 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -149,6 +149,10 @@ def index_query( is_admin = trans.user_is_admin user = trans.user + if show_shared and show_deleted and not is_admin: + message = "show_shared and show_deleted cannot both be specified as true" + raise exceptions.RequestParameterInvalidException(message) + if not user: message = "Requires user to log in." raise exceptions.RequestParameterInvalidException(message) diff --git a/lib/galaxy_test/api/test_pages.py b/lib/galaxy_test/api/test_pages.py index e99210fbb34a..b8dc2d79e4e8 100644 --- a/lib/galaxy_test/api/test_pages.py +++ b/lib/galaxy_test/api/test_pages.py @@ -125,7 +125,7 @@ def test_index_deleted(self): delete_response.raise_for_status() assert self._users_index_has_page_with_id(response1) assert not self._users_index_has_page_with_id(response2) - assert self._users_index_has_page_with_id(response2, dict(deleted=True)) + assert self._users_index_has_page_with_id(response2, dict(deleted=True, show_published=False)) def test_index_user_id_security(self): user_id = self.dataset_populator.user_id() From 6609485f48e8cf03fd5388e3871af839cdc55191 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 11:08:28 -0500 Subject: [PATCH 27/42] Adjust selenium test and navigation helpers --- client/src/components/Grid/GridHistory.vue | 10 +++-- client/src/components/Grid/GridPage.vue | 2 +- client/src/utils/navigation/navigation.yml | 27 ++++++------- lib/galaxy/selenium/navigates_galaxy.py | 38 ++++++++----------- .../selenium/test_histories_published.py | 5 --- lib/galaxy_test/selenium/test_pages.py | 12 +++--- 6 files changed, 41 insertions(+), 53 deletions(-) diff --git a/client/src/components/Grid/GridHistory.vue b/client/src/components/Grid/GridHistory.vue index 687ff5bf6bcb..5e105842f7cd 100644 --- a/client/src/components/Grid/GridHistory.vue +++ b/client/src/components/Grid/GridHistory.vue @@ -33,9 +33,13 @@ withDefaults(defineProps(), {
- My Histories - Shared with Me - Public Histories + My Histories + + Shared with Me + + + Public Histories + diff --git a/client/src/components/Grid/GridPage.vue b/client/src/components/Grid/GridPage.vue index c1367f8c4c6d..bb225c8ea3f1 100644 --- a/client/src/components/Grid/GridPage.vue +++ b/client/src/components/Grid/GridPage.vue @@ -25,7 +25,7 @@ withDefaults(defineProps(), {
Pages
- + Create Page diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml index 88975e457fb6..0173be055395 100644 --- a/client/src/utils/navigation/navigation.yml +++ b/client/src/utils/navigation/navigation.yml @@ -57,27 +57,24 @@ masthead: labels: # top-level menus + admin: 'Admin' analyze: 'Analyze Data' - workflow: 'Workflow' - shared_data: 'Shared Data' - visualization: 'Visualization' + data: 'Data' help: 'Help' user: 'User' - admin: 'Admin' + visualization: 'Visualization' + workflow: 'Workflow' # user menu logout: 'Sign Out' - preferences: 'Preferences' - histories: 'Histories' invocations: 'Workflow Invocations' - pages: 'Pages' - histories_shared_with_me: 'Histories shared with me' + preferences: 'Preferences' - # Shared data + # data menu libraries: 'Data Libraries' - published_workflows: 'Workflows' - published_histories: 'Histories' - published_pages: 'Pages' + histories: 'Histories' + pages: 'Pages' + workflows: 'Workflows' preferences: selectors: @@ -408,11 +405,13 @@ published_histories: advanced_search_toggle: '#histories-published-grid [data-description="toggle advanced search"]' advanced_search_submit: '#histories-advanced-filter-submit' search_input: '#histories-published-grid input.search-query[data-description="filter text input"]' + tab: '#histories-published-tab' shared_histories: selectors: _: '#histories-shared-grid' histories: '.grid-table tr' + tab: '#histories-shared-tab' history_copy_elements: selectors: @@ -513,13 +512,9 @@ pages: selectors: create: '#page-create' submit: '#submit' - drop: '.page-dropdown' - drop_edit: '.dropdown-item-edit' - drop_view: '.dropdown-item-view' create_title_input: '#form-element-title input' create_slug_input: '#form-element-slug input' export: '.markdown-pdf-export' - dropdown: '[data-page-dropdown*="${id}"]' index_table: "#page-table" index_rows: "#page-table > tbody > tr:not(.b-table-empty-row, [style*='display: none'])" delete_modal_confirm: '#delete-page-modal-${id}___BV_modal_footer_ .btn-primary' diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py index f5a250853ecb..3f47b80269d6 100644 --- a/lib/galaxy/selenium/navigates_galaxy.py +++ b/lib/galaxy/selenium/navigates_galaxy.py @@ -1176,14 +1176,15 @@ def workflow_editor_click_save(self): def navigate_to_histories_page(self): self.home() - self.click_masthead_user() + self.click_masthead_data() self.components.masthead.histories.wait_for_and_click() self.components.histories.histories.wait_for_present() def navigate_to_histories_shared_with_me_page(self): self.home() - self.click_masthead_user() - self.components.masthead.histories_shared_with_me.wait_for_and_click() + self.click_masthead_data() + self.components.masthead.histories.wait_for_and_click() + self.components.shared_histories.tab.wait_for_and_click() def navigate_to_user_preferences(self): self.home() @@ -1197,22 +1198,18 @@ def navigate_to_invocations(self): def navigate_to_pages(self): self.home() - self.click_masthead_user() + self.click_masthead_data() self.components.masthead.pages.wait_for_and_click() - def navigate_to_published_workflows(self): + def navigate_to_published_histories(self): self.home() - self.click_masthead_shared_data() - self.components.masthead.published_workflows.wait_for_and_click() - - def navigate_to_published_histories_page(self): - self.home() - self.click_masthead_shared_data() - self.components.masthead.published_histories.wait_for_and_click() + self.click_masthead_data() + self.components.masthead.histories.wait_for_and_click() + self.components.published_histories.tab.wait_for_and_click() def navigate_to_published_pages(self): self.home() - self.click_masthead_shared_data() + self.click_masthead_data() self.components.masthead.published_pages.wait_for_and_click() def admin_open(self): @@ -1265,7 +1262,7 @@ def create_new_library(self): def libraries_open(self): self.home() - self.click_masthead_shared_data() + self.click_masthead_data() self.components.masthead.libraries.wait_for_and_click() self.components.libraries.selector.wait_for_visible() @@ -1274,11 +1271,10 @@ def libraries_open_with_name(self, name): self.libraries_index_search_for(name) self.libraries_index_table_elements()[0].find_element(By.CSS_SELECTOR, "td a").click() - def page_open_and_screenshot(self, screenshot_name): + def page_open_and_screenshot(self, page_name, screenshot_name): self.home() self.navigate_to_pages() - self.components.pages.drop.wait_for_and_click() - self.components.pages.drop_view.wait_for_and_click() + self.select_grid_operation(page_name, "View") if screenshot_name: self.sleep_for(self.wait_types.UX_RENDER) self.screenshot(screenshot_name) @@ -1600,9 +1596,7 @@ def select_storage(self, storage_id: str) -> None: def create_page_and_edit(self, name=None, slug=None, screenshot_name=None): name = self.create_page(name=name, slug=slug, screenshot_name=screenshot_name) - self.components.pages.drop.wait_for_and_click() - self.sleep_for(self.wait_types.UX_RENDER) - self.components.pages.drop_edit.wait_for_and_click() + self.select_grid_operation(name, "Edit content") self.components.pages.editor.markdown_editor.wait_for_visible() return name @@ -1654,8 +1648,8 @@ def tool_form_execute(self): def click_masthead_user(self): self.components.masthead.user.wait_for_and_click() - def click_masthead_shared_data(self): - self.components.masthead.shared_data.wait_for_and_click() + def click_masthead_data(self): + self.components.masthead.data.wait_for_and_click() def click_masthead_workflow(self): self.components.masthead.workflow.wait_for_and_click() diff --git a/lib/galaxy_test/selenium/test_histories_published.py b/lib/galaxy_test/selenium/test_histories_published.py index 0adb3e594400..d8a84373019e 100644 --- a/lib/galaxy_test/selenium/test_histories_published.py +++ b/lib/galaxy_test/selenium/test_histories_published.py @@ -106,11 +106,6 @@ def get_present_histories(self): self.sleep_for(self.wait_types.UX_RENDER) return self.components.published_histories.histories.all() - def navigate_to_published_histories(self): - self.home() - self.click_masthead_shared_data() - self.components.masthead.published_histories.wait_for_and_click() - def create_history(self, name): self.home() self.history_panel_create_new_with_name(name) diff --git a/lib/galaxy_test/selenium/test_pages.py b/lib/galaxy_test/selenium/test_pages.py index 8a7153874918..3a4c474e3229 100644 --- a/lib/galaxy_test/selenium/test_pages.py +++ b/lib/galaxy_test/selenium/test_pages.py @@ -21,7 +21,7 @@ def test_simple_page_creation_edit_and_view(self): self.history_panel_wait_for_hid_ok(1) self.navigate_to_pages() self.screenshot("pages_grid") - self.create_page_and_edit(screenshot_name="pages_create_form") + page_name = self.create_page_and_edit(screenshot_name="pages_create_form") self.screenshot("pages_editor_new") editor = self._page_editor editor.markdown_editor.wait_for_and_send_keys("moo\n\n\ncow\n\n") @@ -31,7 +31,7 @@ def test_simple_page_creation_edit_and_view(self): self.sleep_for(self.wait_types.UX_RENDER) editor.save.wait_for_and_click() self.screenshot("pages_editor_saved") - self.page_open_and_screenshot("page_view_with_embedded_dataset") + self.page_open_and_screenshot(page_name, "page_view_with_embedded_dataset") @selenium_test @managed_history @@ -44,7 +44,7 @@ def test_workflow_problem_display(self): WORKFLOW_WITH_BAD_COLUMN_PARAMETER, exact_tools=True ) self.navigate_to_pages() - self.create_page_and_edit() + page_name = self.create_page_and_edit() editor = self._page_editor editor.markdown_editor.wait_for_and_send_keys("moo\n\n\ncow\n\n") editor.embed_workflow_display.wait_for_and_click() @@ -55,7 +55,7 @@ def test_workflow_problem_display(self): editor.workflow_selection(id=problem_workflow_2_id).wait_for_and_click() self.sleep_for(self.wait_types.UX_RENDER) editor.save.wait_for_and_click() - self.page_open_and_screenshot("page_view_with_workflow_problems") + self.page_open_and_screenshot(page_name, "page_view_with_workflow_problems") @selenium_test @managed_history @@ -65,13 +65,13 @@ def test_history_links(self): self.current_history_publish() history_id = self.current_history_id() self.navigate_to_pages() - self.create_page_and_edit() + page_name = self.create_page_and_edit() editor = self._page_editor editor.history_link.wait_for_and_click() editor.history_selection(id=history_id).wait_for_and_click() self.sleep_for(self.wait_types.UX_RENDER) editor.save.wait_for_and_click() - self.page_open_and_screenshot("page_view_with_history_link") + self.page_open_and_screenshot(page_name, "page_view_with_history_link") view = self.components.pages.view view.history_link(history_id=history_id).wait_for_and_click() self.sleep_for(self.wait_types.UX_RENDER) From 4f2dea98bbca9dcc0cfc769e0bd0388ed51a64a0 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 13:00:05 -0500 Subject: [PATCH 28/42] Allow anon users to access published resources --- client/src/components/Grid/GridHistory.vue | 23 ++++++++++- client/src/components/Grid/GridPage.vue | 13 +++++- .../src/components/Grid/GridVisualization.vue | 16 +++++++- .../components/Grid/configs/pagesPublished.ts | 8 ---- client/src/entry/analysis/menu.js | 40 +++++++++++++++++-- client/src/entry/analysis/router.js | 2 + client/src/utils/navigation/navigation.yml | 1 + lib/galaxy/managers/histories.py | 2 +- lib/galaxy/managers/pages.py | 2 +- lib/galaxy/managers/visualizations.py | 2 +- lib/galaxy/selenium/navigates_galaxy.py | 6 +++ 11 files changed, 97 insertions(+), 18 deletions(-) diff --git a/client/src/components/Grid/GridHistory.vue b/client/src/components/Grid/GridHistory.vue index 5e105842f7cd..1b17d2bf3fc0 100644 --- a/client/src/components/Grid/GridHistory.vue +++ b/client/src/components/Grid/GridHistory.vue @@ -6,10 +6,14 @@ import { BNav, BNavItem } from "bootstrap-vue"; import historiesGridConfig from "@/components/Grid/configs/histories"; import historiesPublishedGridConfig from "@/components/Grid/configs/historiesPublished"; import historiesSharedGridConfig from "@/components/Grid/configs/historiesShared"; +import { useUserStore } from "@/stores/userStore"; import Heading from "@/components/Common/Heading.vue"; +import LoginRequired from "@/components/Common/LoginRequired.vue"; import GridList from "@/components/Grid/GridList.vue"; +const userStore = useUserStore(); + library.add(faPlus); interface Props { @@ -33,9 +37,24 @@ withDefaults(defineProps(), {
- My Histories - + + My Histories + + + Shared with Me + Public Histories diff --git a/client/src/components/Grid/GridPage.vue b/client/src/components/Grid/GridPage.vue index bb225c8ea3f1..057423af5f09 100644 --- a/client/src/components/Grid/GridPage.vue +++ b/client/src/components/Grid/GridPage.vue @@ -5,10 +5,14 @@ import { BNav, BNavItem } from "bootstrap-vue"; import pagesGridConfig from "@/components/Grid/configs/pages"; import pagesPublishedGridConfig from "@/components/Grid/configs/pagesPublished"; +import { useUserStore } from "@/stores/userStore"; import Heading from "@/components/Common/Heading.vue"; +import LoginRequired from "@/components/Common/LoginRequired.vue"; import GridList from "@/components/Grid/GridList.vue"; +const userStore = useUserStore(); + library.add(faPlus); interface Props { @@ -32,7 +36,14 @@ withDefaults(defineProps(), { - My Pages + + My Pages + + Public Pages diff --git a/client/src/components/Grid/GridVisualization.vue b/client/src/components/Grid/GridVisualization.vue index e8ab12a9488c..0d1fa623f5d5 100644 --- a/client/src/components/Grid/GridVisualization.vue +++ b/client/src/components/Grid/GridVisualization.vue @@ -5,10 +5,14 @@ import { BNav, BNavItem } from "bootstrap-vue"; import visualizationsGridConfig from "@/components/Grid/configs/visualizations"; import visualizationsPublishedGridConfig from "@/components/Grid/configs/visualizationsPublished"; +import { useUserStore } from "@/stores/userStore"; import Heading from "@/components/Common/Heading.vue"; +import LoginRequired from "@/components/Common/LoginRequired.vue"; import GridList from "@/components/Grid/GridList.vue"; +const userStore = useUserStore(); + library.add(faPlus); interface Props { @@ -32,7 +36,17 @@ withDefaults(defineProps(), { - My Visualizations + + My Visualizations + + Public Visualizations diff --git a/client/src/components/Grid/configs/pagesPublished.ts b/client/src/components/Grid/configs/pagesPublished.ts index 0b34382ac36a..615eb9c31696 100644 --- a/client/src/components/Grid/configs/pagesPublished.ts +++ b/client/src/components/Grid/configs/pagesPublished.ts @@ -2,9 +2,7 @@ import { faEye, faPlus } from "@fortawesome/free-solid-svg-icons"; import { useEventBus } from "@vueuse/core"; import { fetcher } from "@/api/schema"; -import { getGalaxyInstance } from "@/app"; import Filtering, { contains, type ValidFilter } from "@/utils/filtering"; -import { rethrowSimple } from "@/utils/simple-error"; import type { ActionArray, FieldArray, GridConfig } from "./types"; @@ -25,12 +23,6 @@ type PageEntry = Record; * Request and return data from server */ async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { - // TODO: Avoid using Galaxy instance to identify current user - const Galaxy = getGalaxyInstance(); - const userId = !Galaxy.isAnonymous && Galaxy.user.id; - if (!userId) { - rethrowSimple("Please login to access this page."); - } const { data, headers } = await getPages({ limit, offset, diff --git a/client/src/entry/analysis/menu.js b/client/src/entry/analysis/menu.js index 13b07b8912d7..b225c3f802b9 100644 --- a/client/src/entry/analysis/menu.js +++ b/client/src/entry/analysis/menu.js @@ -41,7 +41,7 @@ export function fetchMenu(options = {}) { } // - // 'Shared Items' or Libraries tab. + // 'Data Items' or Libraries tab. // if (Galaxy.config.single_user) { // Single user can still use libraries, especially as we may grow that @@ -52,12 +52,12 @@ export function fetchMenu(options = {}) { url: "/libraries", id: "libraries", }); - } else { + } else if (Galaxy.user.id) { menu.push({ id: "resources", title: _l("Data"), url: "javascript:void(0)", - tooltip: _l("Access published resources"), + tooltip: _l("Access resources"), menu: [ { title: _l("Data Libraries"), @@ -86,6 +86,40 @@ export function fetchMenu(options = {}) { }, ], }); + } else { + menu.push({ + id: "resources", + title: _l("Data"), + url: "javascript:void(0)", + tooltip: _l("Access published resources"), + menu: [ + { + title: _l("Data Libraries"), + url: "/libraries", + target: "_top", + }, + { + title: _l("Datasets"), + url: "/datasets/list_published", + }, + { + title: _l("Histories"), + url: "/histories/list_published", + }, + { + title: _l("Pages"), + url: "/pages/list_published", + }, + { + title: _l("Visualizations"), + url: "/visualizations/list_published", + }, + { + title: _l("Workflows"), + url: "/workflows/list_published", + }, + ], + }); } // diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index e25f0dfcdbf8..f3dbb4513aa7 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -375,6 +375,7 @@ export function getRouter(Galaxy) { props: { activeList: "my", }, + redirect: redirectAnon(), }, { path: "pages/list_published", @@ -490,6 +491,7 @@ export function getRouter(Galaxy) { props: { activeList: "my", }, + redirect: redirectAnon(), }, { path: "visualizations/list_published", diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml index 0173be055395..bbc9154aa00e 100644 --- a/client/src/utils/navigation/navigation.yml +++ b/client/src/utils/navigation/navigation.yml @@ -582,6 +582,7 @@ tool_form: workflows: selectors: shared_with_me_tab: '#shared-with-me' + published_tab: '#published a' new_button: '#workflow-create' import_button: '#workflow-import' save_button: '#workflow-save-button' diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index e704c3569815..880a173fd9cf 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -130,7 +130,7 @@ def index_query( is_admin = trans.user_is_admin user = trans.user - if not user: + if not user and not show_published: message = "Requires user to log in." raise RequestParameterInvalidException(message) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 2264b5679da6..8d84aa811d7b 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -153,7 +153,7 @@ def index_query( message = "show_shared and show_deleted cannot both be specified as true" raise exceptions.RequestParameterInvalidException(message) - if not user: + if not user and not show_published: message = "Requires user to log in." raise exceptions.RequestParameterInvalidException(message) diff --git a/lib/galaxy/managers/visualizations.py b/lib/galaxy/managers/visualizations.py index c6fe3f365b74..581ab36eadcc 100644 --- a/lib/galaxy/managers/visualizations.py +++ b/lib/galaxy/managers/visualizations.py @@ -82,7 +82,7 @@ def index_query( is_admin = trans.user_is_admin user = trans.user - if not user: + if not user and not show_published: message = "Requires user to log in." raise exceptions.RequestParameterInvalidException(message) diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py index 3f47b80269d6..e80143efd39d 100644 --- a/lib/galaxy/selenium/navigates_galaxy.py +++ b/lib/galaxy/selenium/navigates_galaxy.py @@ -1201,6 +1201,12 @@ def navigate_to_pages(self): self.click_masthead_data() self.components.masthead.pages.wait_for_and_click() + def navigate_to_published_workflows(self): + self.home() + self.click_masthead_data() + self.components.masthead.workflows.wait_for_and_click() + self.components.workflows.published_tab.wait_for_and_click() + def navigate_to_published_histories(self): self.home() self.click_masthead_data() From 6f310bdf693484cdb98af7e4eb3589b52109aa02 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 13:18:21 -0500 Subject: [PATCH 29/42] Apply more consistent resource manager options --- client/src/components/Grid/configs/histories.ts | 1 + client/src/components/Grid/configs/pages.ts | 12 +++--------- client/src/components/Grid/configs/visualizations.ts | 10 ++-------- lib/galaxy/managers/histories.py | 2 +- lib/galaxy/managers/pages.py | 4 ++-- lib/galaxy/managers/visualizations.py | 4 ++-- 6 files changed, 11 insertions(+), 22 deletions(-) diff --git a/client/src/components/Grid/configs/histories.ts b/client/src/components/Grid/configs/histories.ts index d5df8a38d50a..678c55eaa993 100644 --- a/client/src/components/Grid/configs/histories.ts +++ b/client/src/components/Grid/configs/histories.ts @@ -39,6 +39,7 @@ async function getData(offset: number, limit: number, search: string, sort_by: s sort_desc, show_own: true, show_published: false, + show_shared: false, }); const totalMatches = parseInt(headers.get("total_matches") ?? "0"); return [data, totalMatches]; diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts index aed9025d1380..ffc48e6546ef 100644 --- a/client/src/components/Grid/configs/pages.ts +++ b/client/src/components/Grid/configs/pages.ts @@ -2,9 +2,8 @@ import { faEdit, faEye, faPlus, faShareAlt, faTrash } from "@fortawesome/free-so import { useEventBus } from "@vueuse/core"; import { fetcher } from "@/api/schema"; -import { getGalaxyInstance } from "@/app"; import Filtering, { contains, equals, toBool, type ValidFilter } from "@/utils/filtering"; -import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error"; +import { errorMessageAsString } from "@/utils/simple-error"; import type { ActionArray, FieldArray, GridConfig } from "./types"; @@ -26,12 +25,6 @@ type PageEntry = Record; * Request and return data from server */ async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { - // TODO: Avoid using Galaxy instance to identify current user - const Galaxy = getGalaxyInstance(); - const userId = !Galaxy.isAnonymous && Galaxy.user.id; - if (!userId) { - rethrowSimple("Please login to access this page."); - } const { data, headers } = await getPages({ limit, offset, @@ -39,7 +32,8 @@ async function getData(offset: number, limit: number, search: string, sort_by: s sort_by: sort_by as SortKeyLiteral, sort_desc, show_published: false, - user_id: userId, + show_own: true, + show_shared: false, }); const totalMatches = parseInt(headers.get("total_matches") ?? "0"); return [data, totalMatches]; diff --git a/client/src/components/Grid/configs/visualizations.ts b/client/src/components/Grid/configs/visualizations.ts index f9623f25e4a7..65eaec80e328 100644 --- a/client/src/components/Grid/configs/visualizations.ts +++ b/client/src/components/Grid/configs/visualizations.ts @@ -4,7 +4,6 @@ import axios from "axios"; import { fetcher } from "@/api/schema"; import { updateTags } from "@/api/tags"; -import { getGalaxyInstance } from "@/app"; import Filtering, { contains, equals, expandNameTag, toBool, type ValidFilter } from "@/utils/filtering"; import { withPrefix } from "@/utils/redirect"; import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error"; @@ -28,12 +27,6 @@ type VisualizationEntry = Record; * Request and return data from server */ async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { - // TODO: Avoid using Galaxy instance to identify current user - const Galaxy = getGalaxyInstance(); - const userId = !Galaxy.isAnonymous && Galaxy.user.id; - if (!userId) { - rethrowSimple("Please login to access this page."); - } const { data, headers } = await getVisualizations({ limit, offset, @@ -41,7 +34,8 @@ async function getData(offset: number, limit: number, search: string, sort_by: s sort_by: sort_by as SortKeyLiteral, sort_desc, show_published: false, - user_id: userId, + show_own: true, + show_shared: false, }); const totalMatches = parseInt(headers.get("total_matches") ?? "0"); return [data, totalMatches]; diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index 880a173fd9cf..bebd03446786 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -196,7 +196,7 @@ def p_tag_filter(term_text: str, quoted: bool): ) ) - if show_published and not is_admin: + if (show_published or show_shared) and not is_admin: show_deleted = False show_purged = False diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 8d84aa811d7b..da0d076305b0 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -160,7 +160,7 @@ def index_query( stmt = select(self.model_class) filters = [] - if show_own or (not show_published and not is_admin): + if show_own or (not show_published and not show_shared and not is_admin): filters = [self.model_class.user == user] if show_published: filters.append(self.model_class.published == true()) @@ -223,7 +223,7 @@ def p_tag_filter(term_text: str, quoted: bool): ) ) - if show_published and not is_admin: + if (show_published or show_shared) and not is_admin: show_deleted = False stmt = stmt.where(self.model_class.deleted == (true() if show_deleted else false())).distinct() diff --git a/lib/galaxy/managers/visualizations.py b/lib/galaxy/managers/visualizations.py index 581ab36eadcc..1f03e3900ff1 100644 --- a/lib/galaxy/managers/visualizations.py +++ b/lib/galaxy/managers/visualizations.py @@ -89,7 +89,7 @@ def index_query( query = trans.sa_session.query(self.model_class) filters = [] - if show_own or (not show_published and not is_admin): + if show_own or (not show_published and not show_shared and not is_admin): filters = [self.model_class.user == user] if show_published: filters.append(self.model_class.published == true()) @@ -152,7 +152,7 @@ def p_tag_filter(term_text: str, quoted: bool): ) ) - if show_published and not is_admin: + if (show_published or show_shared) and not is_admin: show_deleted = False query = query.filter(self.model_class.deleted == (true() if show_deleted else false())).distinct() From 6625d47f078c1c4c4a9a41fee90fe62f7ae3f851 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 13:32:06 -0500 Subject: [PATCH 30/42] Fix link to published history display --- client/src/components/Grid/configs/historiesPublished.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Grid/configs/historiesPublished.ts b/client/src/components/Grid/configs/historiesPublished.ts index 61e80ed3506a..23dc4bcccaff 100644 --- a/client/src/components/Grid/configs/historiesPublished.ts +++ b/client/src/components/Grid/configs/historiesPublished.ts @@ -46,7 +46,7 @@ const fields: FieldArray = [ title: "View", icon: faEye, handler: (data: HistoryEntry) => { - emit(`/histories/view?id=${data.id}`); + emit(`/published/history?id=${data.id}`); }, }, ], From 24b6b2234ca85b3cfbcbf967e6f193b8a6a61d39 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 13:35:14 -0500 Subject: [PATCH 31/42] Hide grid actions for anon users --- client/src/components/Grid/GridHistory.vue | 2 +- client/src/components/Grid/GridPage.vue | 2 +- client/src/components/Grid/GridVisualization.vue | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/Grid/GridHistory.vue b/client/src/components/Grid/GridHistory.vue index 1b17d2bf3fc0..ca9a00fa0866 100644 --- a/client/src/components/Grid/GridHistory.vue +++ b/client/src/components/Grid/GridHistory.vue @@ -29,7 +29,7 @@ withDefaults(defineProps(), {
Histories -
+
Import History diff --git a/client/src/components/Grid/GridPage.vue b/client/src/components/Grid/GridPage.vue index 057423af5f09..e340c9eaecfa 100644 --- a/client/src/components/Grid/GridPage.vue +++ b/client/src/components/Grid/GridPage.vue @@ -28,7 +28,7 @@ withDefaults(defineProps(), {
Pages -
+
Create Page diff --git a/client/src/components/Grid/GridVisualization.vue b/client/src/components/Grid/GridVisualization.vue index 0d1fa623f5d5..6c096572d595 100644 --- a/client/src/components/Grid/GridVisualization.vue +++ b/client/src/components/Grid/GridVisualization.vue @@ -28,7 +28,7 @@ withDefaults(defineProps(), {
Visualizations -
+
Create Visualization From 4cc5766571ea315c18ebd4089bebe2dced361751 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 14:56:36 -0500 Subject: [PATCH 32/42] Fix history grid action data description --- client/src/components/Grid/GridHistory.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/components/Grid/GridHistory.vue b/client/src/components/Grid/GridHistory.vue index ca9a00fa0866..071ab611c156 100644 --- a/client/src/components/Grid/GridHistory.vue +++ b/client/src/components/Grid/GridHistory.vue @@ -30,7 +30,11 @@ withDefaults(defineProps(), {
Histories
- + Import History From fa9720437a47251283d978c22b1d4a439d014f9f Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 17:02:49 -0500 Subject: [PATCH 33/42] Fix page pdf export test case --- test/integration_selenium/test_pages_pdf_export.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/integration_selenium/test_pages_pdf_export.py b/test/integration_selenium/test_pages_pdf_export.py index 801dcbae620b..9efc0e539ece 100644 --- a/test/integration_selenium/test_pages_pdf_export.py +++ b/test/integration_selenium/test_pages_pdf_export.py @@ -16,10 +16,8 @@ def handle_galaxy_config_kwds(cls, config): def test_page_pdf_export(self): self.navigate_to_pages() self.screenshot("pages_grid") - self.create_page() - self.components.pages.drop.wait_for_and_click() - self.sleep_for(self.wait_types.UX_RENDER) - self.components.pages.drop_edit.wait_for_and_click() + page_name = self.create_page() + self.select_grid_operation(page_name, "Edit content") self.components.pages.editor.markdown_editor.wait_for_and_send_keys("moo\n\n\ncow\n\n") self.screenshot("pages_markdown_editor") self.sleep_for(self.wait_types.UX_RENDER) @@ -28,9 +26,7 @@ def test_page_pdf_export(self): self.screenshot("pages_markdown_editor_saved") self.sleep_for(self.wait_types.UX_RENDER) self.navigate_to_pages() - self.components.pages.drop.wait_for_and_click() - self.sleep_for(self.wait_types.UX_RENDER) - self.components.pages.drop_view.wait_for_and_click() + self.select_grid_operation(page_name, "View") self.screenshot("pages_view_simple") self.components.pages.export.wait_for_and_click() self.sleep_for(self.wait_types.UX_RENDER) From a97fd990e50245e9990f0c18d10a50bdf3a3808f Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 18:56:51 -0500 Subject: [PATCH 34/42] Adjust pages index test, add confirmation alert, reuse grid name helper --- client/src/components/Grid/configs/pages.ts | 25 +++++++++-------- client/src/utils/navigation/navigation.yml | 3 --- lib/galaxy/selenium/navigates_galaxy.py | 27 ++++++++----------- .../selenium/test_histories_list.py | 2 +- .../selenium/test_histories_published.py | 2 +- lib/galaxy_test/selenium/test_pages.py | 2 +- lib/galaxy_test/selenium/test_pages_index.py | 10 +++---- .../test_history_import_export_ftp.py | 2 +- 8 files changed, 33 insertions(+), 40 deletions(-) diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts index ffc48e6546ef..24eaad541bfc 100644 --- a/client/src/components/Grid/configs/pages.ts +++ b/client/src/components/Grid/configs/pages.ts @@ -3,6 +3,7 @@ import { useEventBus } from "@vueuse/core"; import { fetcher } from "@/api/schema"; import Filtering, { contains, equals, toBool, type ValidFilter } from "@/utils/filtering"; +import _l from "@/utils/localization"; import { errorMessageAsString } from "@/utils/simple-error"; import type { ActionArray, FieldArray, GridConfig } from "./types"; @@ -98,17 +99,19 @@ const fields: FieldArray = [ icon: faTrash, condition: (data: PageEntry) => !data.deleted, handler: async (data: PageEntry) => { - try { - await deletePage({ id: String(data.id) }); - return { - status: "success", - message: `'${data.title}' has been deleted.`, - }; - } catch (e) { - return { - status: "danger", - message: `Failed to delete '${data.title}': ${errorMessageAsString(e)}.`, - }; + if (confirm(_l(`Are you sure that you want to restore the selected histories?`))) { + try { + await deletePage({ id: String(data.id) }); + return { + status: "success", + message: `'${data.title}' has been deleted.`, + }; + } catch (e) { + return { + status: "danger", + message: `Failed to delete '${data.title}': ${errorMessageAsString(e)}.`, + }; + } } }, }, diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml index bbc9154aa00e..44704df383f7 100644 --- a/client/src/utils/navigation/navigation.yml +++ b/client/src/utils/navigation/navigation.yml @@ -515,9 +515,6 @@ pages: create_title_input: '#form-element-title input' create_slug_input: '#form-element-slug input' export: '.markdown-pdf-export' - index_table: "#page-table" - index_rows: "#page-table > tbody > tr:not(.b-table-empty-row, [style*='display: none'])" - delete_modal_confirm: '#delete-page-modal-${id}___BV_modal_footer_ .btn-primary' editor: selectors: diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py index e80143efd39d..045918d7b038 100644 --- a/lib/galaxy/selenium/navigates_galaxy.py +++ b/lib/galaxy/selenium/navigates_galaxy.py @@ -540,6 +540,17 @@ def history_panel_wait_for_hid_state(self, hid, state, allowed_force_refreshes=0 raise self.prepend_timeout_message(e, message) return history_item_selector_state + @retry_during_transitions + def get_grid_entry_names(self, selector): + self.sleep_for(self.wait_types.UX_RENDER) + names = [] + grid = self.wait_for_selector(selector) + for row in grid.find_elements(By.TAG_NAME, "tr"): + td = row.find_elements(By.TAG_NAME, "td") + name = td[1].text if td[0].text == "" else td[0].text + names.append(name) + return names + def select_grid_operation(self, item_name, option_label): target_item = None grid = self.components.grids.body.wait_for_visible() @@ -1390,11 +1401,6 @@ def pages_index_table_elements(self): pages.index_table.wait_for_visible() return pages.index_rows.all() - def page_index_click_option(self, option_title, page_id): - self.components.pages.dropdown(id=page_id).wait_for_and_click() - if not self.select_dropdown_item(option_title): - raise AssertionError(f"Failed to find page action option with title [{option_title}]") - def workflow_index_open(self): self.home() self.click_masthead_workflow() @@ -1737,17 +1743,6 @@ def histories_click_advanced_search(self): search_selector = "#standard-search .advanced-search-toggle" self.wait_for_and_click_selector(search_selector) - @retry_during_transitions - def histories_get_history_names(self, selector="#histories-grid"): - self.sleep_for(self.wait_types.UX_RENDER) - names = [] - grid = self.wait_for_selector(selector) - for row in grid.find_elements(By.TAG_NAME, "tr"): - td = row.find_elements(By.TAG_NAME, "td") - name = td[1].text if td[0].text == "" else td[0].text - names.append(name) - return names - @edit_details def history_panel_add_tags(self, tags): tag_area_button = self.components.history_panel.tag_area_button diff --git a/lib/galaxy_test/selenium/test_histories_list.py b/lib/galaxy_test/selenium/test_histories_list.py index d89cb0b94983..7d516ae0ede3 100644 --- a/lib/galaxy_test/selenium/test_histories_list.py +++ b/lib/galaxy_test/selenium/test_histories_list.py @@ -272,7 +272,7 @@ def assert_histories_in_grid(self, expected_histories, present=True): assert intersection == set() def get_histories(self): - return self.histories_get_history_names() + return self.get_grid_entry_names("#histories-grid") def add_tag(self, tags_cell, tag): tag_button = tags_cell.find_element(By.CSS_SELECTOR, ".stateless-tags button") diff --git a/lib/galaxy_test/selenium/test_histories_published.py b/lib/galaxy_test/selenium/test_histories_published.py index d8a84373019e..392991ad1813 100644 --- a/lib/galaxy_test/selenium/test_histories_published.py +++ b/lib/galaxy_test/selenium/test_histories_published.py @@ -85,7 +85,7 @@ def test_published_histories_search_advanced(self): @retry_assertion_during_transitions def assert_histories_present(self, expected_histories, sort_by_matters=False): - present_histories = self.histories_get_history_names(selector="#histories-published-grid") + present_histories = self.get_grid_entry_names("#histories-published-grid") assert len(present_histories) == len(expected_histories) for index, history_name in enumerate(present_histories): if not sort_by_matters: diff --git a/lib/galaxy_test/selenium/test_pages.py b/lib/galaxy_test/selenium/test_pages.py index 3a4c474e3229..6c2b5135bb2b 100644 --- a/lib/galaxy_test/selenium/test_pages.py +++ b/lib/galaxy_test/selenium/test_pages.py @@ -76,7 +76,7 @@ def test_history_links(self): view.history_link(history_id=history_id).wait_for_and_click() self.sleep_for(self.wait_types.UX_RENDER) self.navigate_to_histories_page() - history_names = self.histories_get_history_names() + history_names = self.get_grid_entry_names("#histories-grid") assert f"Copy of '{new_history_name}'" in history_names @property diff --git a/lib/galaxy_test/selenium/test_pages_index.py b/lib/galaxy_test/selenium/test_pages_index.py index baca1d802e97..606a4b7b0d78 100644 --- a/lib/galaxy_test/selenium/test_pages_index.py +++ b/lib/galaxy_test/selenium/test_pages_index.py @@ -14,11 +14,9 @@ def test_page_deletion(self): page_id = page_response["id"] self.navigate_to_pages() self._assert_showing_n_pages(1) - self.components.pages.dropdown(id=page_id).wait_for_visible() - self.page_index_click_option("Delete", page_id) - self.sleep_for(self.wait_types.UX_RENDER) - self.components.pages.delete_modal_confirm(id=page_id).wait_for_and_click() - self.components.pages.dropdown(id=page_id).wait_for_absent_or_hidden() + self.select_grid_operation("MY PAGE", "Delete") + alert = self.driver.switch_to.alert + alert.accept() self._assert_showing_n_pages(0) def new_page(self): @@ -28,6 +26,6 @@ def new_page(self): @retry_assertion_during_transitions def _assert_showing_n_pages(self, n): - if (actual_count := len(self.pages_index_table_elements())) != n: + if (actual_count := len(self.get_grid_entry_names("#pages-grid"))) != n: message = f"Expected {n} pages to be displayed, based on DOM found {actual_count} page index rows." raise AssertionError(message) diff --git a/test/integration_selenium/test_history_import_export_ftp.py b/test/integration_selenium/test_history_import_export_ftp.py index 8bae05ea040e..be0749714db7 100644 --- a/test/integration_selenium/test_history_import_export_ftp.py +++ b/test/integration_selenium/test_history_import_export_ftp.py @@ -82,7 +82,7 @@ def test_history_import_export(self): history_import.success_message.wait_for_visible() gx_selenium_context.navigate_to_histories_page() - newest_history_name = gx_selenium_context.histories_get_history_names()[0] + newest_history_name = gx_selenium_context.get_grid_entry_names("#histories-grid")[0] assert newest_history_name.startswith("imported from archive") From 0db37564cdf896456a6fdde4dff61bde0a596858 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 22:45:54 -0500 Subject: [PATCH 35/42] Adjust page deletion test case --- lib/galaxy_test/selenium/test_pages_index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy_test/selenium/test_pages_index.py b/lib/galaxy_test/selenium/test_pages_index.py index 606a4b7b0d78..7e0af6db293d 100644 --- a/lib/galaxy_test/selenium/test_pages_index.py +++ b/lib/galaxy_test/selenium/test_pages_index.py @@ -11,10 +11,10 @@ class TestPagesIndex(SeleniumTestCase): @selenium_test def test_page_deletion(self): page_response = self.new_page() - page_id = page_response["id"] + page_title = page_response["title"] self.navigate_to_pages() self._assert_showing_n_pages(1) - self.select_grid_operation("MY PAGE", "Delete") + self.select_grid_operation(page_title, "Delete") alert = self.driver.switch_to.alert alert.accept() self._assert_showing_n_pages(0) From 6419c29e8f7a32c827dd4c71cad4776c22d806f9 Mon Sep 17 00:00:00 2001 From: guerler Date: Mon, 19 Feb 2024 22:55:59 -0500 Subject: [PATCH 36/42] Adjust published pages test case --- lib/galaxy/selenium/navigates_galaxy.py | 2 +- lib/galaxy_test/selenium/test_published_pages.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py index 045918d7b038..60be6ac0a58e 100644 --- a/lib/galaxy/selenium/navigates_galaxy.py +++ b/lib/galaxy/selenium/navigates_galaxy.py @@ -1227,7 +1227,7 @@ def navigate_to_published_histories(self): def navigate_to_published_pages(self): self.home() self.click_masthead_data() - self.components.masthead.published_pages.wait_for_and_click() + self.components.masthead.pages.wait_for_and_click() def admin_open(self): self.components.masthead.admin.wait_for_and_click() diff --git a/lib/galaxy_test/selenium/test_published_pages.py b/lib/galaxy_test/selenium/test_published_pages.py index 7d367ae7da17..d8813f323ba1 100644 --- a/lib/galaxy_test/selenium/test_published_pages.py +++ b/lib/galaxy_test/selenium/test_published_pages.py @@ -8,7 +8,7 @@ class TestPublishedPagesGrid(SharedStateSeleniumTestCase): @selenium_test def test_index(self): self.navigate_to_published_pages() - self.components.pages.dropdown(id=self.page_id_1).wait_for_visible() + assert len(self.get_grid_entry_names("#pages-published-grid")) == 2 def setup_shared_state(self): self.user1_email = self._get_random_email("test1") From 1217f1173fafc3c2d8b5109b7cd6523b32833177 Mon Sep 17 00:00:00 2001 From: guerler Date: Tue, 20 Feb 2024 07:09:29 -0500 Subject: [PATCH 37/42] Adjust ui tour --- config/plugins/tours/core.galaxy_ui.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/plugins/tours/core.galaxy_ui.yaml b/config/plugins/tours/core.galaxy_ui.yaml index 2fed473e3859..8cdbed21d4a1 100644 --- a/config/plugins/tours/core.galaxy_ui.yaml +++ b/config/plugins/tours/core.galaxy_ui.yaml @@ -129,9 +129,9 @@ steps: element: "#workflow" intro: "Create, manage, import, export and share your Workflows." - - title: "Shared data" - element: "#shared" - intro: "Get access to all Workflows, Histories, Pages, Visualizations and your Data Library that are shared with you." + - title: "Data" + element: "#resources" + intro: "Get access to all Workflows, Histories, Pages and Visualizations." - title: "Visualization" element: "#visualization" From 8f4af4f7445f4ba2230aecb020760f194f9763f1 Mon Sep 17 00:00:00 2001 From: guerler Date: Tue, 20 Feb 2024 13:09:22 -0500 Subject: [PATCH 38/42] Fix page deletion alert, use different icon for editing content vs editing attributes --- client/src/components/Grid/configs/pages.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts index 24eaad541bfc..bf31e6449c85 100644 --- a/client/src/components/Grid/configs/pages.ts +++ b/client/src/components/Grid/configs/pages.ts @@ -1,4 +1,4 @@ -import { faEdit, faEye, faPlus, faShareAlt, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faEdit, faEye, faPen, faPlus, faShareAlt, faTrash } from "@fortawesome/free-solid-svg-icons"; import { useEventBus } from "@vueuse/core"; import { fetcher } from "@/api/schema"; @@ -80,7 +80,7 @@ const fields: FieldArray = [ }, { title: "Edit Content", - icon: faEdit, + icon: faPen, condition: (data: PageEntry) => !data.deleted, handler: (data: PageEntry) => { emit(`/pages/editor?id=${data.id}`); @@ -99,7 +99,7 @@ const fields: FieldArray = [ icon: faTrash, condition: (data: PageEntry) => !data.deleted, handler: async (data: PageEntry) => { - if (confirm(_l(`Are you sure that you want to restore the selected histories?`))) { + if (confirm(_l(`Are you sure that you want to delete the selected page?`))) { try { await deletePage({ id: String(data.id) }); return { From 93b1d83c91b35da1d725a11451f97c2fb36095ff Mon Sep 17 00:00:00 2001 From: guerler Date: Tue, 20 Feb 2024 13:12:28 -0500 Subject: [PATCH 39/42] Add undelete endpoint for pages --- client/src/api/schema/schema.ts | 33 +++++++++++++++++++++ lib/galaxy/webapps/galaxy/api/pages.py | 14 +++++++++ lib/galaxy/webapps/galaxy/services/pages.py | 12 ++++++++ 3 files changed, 59 insertions(+) diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 1eeb526466d7..93ef97a5fdf9 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -1361,6 +1361,13 @@ export interface paths { */ put: operations["set_slug_api_pages__id__slug_put"]; }; + "/api/pages/{id}/undelete": { + /** + * Undelete the specific Page. + * @description Marks the Page with the given ID as undeleted. + */ + put: operations["undelete_api_pages__id__undelete_put"]; + }; "/api/pages/{id}/unpublish": { /** * Removes this item from the published list. @@ -18989,6 +18996,32 @@ export interface operations { }; }; }; + undelete_api_pages__id__undelete_put: { + /** + * Undelete the specific Page. + * @description Marks the Page with the given ID as undeleted. + */ + parameters: { + /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ + header?: { + "run-as"?: string | null; + }; + /** @description The ID of the Page. */ + path: { + id: string; + }; + }; + responses: { + /** @description Successful Response */ + 204: never; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; unpublish_api_pages__id__unpublish_put: { /** * Removes this item from the published list. diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index 7cea098afdf3..e098e9870e48 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -161,6 +161,20 @@ async def delete( self.service.delete(trans, id) return Response(status_code=status.HTTP_204_NO_CONTENT) + @router.put( + "/api/pages/{id}/undelete", + summary="Undelete the specific Page.", + status_code=status.HTTP_204_NO_CONTENT, + ) + async def undelete( + self, + id: PageIdPathParam, + trans: ProvidesUserContext = DependsOnTrans, + ): + """Marks the Page with the given ID as undeleted.""" + self.service.undelete(trans, id) + return Response(status_code=status.HTTP_204_NO_CONTENT) + @router.get( "/api/pages/{id}.pdf", summary="Return a PDF document of the last revision of the Page.", diff --git a/lib/galaxy/webapps/galaxy/services/pages.py b/lib/galaxy/webapps/galaxy/services/pages.py index a9154d7eccd5..252078fca957 100644 --- a/lib/galaxy/webapps/galaxy/services/pages.py +++ b/lib/galaxy/webapps/galaxy/services/pages.py @@ -100,6 +100,18 @@ def delete(self, trans, id: DecodedDatabaseIdField): with transaction(trans.sa_session): trans.sa_session.commit() + def delete(self, trans, id: DecodedDatabaseIdField): + """ + Undelete page + + :param id: ID of the page to be undeleted + """ + page = base.get_object(trans, id, "Page", check_ownership=True) + + page.deleted = False + with transaction(trans.sa_session): + trans.sa_session.commit() + def show(self, trans, id: DecodedDatabaseIdField) -> PageDetails: """View a page summary and the content of the latest revision From a29b9c69c1619d8fe55d590b56a4b4c8efa043c4 Mon Sep 17 00:00:00 2001 From: guerler Date: Tue, 20 Feb 2024 13:14:33 -0500 Subject: [PATCH 40/42] Add undelete pages option to dropdown --- client/src/components/Grid/configs/pages.ts | 24 ++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/client/src/components/Grid/configs/pages.ts b/client/src/components/Grid/configs/pages.ts index bf31e6449c85..68abfb70e196 100644 --- a/client/src/components/Grid/configs/pages.ts +++ b/client/src/components/Grid/configs/pages.ts @@ -1,4 +1,4 @@ -import { faEdit, faEye, faPen, faPlus, faShareAlt, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faEdit, faEye, faPen, faPlus, faShareAlt, faTrash, faTrashRestore } from "@fortawesome/free-solid-svg-icons"; import { useEventBus } from "@vueuse/core"; import { fetcher } from "@/api/schema"; @@ -15,6 +15,7 @@ const { emit } = useEventBus("grid-router-push"); */ const getPages = fetcher.path("/api/pages").method("get").create(); const deletePage = fetcher.path("/api/pages/{id}").method("delete").create(); +const undeletePage = fetcher.path("/api/pages/{id}/undelete").method("put").create(); /** * Local types @@ -115,6 +116,27 @@ const fields: FieldArray = [ } }, }, + { + title: "Restore", + icon: faTrashRestore, + condition: (data: PageEntry) => !!data.deleted, + handler: async (data: PageEntry) => { + if (confirm(_l(`Are you sure that you want to restore the selected page?`))) { + try { + await undeletePage({ id: String(data.id) }); + return { + status: "success", + message: `'${data.title}' has been restored.`, + }; + } catch (e) { + return { + status: "danger", + message: `Failed to restore '${data.title}': ${errorMessageAsString(e)}.`, + }; + } + } + }, + }, ], }, { From f26c3869ca9ac493ec6c6ee8c67f5539e925037b Mon Sep 17 00:00:00 2001 From: guerler Date: Tue, 20 Feb 2024 13:17:43 -0500 Subject: [PATCH 41/42] Fix undelete service helper name --- lib/galaxy/webapps/galaxy/services/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/galaxy/services/pages.py b/lib/galaxy/webapps/galaxy/services/pages.py index 252078fca957..09ff71e0262b 100644 --- a/lib/galaxy/webapps/galaxy/services/pages.py +++ b/lib/galaxy/webapps/galaxy/services/pages.py @@ -100,7 +100,7 @@ def delete(self, trans, id: DecodedDatabaseIdField): with transaction(trans.sa_session): trans.sa_session.commit() - def delete(self, trans, id: DecodedDatabaseIdField): + def undelete(self, trans, id: DecodedDatabaseIdField): """ Undelete page From 5c4c77867b40452f22fce9da868cf6769db9d729 Mon Sep 17 00:00:00 2001 From: guerler Date: Tue, 20 Feb 2024 13:21:18 -0500 Subject: [PATCH 42/42] Move workflow invocations to data tab --- client/src/entry/analysis/menu.js | 8 ++++---- client/src/utils/navigation/navigation.yml | 2 +- lib/galaxy/selenium/navigates_galaxy.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/entry/analysis/menu.js b/client/src/entry/analysis/menu.js index b225c3f802b9..46238fd1b020 100644 --- a/client/src/entry/analysis/menu.js +++ b/client/src/entry/analysis/menu.js @@ -84,6 +84,10 @@ export function fetchMenu(options = {}) { title: _l("Workflows"), url: "/workflows/list", }, + { + title: _l("Workflow Invocations"), + url: "/workflows/invocations", + }, ], }); } else { @@ -253,10 +257,6 @@ export function fetchMenu(options = {}) { url: "/user/notifications", }); } - userTab.menu.push({ - title: _l("Workflow Invocations"), - url: "/workflows/invocations", - }); userTab.menu.push({ divider: true }); userTab.menu.push({ title: _l("Preferences"), diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml index 44704df383f7..de60da96d863 100644 --- a/client/src/utils/navigation/navigation.yml +++ b/client/src/utils/navigation/navigation.yml @@ -67,12 +67,12 @@ masthead: # user menu logout: 'Sign Out' - invocations: 'Workflow Invocations' preferences: 'Preferences' # data menu libraries: 'Data Libraries' histories: 'Histories' + invocations: 'Workflow Invocations' pages: 'Pages' workflows: 'Workflows' diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py index 60be6ac0a58e..9d3dae8fb6bd 100644 --- a/lib/galaxy/selenium/navigates_galaxy.py +++ b/lib/galaxy/selenium/navigates_galaxy.py @@ -1204,7 +1204,7 @@ def navigate_to_user_preferences(self): def navigate_to_invocations(self): self.home() - self.click_masthead_user() + self.click_masthead_data() self.components.masthead.invocations.wait_for_and_click() def navigate_to_pages(self):