import Vue from "vue"; import VueRouter from "vue-router"; import { getGalaxyInstance } from "@/app"; import { HistoryExport } from "@/components/HistoryExport/index"; import { APIKey } from "@/components/User/APIKey"; import { ExternalIdentities } from "@/components/User/ExternalIdentities"; import { hasSingleOidcProfile } from "@/components/User/ExternalIdentities/ExternalIDHelper"; import AdminRoutes from "@/entry/analysis/routes/admin-routes"; import LibraryRoutes from "@/entry/analysis/routes/library-routes"; import StorageRoutes from "@/entry/analysis/routes/storage-routes"; import { getAppRoot } from "@/onload/loadConfig"; import { requireAuth } from "@/router/guards"; import { parseBool } from "@/utils/utils"; import { patchRouterPush } from "./router-push"; import CenterFrame from "./modules/CenterFrame.vue"; import AboutGalaxy from "@/components/AboutGalaxy.vue"; import AvailableDatatypes from "@/components/AvailableDatatypes/AvailableDatatypes.vue"; import ChatGXY from "@/components/ChatGXY.vue"; import CitationsList from "@/components/Citation/CitationsList.vue"; import ClientError from "@/components/ClientError.vue"; import CollectionEditView from "@/components/Collections/common/CollectionEditView.vue"; import DisplayCollectionAsSheet from "@/components/Collections/common/DisplayCollectionAsSheet.vue"; import ListWizard from "@/components/Collections/ListWizard.vue"; import RulesStandalone from "@/components/Collections/RulesStandalone.vue"; import DatasetCopy from "@/components/Dataset/DatasetCopy.vue"; import DatasetList from "@/components/Dataset/DatasetList.vue"; import DatasetView from "@/components/Dataset/DatasetView.vue"; import DatasetDetails from "@/components/DatasetInformation/DatasetDetails.vue"; import RecentDownloads from "@/components/Downloads/RecentDownloads.vue"; import CreateFileSourceInstance from "@/components/FileSources/Instances/CreateInstance.vue"; import EditFileSourceInstance from "@/components/FileSources/Instances/EditInstance.vue"; import ManageFileSourceIndex from "@/components/FileSources/Instances/ManageIndex.vue"; import UpgradeFileSourceInstance from "@/components/FileSources/Instances/UpgradeInstance.vue"; import CreateUserFileSource from "@/components/FileSources/Templates/CreateUserFileSource.vue"; import FormGeneric from "@/components/Form/FormGeneric.vue"; import GalaxyWizard from "@/components/GalaxyWizard.vue"; import GridInvocation from "@/components/Grid/GridInvocation.vue"; import GridPage from "@/components/Grid/GridPage.vue"; import GridVisualization from "@/components/Grid/GridVisualization.vue"; import HelpTerm from "@/components/Help/HelpTerm.vue"; import HistoryArchiveWizard from "@/components/History/Archiving/HistoryArchiveWizard.vue"; import HistoryExportTasks from "@/components/History/Export/HistoryExport.vue"; import HistoryAccessibility from "@/components/History/HistoryAccessibility.vue"; import HistoryDatasetPermissions from "@/components/History/HistoryDatasetPermissions.vue"; import HistoryList from "@/components/History/HistoryList.vue"; import HistoryPublished from "@/components/History/HistoryPublished.vue"; import HistoryView from "@/components/History/HistoryView.vue"; import HistoryMultipleView from "@/components/History/Multiple/MultipleView.vue"; import HistoryImport from "@/components/HistoryImport.vue"; import ZipImportResults from "@/components/ImportData/zip/ZipImportResults.vue"; import ZipImportWizard from "@/components/ImportData/zip/ZipImportWizard.vue"; import InteractiveToolFrame from "@/components/InteractiveTools/InteractiveToolFrame.vue"; import InteractiveTools from "@/components/InteractiveTools/InteractiveTools.vue"; import JobDetails from "@/components/JobInformation/JobDetails.vue"; import CarbonEmissionsCalculations from "@/components/JobMetrics/CarbonEmissions/CarbonEmissionsCalculations.vue"; import ToolLanding from "@/components/Landing/ToolLanding.vue"; import WorkflowLanding from "@/components/Landing/WorkflowLanding.vue"; import NotificationsList from "@/components/Notifications/NotificationsList.vue"; import CreateObjectStoreInstance from "@/components/ObjectStore/Instances/CreateInstance.vue"; import EditObjectStoreInstance from "@/components/ObjectStore/Instances/EditInstance.vue"; import ManageObjectStoreIndex from "@/components/ObjectStore/Instances/ManageIndex.vue"; import UpgradeObjectStoreInstance from "@/components/ObjectStore/Instances/UpgradeInstance.vue"; import CreateUserObjectStore from "@/components/ObjectStore/Templates/CreateUserObjectStore.vue"; import PageView from "@/components/Page/PageView.vue"; import PageForm from "@/components/PageDisplay/PageForm.vue"; import PageEditor from "@/components/PageEditor/PageEditor.vue"; import UploadMethodView from "@/components/Panels/Upload/UploadMethodView.vue"; import UploadPage from "@/components/Panels/Upload/UploadPage.vue"; import UploadProgress from "@/components/Panels/Upload/UploadProgress.vue"; import Sharing from "@/components/Sharing/SharingPage.vue"; import ToolReport from "@/components/Tool/ToolReport.vue"; import ToolSuccess from "@/components/Tool/ToolSuccess.vue"; import ToolOntologies from "@/components/ToolsList/ToolOntologies.vue"; import ToolsList from "@/components/ToolsList/ToolsList.vue"; import ToolsJson from "@/components/ToolsView/ToolsSchemaJson/ToolsJson.vue"; import TourList from "@/components/Tour/TourList.vue"; import CredentialsManagement from "@/components/User/Credentials/CredentialsManagement.vue"; import CustomBuilds from "@/components/User/CustomBuilds.vue"; import HistoryStorageOverview from "@/components/User/DiskUsage/Visualizations/HistoryStorageOverview.vue"; import NotificationsPreferences from "@/components/User/Notifications/NotificationsPreferences.vue"; import UserDatasetPermissions from "@/components/User/UserDatasetPermissions.vue"; import UserOidcProfile from "@/components/User/UserOidcProfile.vue"; import UserPreferences from "@/components/User/UserPreferences.vue"; import UserPreferencesForm from "@/components/User/UserPreferencesForm.vue"; import DisplayApplication from "@/components/Visualizations/DisplayApplication.vue"; import VisualizationsList from "@/components/Visualizations/Index.vue"; import VisualizationCreate from "@/components/Visualizations/VisualizationCreate.vue"; import VisualizationDisplay from "@/components/Visualizations/VisualizationDisplay.vue"; import VisualizationPublished from "@/components/Visualizations/VisualizationPublished.vue"; import HistoryInvocations from "@/components/Workflow/HistoryInvocations.vue"; import TrsSearch from "@/components/Workflow/Import/TrsSearch.vue"; import InvocationReport from "@/components/Workflow/InvocationReport.vue"; import WorkflowList from "@/components/Workflow/List/WorkflowList.vue"; import WorkflowPublished from "@/components/Workflow/Published/WorkflowPublished.vue"; import WorkflowRerun from "@/components/Workflow/Run/WorkflowRerun.vue"; import WorkflowRun from "@/components/Workflow/Run/WorkflowRun.vue"; import StoredWorkflowInvocations from "@/components/Workflow/StoredWorkflowInvocations.vue"; import WorkflowCreate from "@/components/Workflow/WorkflowCreate.vue"; import WorkflowExport from "@/components/Workflow/WorkflowExport.vue"; import WorkflowImport from "@/components/Workflow/WorkflowImport.vue"; import WorkflowInvocationState from "@/components/WorkflowInvocationState/WorkflowInvocationState.vue"; import Analysis from "@/entry/analysis/modules/Analysis.vue"; import Home from "@/entry/analysis/modules/Home.vue"; import Login from "@/entry/analysis/modules/Login.vue"; import Register from "@/entry/analysis/modules/Register.vue"; import WorkflowEditorModule from "@/entry/analysis/modules/WorkflowEditor.vue"; Vue.use(VueRouter); // Async component for CustomToolEditor to reduce bundle size // NOTE: We use the full async component factory pattern instead of simple dynamic imports // (i.e., `() => import("@/components/Tool/CustomToolEditor.vue")`) due to what I think are router limitations. Revisit with vr-4 const CustomToolEditor = () => ({ component: import("@/components/Tool/CustomToolEditor.vue"), loading: { template: '
Loading Tool Editor...
', }, error: { template: '
Failed to load Tool Editor
', }, delay: 200, timeout: 10000, }); // patches $router.push() to trigger an event and hide duplication warnings patchRouterPush(VueRouter); // redirect anon users function redirectAnon(redirect = "") { const Galaxy = getGalaxyInstance(); if (!Galaxy.user || !Galaxy.user.id) { if (redirect !== "") { return redirect; } else { return "/login/start"; } } } // redirect logged in users function redirectLoggedIn() { const Galaxy = getGalaxyInstance(); if (Galaxy.user.id) { return "/"; } } function redirectIf(condition, path) { if (condition) { return path; } } // produces the client router export function getRouter(Galaxy) { const router = new VueRouter({ base: getAppRoot(), mode: "history", routes: [ /** Login entry route */ { path: "/login/start", component: Login, redirect: redirectLoggedIn(), }, /** Registration entry route */ { path: "/register/start", component: Register, redirect: redirectLoggedIn(), }, /** Workflow editor */ { path: "/workflows/edit", component: WorkflowEditorModule, redirect: redirectAnon(), }, /** Published resources routes */ { path: "/published/history", component: HistoryPublished, props: (route) => ({ id: route.query.id }), }, { path: "/published/page", component: PageView, props: (route) => ({ pageId: route.query.id, embed: route.query.embed ? parseBool(route.query.embed) : undefined, showHeading: route.query.heading ? parseBool(route.query.heading) : undefined, }), }, { path: "/published/visualization", component: VisualizationPublished, props: (route) => ({ id: route.query.id }), }, { path: "/published/workflow", component: WorkflowPublished, props: (route) => ({ id: route.query.id, version: route.query.version, zoom: route.query.zoom ? parseFloat(route.query.zoom) : undefined, embed: route.query.embed ? parseBool(route.query.embed) : undefined, showButtons: route.query.buttons ? parseBool(route.query.buttons) : undefined, showAbout: route.query.about ? parseBool(route.query.about) : undefined, showHeading: route.query.heading ? parseBool(route.query.heading) : undefined, showMinimap: route.query.minimap ? parseBool(route.query.minimap) : undefined, showZoomControls: route.query.zoom_controls ? parseBool(route.query.zoom_controls) : undefined, initialX: route.query.initialX ? parseInt(route.query.initialX) : undefined, initialY: route.query.initialY ? parseInt(route.query.initialY) : undefined, }), }, { name: "error", path: "/client-error/", component: ClientError, props: true, }, /** Analysis routes */ { path: "/", component: Analysis, children: [ ...AdminRoutes, ...LibraryRoutes, ...StorageRoutes, { path: "", alias: "root", component: Home, props: (route) => ({ config: Galaxy.config, query: route.query }), }, { path: "about", component: AboutGalaxy, }, { path: "upload", component: UploadPage, }, { path: "upload/progress", component: UploadProgress, }, { path: "upload/:methodId", component: UploadMethodView, props: true, }, { path: "help/terms/:term", component: HelpTerm, props: true, }, { path: "carbon_emissions_calculations", component: CarbonEmissionsCalculations, }, { path: "custom_builds", component: CustomBuilds, redirect: redirectAnon(), }, { path: "collection/new_list", component: ListWizard, props: (route) => ({ initialAdvanced: parseBool(route.query.advanced), }), }, { path: "collection/:collectionId/edit", component: CollectionEditView, props: true, }, { path: "collection/:collectionId/sheet", component: DisplayCollectionAsSheet, props: true, }, { path: "datasets/copy", component: DatasetCopy, }, { path: "datasets/list", component: DatasetList, }, { path: "datasets/:datasetId/report", component: ToolReport, props: true, }, { // legacy route, potentially used by 3rd parties path: "datasets/:datasetId/show_params", component: DatasetDetails, props: true, }, { // Consolidated route for dataset view with optional tab // Handles /datasets/{id}, /datasets/{id}/details, /datasets/{id}/visualize, etc. path: "datasets/:datasetId/:tab?", name: "DatasetDetails", component: DatasetView, props: (route) => ({ datasetId: route.params.datasetId, tab: route.params.tab, displayOnly: route.query.displayOnly === "true", }), }, { path: "datatypes", component: AvailableDatatypes, }, { path: "display_applications/:datasetId/:appName/:linkName", component: DisplayApplication, props: true, redirect: redirectAnon(), }, { path: "histories/import", component: HistoryImport, }, { path: "histories/citations", component: CitationsList, props: (route) => ({ id: route.query.id, source: "histories", }), }, { path: "histories/rename", component: FormGeneric, props: (route) => ({ url: `/history/rename?id=${route.query.id}`, redirect: "/histories/list", }), }, { path: "histories/sharing", component: HistoryAccessibility, props: (route) => ({ historyId: route.query.id, }), }, { path: "histories/permissions", component: HistoryDatasetPermissions, props: (route) => ({ historyId: route.query.id, }), }, { path: "histories/view", component: HistoryView, props: (route) => ({ id: route.query.id, }), }, { path: "histories/view_multiple", component: HistoryMultipleView, props: true, redirect: redirectAnon(), }, { path: "histories/list_published", component: HistoryList, props: (route) => ({ activeList: "published", username: route.query["f-username"], }), }, { path: "histories/archived", component: HistoryList, props: { activeList: "archived", }, redirect: redirectAnon(), }, { path: "histories/list", component: HistoryList, props: { activeList: "my", }, redirect: redirectAnon("/histories/list_published"), }, { path: "histories/list_shared", component: HistoryList, props: { activeList: "shared", }, redirect: redirectAnon(), }, { path: "histories/:historyId/export", get component() { return Galaxy.config.enable_celery_tasks ? HistoryExportTasks : HistoryExport; }, props: true, }, { path: "histories/:historyId/archive", component: HistoryArchiveWizard, props: true, }, { path: "histories/:historyId/invocations", component: HistoryInvocations, props: true, }, { path: "interactivetool_entry_points/list", component: InteractiveTools, }, { path: "interactivetool_entry_points/:entryId/display", component: InteractiveToolFrame, props: true, name: "InteractiveToolDisplay", }, { path: "jobs/submission/success", component: ToolSuccess, props: true, }, { path: "jobs/:jobId/view", component: JobDetails, props: true, }, { path: "object_store_instances/create", component: CreateUserObjectStore, }, { path: "object_store_instances/index", component: ManageObjectStoreIndex, props: (route) => { return { message: route.query["message"] }; }, }, { path: "object_store_instances/:instanceId/edit", component: EditObjectStoreInstance, props: true, }, { path: "object_store_instances/:instanceId/upgrade", component: UpgradeObjectStoreInstance, props: true, }, { path: "object_store_templates/:templateId/new", component: CreateObjectStoreInstance, props: true, }, { path: "file_source_instances/create", component: CreateUserFileSource, props: (route) => { return { error: route.params.error, }; }, }, { path: "file_source_instances/index", component: ManageFileSourceIndex, props: (route) => { return { message: route.query["message"] }; }, }, { path: "file_source_instances/:instanceId/edit", component: EditFileSourceInstance, props: true, }, { path: "file_source_instances/:instanceId/upgrade", component: UpgradeFileSourceInstance, props: true, }, { path: "file_source_templates/:templateId/new", component: CreateFileSourceInstance, props: (route) => ({ templateId: route.params.templateId, uuid: route.query.uuid, }), }, { path: "pages/create", component: PageForm, props: (route) => ({ invocationId: route.query.invocation_id, mode: "create", }), }, { path: "pages/edit", component: PageForm, props: (route) => ({ id: route.query.id, mode: "edit", }), }, { path: "/pages/editor", component: PageEditor, props: (route) => ({ pageId: route.query.id, }), }, { path: "/tools/editor", component: CustomToolEditor, redirect: redirectAnon(), }, { path: "/tools/editor/:toolUuid", component: CustomToolEditor, redirect: redirectAnon(), props: true, }, { path: "pages/sharing", component: Sharing, props: (route) => ({ id: route.query.id, pluralName: "Pages", modelClass: "Page", }), }, { path: "pages/list", component: GridPage, props: { activeList: "my", }, redirect: redirectAnon("/pages/list_published"), }, { path: "pages/list_published", component: GridPage, props: (route) => ({ activeList: "published", username: route.query["f-username"], }), }, { path: "storage/history/:historyId", name: "HistoryOverviewInAnalysis", component: HistoryStorageOverview, props: true, }, { path: "tours", component: TourList, }, { path: "chatgxy/:exchangeId?", component: ChatGXY, redirect: redirectAnon(), props: (route) => ({ exchangeId: route.params.exchangeId || undefined, compact: route.query.compact === "true", }), }, { path: "wizard", component: GalaxyWizard, }, { path: "tours/:tourId", component: CenterFrame, props: (route) => ({ src: "/welcome", }), }, { path: "rules", component: RulesStandalone, props: (route) => { return { mode: "standalone", ...route.query, }; }, }, { path: "tools/list", component: ToolsList, props: (route) => { return { ...route.query, }; }, }, { path: "tools/list/ontologies", component: ToolOntologies, props: true, }, { path: "tools/json", component: ToolsJson, }, { path: "tool_landings/:uuid", component: ToolLanding, props: (route) => ({ uuid: route.params.uuid, public: Boolean(route.query.public), secret: route.query.client_secret, }), beforeEnter: requireAuth, }, { path: "workflow_landings/:uuid", component: WorkflowLanding, props: (route) => ({ uuid: route.params.uuid, public: (route.query.public || "").toLowerCase() === "true", secret: route.query.client_secret, }), beforeEnter: requireAuth, }, { path: "user", component: UserPreferences, redirect: redirectAnon(), }, { path: "user/api_key", component: APIKey, redirect: redirectAnon(), }, { path: "user/credentials", component: CredentialsManagement, redirect: redirectAnon(), }, { path: "user/oidc-profile", component: UserOidcProfile, redirect: redirectIf( !Galaxy.config.enable_oidc || Galaxy.config.enable_account_interface || !hasSingleOidcProfile(Galaxy.config.oidc), "/user", ) || redirectAnon(), }, { path: "user/external_ids", component: ExternalIdentities, redirect: redirectIf(Galaxy.config.fixed_delegated_auth, "/") || redirectAnon(), }, { path: "user/notifications", component: NotificationsList, redirect: redirectIf(!Galaxy.config.enable_notification_system, "/") || redirectAnon(), props: (route) => ({ shouldOpenPreferences: Boolean(route.query.preferences), }), }, { path: "user/notifications/preferences", component: NotificationsPreferences, redirect: redirectAnon(), }, { path: "user/permissions", component: UserDatasetPermissions, redirect: redirectAnon(), props: { userId: Galaxy.user.id, }, }, { path: "user/:formId", component: UserPreferencesForm, props: (route) => ({ formId: route.params.formId, id: route.query.id, }), redirect: redirectAnon(), }, { path: "visualizations", component: VisualizationsList, props: (route) => ({ datasetId: route.query.dataset_id, }), }, { path: "visualizations/create/:visualization", component: VisualizationCreate, name: "VisualizationsCreate", props: true, }, { path: "visualizations/display", component: VisualizationDisplay, name: "VisualizationsDisplay", props: (route) => ({ datasetId: route.query.dataset_id, visualization: route.query.visualization, visualizationId: route.query.visualization_id, }), }, { path: "visualizations/edit", component: FormGeneric, props: (route) => ({ url: `/visualization/edit?id=${route.query.id}`, redirect: "/visualizations/list", active_tab: "visualization", }), }, { path: "visualizations/sharing", component: Sharing, props: (route) => ({ id: route.query.id, pluralName: "Visualizations", modelClass: "Visualization", }), }, { path: "visualizations/list", component: GridVisualization, props: { activeList: "my", }, redirect: redirectAnon("/visualizations/list_published"), }, { path: "visualizations/list_published", component: GridVisualization, props: (route) => ({ activeList: "published", username: route.query["f-username"], }), }, { path: "visualizations/list_shared", component: GridVisualization, props: { activeList: "shared", }, redirect: redirectAnon(), }, { path: "workflows/create", component: WorkflowCreate, redirect: redirectAnon(), }, { path: "workflows/export", component: WorkflowExport, props: (route) => ({ id: route.query.id, }), }, { path: "workflows/import", component: WorkflowImport, redirect: redirectAnon(), }, { path: "workflows/trs_import", component: WorkflowImport, redirect: redirectAnon(), }, { path: "workflows/trs_search", component: TrsSearch, redirect: redirectAnon(), }, { path: "workflows/invocations", component: GridInvocation, redirect: redirectAnon(), }, { path: "workflows/invocations/import", component: HistoryImport, props: { invocationImport: true, }, }, { path: "workflows/invocations/report", component: InvocationReport, props: (route) => ({ invocationId: route.query.id, }), }, { // Consolidated route for workflow invocation state with optional success query param // Handles /workflows/invocations/{id}, /workflows/invocations/{id}/steps, /workflows/invocations/{id}/inputs, etc. path: "workflows/invocations/:invocationId/:tab?", component: WorkflowInvocationState, props: (route) => ({ invocationId: route.params.invocationId, tab: route.params.tab, isFullPage: true, success: Boolean(route.query.success), }), }, { path: "workflows/list", component: WorkflowList, redirect: redirectAnon("/workflows/list_published"), }, { path: "workflows/list_published", component: WorkflowList, props: (route) => ({ activeList: "published", query: { ...route.query }, }), }, { path: "workflows/list_shared_with_me", component: WorkflowList, redirect: redirectAnon(), props: (route) => ({ activeList: "shared_with_me", query: { ...route.query }, }), }, { path: "workflows/run", component: WorkflowRun, redirect: redirectAnon(), props: (route) => ({ workflowId: route.query.id, version: route.query.version, instance: route.query.instance, preferSimpleForm: Galaxy.config.simplified_workflow_run_ui === "prefer", simpleFormTargetHistory: Galaxy.config.simplified_workflow_run_ui_target_history, simpleFormUseJobCache: Galaxy.config.simplified_workflow_run_ui_job_cache === "on", }), }, { path: "workflows/rerun", component: WorkflowRerun, redirect: redirectAnon(), props: (route) => ({ invocationId: route.query.invocation_id, }), }, { path: "workflows/sharing", component: Sharing, props: (route) => ({ id: route.query.id, pluralName: "Workflows", modelClass: "Workflow", }), }, { path: "workflows/:storedWorkflowId/invocations", component: StoredWorkflowInvocations, props: true, }, { path: "import/zip", name: "ZipImportWizard", component: ZipImportWizard, props: true, redirect: redirectAnon(), }, { path: "import/zip/results", name: "ZipImportResults", component: ZipImportResults, props: (route) => ({ workflowFileCount: Number(route.params.workflowFileCount), regularFileCount: Number(route.params.regularFileCount), }), redirect: redirectAnon(), }, { path: "downloads", name: "RecentDownloads", component: RecentDownloads, redirect: redirectAnon(), }, ], }, ], }); function checkAdminAccessRequired(to) { // Check parent route hierarchy to see if we require admin access here. // Access is required if *any* component in the hierarchy requires it. if (to.matched.some((record) => record.meta.requiresAdmin === true)) { const isAdmin = getGalaxyInstance()?.user?.isAdmin(); return !isAdmin; } return false; } function checkRegisteredUserAccessRequired(to) { // Check parent route hierarchy to see if we require registered user access here. // Access is required if *any* component in the hierarchy requires it. if (to.matched.some((record) => record.meta.requiresRegisteredUser === true)) { const isAnonymous = getGalaxyInstance()?.user?.isAnonymous(); return isAnonymous; } return false; } router.beforeEach(async (to, from, next) => { // TODO: merge anon redirect functionality here for more standard handling const isAdminAccessRequired = checkAdminAccessRequired(to); if (isAdminAccessRequired) { const error = new Error(`Admin access required for '${to.path}'.`); error.name = "AdminRequired"; next(error); } const isRegisteredUserAccessRequired = checkRegisteredUserAccessRequired(to); if (isRegisteredUserAccessRequired) { const error = new Error(`Registered user access required for '${to.path}'.`); error.name = "RegisteredUserRequired"; next(error); } next(); }); router.onError((error) => { router.push({ name: "error", params: { error: error } }); }); return router; }