diff --git a/.github/labeler.yml b/.github/labeler.yml index 0736c48376fb..fc5c7eb38d11 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -63,7 +63,7 @@ area/toolshed: - lib/toolshed/**/* - templates/webapps/tool_shed/**/* area/UI-UX: - - all: ["client/src/**/*", "!client/src/schema/schema.ts"] + - all: ["client/src/**/*", "!client/src/api/schema/schema.ts"] any: ["templates/**/*"] area/util: - lib/galaxy/util/**/* diff --git a/.github/workflows/lint_openapi_schema.yml b/.github/workflows/lint_openapi_schema.yml index 02cfb35e640d..32ddedf5cd7d 100644 --- a/.github/workflows/lint_openapi_schema.yml +++ b/.github/workflows/lint_openapi_schema.yml @@ -57,7 +57,7 @@ jobs: - name: Check for changes run: | if [[ `git status --porcelain` ]]; then - echo "Rebuilding client/src/schema/schema.ts resulted in changes, run 'make update-client-api-schema' and commit results" + echo "Rebuilding client/src/api/schema/schema.ts resulted in changes, run 'make update-client-api-schema' and commit results" exit 1 fi working-directory: 'galaxy root' diff --git a/Makefile b/Makefile index f20548df435e..28f320e6f44a 100644 --- a/Makefile +++ b/Makefile @@ -189,7 +189,7 @@ remove-api-schema: rm _shed_schema.yaml update-client-api-schema: client-node-deps build-api-schema - $(IN_VENV) cd client && node openapi_to_schema.mjs ../_schema.yaml > src/schema/schema.ts && npx prettier --write src/schema/schema.ts + $(IN_VENV) cd client && node openapi_to_schema.mjs ../_schema.yaml > src/api/schema/schema.ts && npx prettier --write src/api/schema/schema.ts $(IN_VENV) cd client && node openapi_to_schema.mjs ../_shed_schema.yaml > ../lib/tool_shed/webapp/frontend/src/schema/schema.ts && npx prettier --write ../lib/tool_shed/webapp/frontend/src/schema/schema.ts $(MAKE) remove-api-schema diff --git a/client/docs/querying-the-api.md b/client/docs/querying-the-api.md new file mode 100644 index 000000000000..63dc48b873ea --- /dev/null +++ b/client/docs/querying-the-api.md @@ -0,0 +1,86 @@ +# Best practices when querying the API from UI components + +If you need to query the API from a component, there are several ways to do so. This document will help you decide which one to use and provide some best practices when doing so to keep the code clean and maintainable. + +## Choose the Right Approach + +When querying APIs in Vue components, consider the following approaches and their best practices: + +## 1. Prefer Composables over Stores over Direct API Calls + +### Composables + +- **What Are Composables?**: please read the official Vue documentation on composables [here](https://vuejs.org/guide/reusability/composables.html). + +If there is already a composable that takes care of the logic you need for accessing a particular resource in the API please use it or consider writing a new one. They provide a type-safe interface and a higher level of abstraction than the related Store or the API itself. They might rely on one or more Stores for caching and reactivity. + +### Stores + +- **Stores Explained**: Please read the official Vue documentation on State Management [here](https://vuejs.org/guide/scaling-up/state-management.html). + +If there is no Composable for the API endpoint you are using, try using a (Pinia) Store instead. Stores are type-safe and provide a reactive interface to the API. They can also be used to cache data, ensuring a single source of truth. + +- **Use Pinia Stores**: If you need to create a new Store, make sure to create a Pinia Store, and not a Vuex Store as Pinia will be the replacement for Vuex. Also, use [Composition API syntax over the Options API](https://vuejs.org/guide/extras/composition-api-faq.html) for increased readability, code organization, type inference, etc. + +### Direct API Calls + +- If the type of data you are querying should not be cached, or you just need to update or create new data, you can use the API directly. Make sure to use the **Fetcher** (see below) instead of Axios, as it provides a type-safe interface to the API along with some extra benefits. + +## 2. Prefer Fetcher over Axios (when possible) + +- **Use Fetcher with OpenAPI Specs**: If there is an OpenAPI spec for the API endpoint you are using (in other words, there is a FastAPI route defined in Galaxy), always use the Fetcher. It will provide you with a type-safe interface to the API. + +**Do** + +```typescript +import { fetcher } from "@/api/schema"; +const datasetsFetcher = fetcher.path("/api/dataset/{id}").method("get").create(); + +const { data: dataset } = await datasetsFetcher({ id: "testID" }); +``` + +**Don't** + +```js +import axios from "axios"; +import { getAppRoot } from "onload/loadConfig"; +import { rethrowSimple } from "utils/simple-error"; + +async getDataset(datasetId) { + const url = `${getAppRoot()}api/datasets/${datasetId}`; + try { + const response = await axios.get(url); + return response.data; + } catch (e) { + rethrowSimple(e); + } +} + +const dataset = await getDataset("testID"); +``` + +> **Reason** +> +> The `fetcher` class provides a type-safe interface to the API, and is already configured to use the correct base URL and error handling. + +## 3. Where to put your API queries? + +The short answer is: **it depends**. There are several factors to consider when deciding where to put your API queries: + +### Is the data you are querying related exclusively to a particular component? + +If so, you should put the query in the component itself. If not, you should consider putting it in a Composable or a Store. + +### Can the data be cached? + +If so, you should consider putting the query in a Store. If not, you should consider putting it in a Composable or the component itself. + +### Is the query going to be used in more than one place? + +If so, you should consider putting it under src/api/.ts and exporting it from there. This will allow you to reuse the query in multiple places specially if you need to do some extra processing of the data. Also it will help to keep track of what parts of the API are being used and where. + +### Should I use the `fetcher` directly or should I write a wrapper function? + +- If you **don't need to do any extra processing** of the data, you can use the `fetcher` directly. +- If you **need to do some extra processing**, you should consider writing a wrapper function. Extra processing can be anything from transforming the data to adding extra parameters to the query or omitting some of them, handling conditional types in response data, etc. +- Using a **wrapper function** will help in case we decide to replace the `fetcher` with something else in the future (as we are doing now with _Axios_). diff --git a/client/src/stores/services/datasetCollection.service.ts b/client/src/api/datasetCollections.ts similarity index 85% rename from client/src/stores/services/datasetCollection.service.ts rename to client/src/api/datasetCollections.ts index c52aabfc3350..50af420d3442 100644 --- a/client/src/stores/services/datasetCollection.service.ts +++ b/client/src/api/datasetCollections.ts @@ -1,13 +1,16 @@ -import { fetcher } from "@/schema"; - -import { CollectionEntry, DatasetCollectionAttributes, DCESummary, HDCADetailed, isHDCA } from "."; +import { CollectionEntry, DatasetCollectionAttributes, DCESummary, HDCADetailed, isHDCA } from "@/api"; +import { fetcher } from "@/api/schema"; const DEFAULT_LIMIT = 50; const getCollectionDetails = fetcher.path("/api/dataset_collections/{id}").method("get").create(); -export async function fetchCollectionDetails(params: { hdcaId: string }): Promise { - const { data } = await getCollectionDetails({ id: params.hdcaId }); +/** + * Fetches the details of a collection. + * @param params.id The ID of the collection (HDCA) to fetch. + */ +export async function fetchCollectionDetails(params: { id: string }): Promise { + const { data } = await getCollectionDetails({ id: params.id }); return data; } diff --git a/client/src/api/datasets.ts b/client/src/api/datasets.ts new file mode 100644 index 000000000000..09948b45ce26 --- /dev/null +++ b/client/src/api/datasets.ts @@ -0,0 +1,89 @@ +import type { FetchArgType } from "openapi-typescript-fetch"; + +import { DatasetDetails } from "@/api"; +import { fetcher } from "@/api/schema"; +import { withPrefix } from "@/utils/redirect"; + +export const datasetsFetcher = fetcher.path("/api/datasets").method("get").create(); + +type GetDatasetsApiOptions = FetchArgType; +type GetDatasetsQuery = Pick; +// custom interface for how we use getDatasets +interface GetDatasetsOptions extends GetDatasetsQuery { + sortBy?: string; + sortDesc?: string; + query?: string; +} + +/** Datasets request helper **/ +export async function getDatasets(options: GetDatasetsOptions = {}) { + const params: GetDatasetsApiOptions = {}; + if (options.sortBy) { + const sortPrefix = options.sortDesc ? "-dsc" : "-asc"; + params.order = `${options.sortBy}${sortPrefix}`; + } + if (options.limit) { + params.limit = options.limit; + } + if (options.offset) { + params.offset = options.offset; + } + if (options.query) { + params.q = ["name-contains"]; + params.qv = [options.query]; + } + const { data } = await datasetsFetcher(params); + return data; +} + +const getDataset = fetcher.path("/api/datasets/{dataset_id}").method("get").create(); + +export async function fetchDatasetDetails(params: { id: string }): Promise { + const { data } = await getDataset({ dataset_id: params.id, view: "detailed" }); + // We know that the server will return a DatasetDetails object because of the view parameter + // but the type system doesn't, so we have to cast it. + return data as unknown as DatasetDetails; +} + +const updateHistoryDataset = fetcher.path("/api/histories/{history_id}/contents/{type}s/{id}").method("put").create(); + +export async function undeleteHistoryDataset(historyId: string, datasetId: string) { + const { data } = await updateHistoryDataset({ + history_id: historyId, + id: datasetId, + type: "dataset", + deleted: false, + }); + return data; +} + +const deleteHistoryDataset = fetcher + .path("/api/histories/{history_id}/contents/{type}s/{id}") + .method("delete") + .create(); + +export async function purgeHistoryDataset(historyId: string, datasetId: string) { + const { data } = await deleteHistoryDataset({ history_id: historyId, id: datasetId, type: "dataset", purge: true }); + return data; +} + +const datasetCopy = fetcher.path("/api/histories/{history_id}/contents/{type}s").method("post").create(); +type HistoryContentsArgs = FetchArgType; +export async function copyDataset( + datasetId: HistoryContentsArgs["content"], + historyId: HistoryContentsArgs["history_id"], + type: HistoryContentsArgs["type"] = "dataset", + source: HistoryContentsArgs["source"] = "hda" +) { + const response = await datasetCopy({ + history_id: historyId, + type, + source: source, + content: datasetId, + }); + return response.data; +} + +export function getCompositeDatasetLink(historyDatasetId: string, path: string) { + return withPrefix(`/api/datasets/${historyDatasetId}/display?filename=${path}`); +} diff --git a/client/src/api/datatypes.ts b/client/src/api/datatypes.ts new file mode 100644 index 000000000000..fde4da5b529b --- /dev/null +++ b/client/src/api/datatypes.ts @@ -0,0 +1,13 @@ +import { fetcher } from "@/api/schema"; + +export const datatypesFetcher = fetcher.path("/api/datatypes").method("get").create(); + +export const edamFormatsFetcher = fetcher.path("/api/datatypes/edam_formats/detailed").method("get").create(); +export const edamDataFetcher = fetcher.path("/api/datatypes/edam_data/detailed").method("get").create(); + +const typesAndMappingsFetcher = fetcher.path("/api/datatypes/types_and_mapping").method("get").create(); + +export async function fetchDatatypesAndMappings(upload_only = true) { + const { data } = await typesAndMappingsFetcher({ upload_only }); + return data; +} diff --git a/client/src/api/dbKeys.ts b/client/src/api/dbKeys.ts new file mode 100644 index 000000000000..ec1525137883 --- /dev/null +++ b/client/src/api/dbKeys.ts @@ -0,0 +1,8 @@ +/** + * Historically, this API was used to get the list of genomes that were available + * but now it is used to get the list of more generic "dbkeys". + */ + +import { fetcher } from "@/api/schema"; + +export const dbKeysFetcher = fetcher.path("/api/genomes").method("get").create(); diff --git a/client/src/api/groups.ts b/client/src/api/groups.ts new file mode 100644 index 000000000000..4f0c3e236e87 --- /dev/null +++ b/client/src/api/groups.ts @@ -0,0 +1,9 @@ +import axios from "axios"; + +import { components } from "@/api/schema"; + +type GroupModel = components["schemas"]["GroupModel"]; +export async function getAllGroups(): Promise { + const { data } = await axios.get("/api/groups"); + return data; +} diff --git a/client/src/stores/services/historyArchive.services.ts b/client/src/api/histories.archived.ts similarity index 88% rename from client/src/stores/services/historyArchive.services.ts rename to client/src/api/histories.archived.ts index 5c5b98448ff8..518ab55a72a7 100644 --- a/client/src/stores/services/historyArchive.services.ts +++ b/client/src/api/histories.archived.ts @@ -1,6 +1,6 @@ import type { FetchArgType } from "openapi-typescript-fetch"; -import { type components, fetcher } from "@/schema"; +import { type components, fetcher } from "@/api/schema"; export type ArchivedHistorySummary = components["schemas"]["ArchivedHistorySummary"]; export type ArchivedHistoryDetailed = components["schemas"]["ArchivedHistoryDetailed"]; @@ -55,7 +55,7 @@ export async function fetchArchivedHistories( }; } -const archiveHistory = fetcher.path("/api/histories/{history_id}/archive").method("post").create(); +const postArchiveHistory = fetcher.path("/api/histories/{history_id}/archive").method("post").create(); /** * Archive a history. @@ -64,12 +64,12 @@ const archiveHistory = fetcher.path("/api/histories/{history_id}/archive").metho * @param purgeHistory Whether to purge the history after archiving. Can only be used in combination with an archive export record. * @returns The archived history summary. */ -export async function archiveHistoryById( +export async function archiveHistory( historyId: string, archiveExportId?: string, purgeHistory?: boolean ): Promise { - const { data } = await archiveHistory({ + const { data } = await postArchiveHistory({ history_id: historyId, archive_export_id: archiveExportId, purge_history: purgeHistory, @@ -77,7 +77,7 @@ export async function archiveHistoryById( return data as ArchivedHistorySummary; } -const unarchiveHistory = fetcher +const putUnarchiveHistory = fetcher .path("/api/histories/{history_id}/archive/restore") .method("put") // @ts-ignore: workaround for optional query parameters in PUT. More info here https://github.com/ajaishankar/openapi-typescript-fetch/pull/55 @@ -89,8 +89,8 @@ const unarchiveHistory = fetcher * @param force Whether to force un-archiving for purged histories. * @returns The restored history summary. */ -export async function unarchiveHistoryById(historyId: string, force?: boolean): Promise { - const { data } = await unarchiveHistory({ history_id: historyId, force }); +export async function unarchiveHistory(historyId: string, force?: boolean): Promise { + const { data } = await putUnarchiveHistory({ history_id: historyId, force }); return data as ArchivedHistorySummary; } @@ -102,7 +102,7 @@ const reimportHistoryFromStore = fetcher.path("/api/histories/from_store_async") * @param archivedHistory The archived history to reimport. It must have an associated export record. * @returns The async task result summary to track the reimport progress. */ -export async function reimportHistoryFromExportRecordAsync( +export async function reimportArchivedHistoryFromExportRecord( archivedHistory: ArchivedHistorySummary ): Promise { if (!archivedHistory.export_record_data) { diff --git a/client/src/components/History/Export/services.ts b/client/src/api/histories.export.ts similarity index 93% rename from client/src/components/History/Export/services.ts rename to client/src/api/histories.export.ts index 636c88771cb6..03f7d339eb85 100644 --- a/client/src/components/History/Export/services.ts +++ b/client/src/api/histories.export.ts @@ -1,8 +1,8 @@ +import type { components } from "@/api/schema"; +import { fetcher } from "@/api/schema"; import type { ObjectExportTaskResponse } from "@/components/Common/models/exportRecordModel"; import { ExportRecordModel } from "@/components/Common/models/exportRecordModel"; import { DEFAULT_EXPORT_PARAMS } from "@/composables/shortTermStorage"; -import type { components } from "@/schema"; -import { fetcher } from "@/schema"; type ModelStoreFormat = components["schemas"]["ModelStoreFormat"]; @@ -24,7 +24,7 @@ export const AVAILABLE_EXPORT_FORMATS: { id: ModelStoreFormat; name: string }[] * @param params query and pagination params * @returns a promise with a list of export records associated with the given history. */ -export async function getExportRecords(historyId: string) { +export async function fetchHistoryExportRecords(historyId: string) { const response = await _getExportRecords( { history_id: historyId, @@ -46,7 +46,7 @@ export async function getExportRecords(historyId: string) { * @param exportParams additional parameters to configure the export * @returns A promise with the request response */ -export async function exportToFileSource( +export async function exportHistoryToFileSource( historyId: string, exportDirectory: string, fileName: string, diff --git a/client/src/api/histories.ts b/client/src/api/histories.ts new file mode 100644 index 000000000000..65ad7e2d6b1d --- /dev/null +++ b/client/src/api/histories.ts @@ -0,0 +1,6 @@ +import { fetcher } from "@/api/schema"; + +export const historiesFetcher = fetcher.path("/api/histories").method("get").create(); +export const archivedHistoriesFetcher = fetcher.path("/api/histories/archived").method("get").create(); +export const undeleteHistory = fetcher.path("/api/histories/deleted/{history_id}/undelete").method("post").create(); +export const purgeHistory = fetcher.path("/api/histories/{history_id}").method("delete").create(); diff --git a/client/src/stores/services/index.ts b/client/src/api/index.ts similarity index 89% rename from client/src/stores/services/index.ts rename to client/src/api/index.ts index 5cea4cd8e8da..c999753323be 100644 --- a/client/src/stores/services/index.ts +++ b/client/src/api/index.ts @@ -1,4 +1,16 @@ -import { components } from "@/schema"; +/** Contains type alias and definitions related to Galaxy API models. */ + +import { components } from "@/api/schema"; + +/** + * Contains minimal information about a History. + */ +export type HistorySummary = components["schemas"]["HistorySummary"]; + +/** + * Contains additional details about a History. + */ +export type HistoryDetailed = components["schemas"]["HistoryDetailed"]; /** * Contains minimal information about a HistoryContentItem. diff --git a/client/src/api/jobs.ts b/client/src/api/jobs.ts new file mode 100644 index 000000000000..b96b65352600 --- /dev/null +++ b/client/src/api/jobs.ts @@ -0,0 +1,4 @@ +import { fetcher } from "@/api/schema"; + +export const jobLockStatus = fetcher.path("/api/job_lock").method("get").create(); +export const jobLockUpdate = fetcher.path("/api/job_lock").method("put").create(); diff --git a/client/src/components/admin/Notifications/broadcasts.services.ts b/client/src/api/notifications.broadcast.ts similarity index 61% rename from client/src/components/admin/Notifications/broadcasts.services.ts rename to client/src/api/notifications.broadcast.ts index a62dbb267363..cbea28ada102 100644 --- a/client/src/components/admin/Notifications/broadcasts.services.ts +++ b/client/src/api/notifications.broadcast.ts @@ -1,16 +1,16 @@ -import { type components, fetcher } from "@/schema"; +import { type components, fetcher } from "@/api/schema"; type BroadcastNotificationResponse = components["schemas"]["BroadcastNotificationResponse"]; -const getBroadcast = fetcher.path("/api/notifications/broadcast/{notification_id}").method("get").create(); -export async function loadBroadcast(id: string): Promise { - const { data } = await getBroadcast({ notification_id: id }); +const broadcastFetcher = fetcher.path("/api/notifications/broadcast/{notification_id}").method("get").create(); +export async function fetchBroadcast(id: string): Promise { + const { data } = await broadcastFetcher({ notification_id: id }); return data; } -const getBroadcasts = fetcher.path("/api/notifications/broadcast").method("get").create(); -export async function loadBroadcasts(): Promise { - const { data } = await getBroadcasts({}); +const broadcastsFetcher = fetcher.path("/api/notifications/broadcast").method("get").create(); +export async function fetchAllBroadcasts(): Promise { + const { data } = await broadcastsFetcher({}); return data; } diff --git a/client/src/components/User/Notifications/model/services.ts b/client/src/api/notifications.preferences.ts similarity index 81% rename from client/src/components/User/Notifications/model/services.ts rename to client/src/api/notifications.preferences.ts index 2c7412a12d39..59b910d8c346 100644 --- a/client/src/components/User/Notifications/model/services.ts +++ b/client/src/api/notifications.preferences.ts @@ -1,4 +1,6 @@ -import { type components, fetcher } from "@/schema"; +import { type components, fetcher } from "@/api/schema"; + +export type UserNotificationPreferences = components["schemas"]["UserNotificationPreferences"]; const getNotificationsPreferences = fetcher.path("/api/notifications/preferences").method("get").create(); export async function getNotificationsPreferencesFromServer() { diff --git a/client/src/api/notifications.ts b/client/src/api/notifications.ts new file mode 100644 index 000000000000..2cfaafed6967 --- /dev/null +++ b/client/src/api/notifications.ts @@ -0,0 +1,72 @@ +import { type components, fetcher } from "@/api/schema"; + +export type BaseUserNotification = components["schemas"]["UserNotificationResponse"]; + +export interface MessageNotification extends BaseUserNotification { + category: "message"; + content: components["schemas"]["MessageNotificationContent"]; +} + +export interface SharedItemNotification extends BaseUserNotification { + category: "new_shared_item"; + content: components["schemas"]["NewSharedItemNotificationContent"]; +} + +export type UserNotification = MessageNotification | SharedItemNotification; + +export type NotificationChanges = components["schemas"]["UserNotificationUpdateRequest"]; + +export type UserNotificationsBatchUpdateRequest = components["schemas"]["UserNotificationsBatchUpdateRequest"]; + +export type NotificationVariants = components["schemas"]["NotificationVariant"]; + +export type NewSharedItemNotificationContentItemType = + components["schemas"]["NewSharedItemNotificationContent"]["item_type"]; + +type UserNotificationUpdateRequest = components["schemas"]["UserNotificationUpdateRequest"]; + +type NotificationCreateRequest = components["schemas"]["NotificationCreateRequest"]; + +type NotificationResponse = components["schemas"]["NotificationResponse"]; + +const getNotification = fetcher.path("/api/notifications/{notification_id}").method("get").create(); + +export async function loadNotification(id: string): Promise { + const { data } = await getNotification({ notification_id: id }); + return data; +} + +const postNotification = fetcher.path("/api/notifications").method("post").create(); + +export async function sendNotification(notification: NotificationCreateRequest) { + const { data } = await postNotification(notification); + return data; +} + +const putNotification = fetcher.path("/api/notifications/{notification_id}").method("put").create(); + +export async function updateNotification(id: string, notification: UserNotificationUpdateRequest) { + const { data } = await putNotification({ notification_id: id, ...notification }); + return data; +} + +const getNotifications = fetcher.path("/api/notifications").method("get").create(); + +export async function loadNotificationsFromServer(): Promise { + const { data } = await getNotifications({}); + return data as UserNotification[]; +} + +const putBatchNotifications = fetcher.path("/api/notifications").method("put").create(); + +export async function updateBatchNotificationsOnServer(request: UserNotificationsBatchUpdateRequest) { + const { data } = await putBatchNotifications(request); + return data; +} + +const getNotificationStatus = fetcher.path("/api/notifications/status").method("get").create(); + +export async function loadNotificationsStatus(since: Date) { + const { data } = await getNotificationStatus({ since: since.toISOString().replace("Z", "") }); + return data; +} diff --git a/client/src/components/ObjectStore/services.ts b/client/src/api/objectStores.ts similarity index 91% rename from client/src/components/ObjectStore/services.ts rename to client/src/api/objectStores.ts index cd9e48068b3d..c24f6a096795 100644 --- a/client/src/components/ObjectStore/services.ts +++ b/client/src/api/objectStores.ts @@ -1,4 +1,4 @@ -import { fetcher } from "@/schema/fetcher"; +import { fetcher } from "@/api/schema"; const getObjectStores = fetcher.path("/api/object_stores").method("get").create(); diff --git a/client/src/api/pages.ts b/client/src/api/pages.ts new file mode 100644 index 000000000000..89e89d2e8940 --- /dev/null +++ b/client/src/api/pages.ts @@ -0,0 +1,9 @@ +import { fetcher } from "@/api/schema"; + +/** Page request helper **/ +const deletePageById = fetcher.path("/api/pages/{id}").method("delete").create(); +export async function deletePage(itemId: string): Promise { + await deletePageById({ + id: itemId, + }); +} diff --git a/client/src/components/FilesDialog/services.ts b/client/src/api/remoteFiles.ts similarity index 91% rename from client/src/components/FilesDialog/services.ts rename to client/src/api/remoteFiles.ts index 116429bc7f32..95ae19c1dd2a 100644 --- a/client/src/components/FilesDialog/services.ts +++ b/client/src/api/remoteFiles.ts @@ -1,5 +1,5 @@ -import type { components } from "@/schema"; -import { fetcher } from "@/schema/fetcher"; +import type { components } from "@/api/schema"; +import { fetcher } from "@/api/schema/fetcher"; /** The browsing mode: * - `file` - allows to select files or directories contained in a source (default) @@ -43,7 +43,7 @@ export async function getFileSources(options: FilterFileSourcesOptions = {}): Pr return data as BrowsableFilesSourcePlugin[]; } -const getRemoteFiles = fetcher.path("/api/remote_files").method("get").create(); +export const remoteFilesFetcher = fetcher.path("/api/remote_files").method("get").create(); /** * Get the list of files and directories from the server for the given file source URI. @@ -53,7 +53,7 @@ const getRemoteFiles = fetcher.path("/api/remote_files").method("get").create(); * @returns The list of files and directories from the server for the given URI. */ export async function browseRemoteFiles(uri: string, isRecursive = false, writeable = false): Promise { - const { data } = await getRemoteFiles({ target: uri, recursive: isRecursive, writeable }); + const { data } = await remoteFilesFetcher({ target: uri, recursive: isRecursive, writeable }); return data as RemoteEntry[]; } diff --git a/client/src/api/roles.ts b/client/src/api/roles.ts new file mode 100644 index 000000000000..61fde1360a92 --- /dev/null +++ b/client/src/api/roles.ts @@ -0,0 +1,7 @@ +import { fetcher } from "@/api/schema"; + +const getRoles = fetcher.path("/api/roles").method("get").create(); +export async function getAllRoles() { + const { data } = await getRoles({}); + return data; +} diff --git a/client/src/schema/__mocks__/fetcher.ts b/client/src/api/schema/__mocks__/fetcher.ts similarity index 89% rename from client/src/schema/__mocks__/fetcher.ts rename to client/src/api/schema/__mocks__/fetcher.ts index c8ddabf907ec..5af41cc7e146 100644 --- a/client/src/schema/__mocks__/fetcher.ts +++ b/client/src/api/schema/__mocks__/fetcher.ts @@ -1,10 +1,10 @@ -import type { paths } from "@/schema"; +import type { paths } from "@/api/schema"; -jest.mock("@/schema", () => ({ +jest.mock("@/api/schema", () => ({ fetcher: mockFetcher, })); -jest.mock("@/schema/fetcher", () => ({ +jest.mock("@/api/schema/fetcher", () => ({ fetcher: mockFetcher, })); @@ -60,9 +60,9 @@ function setMockReturn(path: Path | RegExp, method: Method, value: any) { } /** - * Mock implementation for the fetcher found in `@/schema/fetcher` + * Mock implementation for the fetcher found in `@/api/schema/fetcher` * - * You need to call `jest.mock("@/schema")` and/or `jest.mock("@/schema/fetcher")` + * You need to call `jest.mock("@/api/schema")` and/or `jest.mock("@/api/schema/fetcher")` * (depending on what module the file you are testing imported) * in order for this mock to take effect. * diff --git a/client/src/schema/__mocks__/index.ts b/client/src/api/schema/__mocks__/index.ts similarity index 100% rename from client/src/schema/__mocks__/index.ts rename to client/src/api/schema/__mocks__/index.ts diff --git a/client/src/schema/fetcher.ts b/client/src/api/schema/fetcher.ts similarity index 100% rename from client/src/schema/fetcher.ts rename to client/src/api/schema/fetcher.ts diff --git a/client/src/schema/index.ts b/client/src/api/schema/index.ts similarity index 100% rename from client/src/schema/index.ts rename to client/src/api/schema/index.ts diff --git a/client/src/schema/mockFetcher.test.ts b/client/src/api/schema/mockFetcher.test.ts similarity index 94% rename from client/src/schema/mockFetcher.test.ts rename to client/src/api/schema/mockFetcher.test.ts index ac48c08105ff..e4eddd5a3f58 100644 --- a/client/src/schema/mockFetcher.test.ts +++ b/client/src/api/schema/mockFetcher.test.ts @@ -1,8 +1,8 @@ -import { fetcher } from "@/schema"; +import { fetcher } from "@/api/schema"; import { mockFetcher } from "./__mocks__/fetcher"; -jest.mock("@/schema"); +jest.mock("@/api/schema"); mockFetcher.path("/api/configuration").method("get").mock("CONFIGURATION"); diff --git a/client/src/schema/schema.ts b/client/src/api/schema/schema.ts similarity index 100% rename from client/src/schema/schema.ts rename to client/src/api/schema/schema.ts diff --git a/client/src/api/tags.ts b/client/src/api/tags.ts new file mode 100644 index 000000000000..fe68ca0da137 --- /dev/null +++ b/client/src/api/tags.ts @@ -0,0 +1,13 @@ +import { components, fetcher } from "@/api/schema"; + +type TaggableItemClass = components["schemas"]["TaggableItemClass"]; + +const putItemTags = fetcher.path("/api/tags").method("put").create(); + +export async function updateTags(itemId: string, itemClass: TaggableItemClass, itemTags?: string[]): Promise { + await putItemTags({ + item_id: itemId, + item_class: itemClass, + item_tags: itemTags, + }); +} diff --git a/client/src/api/users.ts b/client/src/api/users.ts new file mode 100644 index 000000000000..d3a2a7ac010f --- /dev/null +++ b/client/src/api/users.ts @@ -0,0 +1,10 @@ +import { fetcher } from "@/api/schema"; + +export const recalculateDiskUsage = fetcher.path("/api/users/current/recalculate_disk_usage").method("put").create(); +export const fetchQuotaUsages = fetcher.path("/api/users/{user_id}/usage").method("get").create(); + +const getUsers = fetcher.path("/api/users").method("get").create(); +export async function getAllUsers() { + const { data } = await getUsers({}); + return data; +} diff --git a/client/src/components/Common/ExportForm.vue b/client/src/components/Common/ExportForm.vue index c3387dcd9991..da769f538923 100644 --- a/client/src/components/Common/ExportForm.vue +++ b/client/src/components/Common/ExportForm.vue @@ -2,7 +2,7 @@ import { BButton, BCol, BFormGroup, BFormInput, BRow } from "bootstrap-vue"; import { computed, ref } from "vue"; -import { FilterFileSourcesOptions } from "@/components/FilesDialog/services"; +import { FilterFileSourcesOptions } from "@/api/remoteFiles"; import localize from "@/utils/localization"; import FilesInput from "@/components/FilesDialog/FilesInput.vue"; diff --git a/client/src/components/Common/ExportRDMForm.test.ts b/client/src/components/Common/ExportRDMForm.test.ts index 7f19404d7ab1..116b1d84d932 100644 --- a/client/src/components/Common/ExportRDMForm.test.ts +++ b/client/src/components/Common/ExportRDMForm.test.ts @@ -2,14 +2,13 @@ import { getLocalVue } from "@tests/jest/helpers"; import { mount, Wrapper } from "@vue/test-utils"; import flushPromises from "flush-promises"; -import { mockFetcher } from "@/schema/__mocks__"; - -import { CreatedEntry } from "../FilesDialog/services"; +import { CreatedEntry } from "@/api/remoteFiles"; +import { mockFetcher } from "@/api/schema/__mocks__"; import ExportRDMForm from "./ExportRDMForm.vue"; import FilesInput from "@/components/FilesDialog/FilesInput.vue"; -jest.mock("@/schema"); +jest.mock("@/api/schema"); const localVue = getLocalVue(true); diff --git a/client/src/components/Common/ExportRDMForm.vue b/client/src/components/Common/ExportRDMForm.vue index 81a75640ca80..4fab58cd1fcc 100644 --- a/client/src/components/Common/ExportRDMForm.vue +++ b/client/src/components/Common/ExportRDMForm.vue @@ -2,7 +2,7 @@ import { BButton, BCard, BFormGroup, BFormInput, BFormRadio, BFormRadioGroup } from "bootstrap-vue"; import { computed, ref } from "vue"; -import { CreatedEntry, createRemoteEntry, FilterFileSourcesOptions } from "@/components/FilesDialog/services"; +import { CreatedEntry, createRemoteEntry, FilterFileSourcesOptions } from "@/api/remoteFiles"; import { useToast } from "@/composables/toast"; import localize from "@/utils/localization"; import { errorMessageAsString } from "@/utils/simple-error"; diff --git a/client/src/components/Common/models/exportRecordModel.ts b/client/src/components/Common/models/exportRecordModel.ts index 82a4458c5e3b..78c07ce5ed83 100644 --- a/client/src/components/Common/models/exportRecordModel.ts +++ b/client/src/components/Common/models/exportRecordModel.ts @@ -1,6 +1,6 @@ import { formatDistanceToNow, parseISO } from "date-fns"; -import type { components } from "@/schema"; +import type { components } from "@/api/schema"; type ExportObjectRequestMetadata = components["schemas"]["ExportObjectRequestMetadata"]; diff --git a/client/src/components/Common/models/testData/exportData.ts b/client/src/components/Common/models/testData/exportData.ts index 8253bff39751..665e51b70e94 100644 --- a/client/src/components/Common/models/testData/exportData.ts +++ b/client/src/components/Common/models/testData/exportData.ts @@ -1,5 +1,5 @@ +import type { components } from "@/api/schema"; import { ExportRecordModel } from "@/components/Common/models/exportRecordModel"; -import type { components } from "@/schema"; type ObjectExportTaskResponse = components["schemas"]["ObjectExportTaskResponse"]; type ExportObjectRequestMetadata = components["schemas"]["ExportObjectRequestMetadata"]; diff --git a/client/src/components/Dataset/DatasetLink/DatasetLink.vue b/client/src/components/Dataset/DatasetLink/DatasetLink.vue index 273d76318294..f4b9c2ed8604 100644 --- a/client/src/components/Dataset/DatasetLink/DatasetLink.vue +++ b/client/src/components/Dataset/DatasetLink/DatasetLink.vue @@ -7,7 +7,7 @@