diff --git a/app/src/app/workspaces/clone-workspace.tsx b/app/src/app/workspaces/clone-workspace.tsx new file mode 100644 index 00000000..1196b030 --- /dev/null +++ b/app/src/app/workspaces/clone-workspace.tsx @@ -0,0 +1,57 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useToast } from "@chakra-ui/react"; +import { useMutation } from "redux-query-react"; +import { useTranslation } from "react-i18next"; +import { cloneWorkspace } from "./workspaces-query-configs"; +import { IconButton } from "@chakra-ui/react"; +import { CopyIcon } from "@chakra-ui/icons"; + +type Props = { + id: string; + samples: string[]; +}; + +export function CloneWorkspace(props: Props) { + const { t } = useTranslation(); + const toast = useToast(); + + const [ + cloneWorkspaceQueryState, + cloneWorkspaceMutation, + ] = useMutation((name: string, id: string) => cloneWorkspace({ name, id, samples: props.samples })); + + const [needsNotify, setNeedsNotify] = useState(true); + + const cloneWorkspaceCallback = useCallback(() => { + const name = prompt("Workspace name"); + if (name) { + setNeedsNotify(true); + cloneWorkspaceMutation(name, props.id); + } + }, [cloneWorkspaceMutation, setNeedsNotify, props.id]); + + useEffect(() => { + if ( + needsNotify && + cloneWorkspaceQueryState.status >= 200 && + cloneWorkspaceQueryState.status < 300 && + !cloneWorkspaceQueryState.isPending + ) { + toast({ + title: t("Workspace cloned"), + status: "info", + duration: 3000, + isClosable: true, + }); + setNeedsNotify(false); + } + }, [t, cloneWorkspaceQueryState, toast, needsNotify, setNeedsNotify]); + + return ( + } + aria-label={`${t("Clone workspace")}`} + onClick={cloneWorkspaceCallback} + /> + ); +} diff --git a/app/src/app/workspaces/workspaces-query-configs.ts b/app/src/app/workspaces/workspaces-query-configs.ts index 8992a131..218f6464 100644 --- a/app/src/app/workspaces/workspaces-query-configs.ts +++ b/app/src/app/workspaces/workspaces-query-configs.ts @@ -9,8 +9,9 @@ import { DeleteWorkspaceRequest, PostWorkspaceRequest, DeleteWorkspaceSampleRequest, + cloneWorkspace as cloneWorkspaceApi, } from "sap-client"; -import { CreateWorkspace, WorkspaceInfo } from "sap-client/models"; +import { CreateWorkspace, WorkspaceInfo, CloneWorkspace } from "sap-client/models"; import { getUrl } from "service"; export type WorkspacesSlice = { @@ -106,6 +107,30 @@ export const createWorkspace = (params: CreateWorkspace) => { return base; }; +export const cloneWorkspace = (params: CloneWorkspace & { samples: string[] }) => { + const base = cloneWorkspaceApi({ cloneWorkspace: params }); + base.url = getUrl(base.url); + + base.transform = (response) => { + if (!response.id) { + return undefined; + } + return { workspaces: [{ id: response.id, name: params.name, samples: params.samples }] }; + }; + + + base.update = { + workspaces: (oldValue, newValue) => { + if (!newValue) { + return oldValue; + } + return [].concat(...oldValue, ...newValue); + }, + }; + base.force = true; + return base; +}; + export const updateWorkspace = (params: PostWorkspaceRequest) => { const base = postWorkspaceApi(params); base.url = getUrl(base.url); diff --git a/app/src/app/workspaces/workspaces.tsx b/app/src/app/workspaces/workspaces.tsx index 4c30a90a..9284b1a8 100644 --- a/app/src/app/workspaces/workspaces.tsx +++ b/app/src/app/workspaces/workspaces.tsx @@ -21,6 +21,7 @@ import type { Workspace } from "sap-client"; import { DeleteWorkspace } from "./delete-workspace"; import { CreateWorkspace } from "./create-workspace"; import { ViewWorkspace } from "./view-workspace"; +import { CloneWorkspace } from "./clone-workspace"; export function Workspaces() { const [workspacesQueryState] = useRequest(fetchWorkspaces()); @@ -63,6 +64,7 @@ export function Workspaces() {
+
diff --git a/app/src/sap-client/apis/WorkspacesApi.ts b/app/src/sap-client/apis/WorkspacesApi.ts index 6274cd45..7eef771d 100644 --- a/app/src/sap-client/apis/WorkspacesApi.ts +++ b/app/src/sap-client/apis/WorkspacesApi.ts @@ -15,6 +15,9 @@ import { HttpMethods, QueryConfig, ResponseBody, ResponseText } from 'redux-query'; import * as runtime from '../runtime'; import { + CloneWorkspace, + CloneWorkspaceFromJSON, + CloneWorkspaceToJSON, CreateWorkspace, CreateWorkspaceFromJSON, CreateWorkspaceToJSON, @@ -29,6 +32,10 @@ import { WorkspaceInfoToJSON, } from '../models'; +export interface CloneWorkspaceRequest { + cloneWorkspace?: CloneWorkspace; +} + export interface CreateWorkspaceRequest { createWorkspace?: CreateWorkspace; } @@ -52,6 +59,48 @@ export interface PostWorkspaceRequest { } +/** + */ +function cloneWorkspaceRaw(requestParameters: CloneWorkspaceRequest, requestConfig: runtime.TypedQueryConfig = {}): QueryConfig { + let queryParameters = null; + + + const headerParameters : runtime.HttpHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + + const { meta = {} } = requestConfig; + + meta.authType = ['bearer']; + const config: QueryConfig = { + url: `${runtime.Configuration.basePath}/workspace/clone`, + meta, + update: requestConfig.update, + queryKey: requestConfig.queryKey, + optimisticUpdate: requestConfig.optimisticUpdate, + force: requestConfig.force, + rollback: requestConfig.rollback, + options: { + method: 'POST', + headers: headerParameters, + }, + body: queryParameters || CloneWorkspaceToJSON(requestParameters.cloneWorkspace), + }; + + const { transform: requestTransform } = requestConfig; + if (requestTransform) { + } + + return config; +} + +/** +*/ +export function cloneWorkspace(requestParameters: CloneWorkspaceRequest, requestConfig?: runtime.TypedQueryConfig): QueryConfig { + return cloneWorkspaceRaw(requestParameters, requestConfig); +} + /** */ function createWorkspaceRaw(requestParameters: CreateWorkspaceRequest, requestConfig: runtime.TypedQueryConfig = {}): QueryConfig { diff --git a/app/src/sap-client/models/CloneWorkspace.ts b/app/src/sap-client/models/CloneWorkspace.ts new file mode 100644 index 00000000..7a23472f --- /dev/null +++ b/app/src/sap-client/models/CloneWorkspace.ts @@ -0,0 +1,52 @@ +// tslint:disable +/** + * SOFI + * SOFI Sekvensanalyseplatform + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface CloneWorkspace + */ +export interface CloneWorkspace { + /** + * + * @type {string} + * @memberof CloneWorkspace + */ + name: string; + /** + * + * @type {string} + * @memberof CloneWorkspace + */ + id: string; +} + +export function CloneWorkspaceFromJSON(json: any): CloneWorkspace { + return { + 'name': json['name'], + 'id': json['id'], + }; +} + +export function CloneWorkspaceToJSON(value?: CloneWorkspace): any { + if (value === undefined) { + return undefined; + } + return { + 'name': value.name, + 'id': value.id, + }; +} + + diff --git a/app/src/sap-client/models/index.ts b/app/src/sap-client/models/index.ts index 72307a3e..5352676b 100644 --- a/app/src/sap-client/models/index.ts +++ b/app/src/sap-client/models/index.ts @@ -9,6 +9,7 @@ export * from './ApprovalRequest'; export * from './ApprovalStatus'; export * from './BaseMetadata'; export * from './BioApiStatus'; +export * from './CloneWorkspace'; export * from './Column'; export * from './CreateWorkspace'; export * from './DataClearance'; diff --git a/openapi_specs/SOFI/SOFI.yaml b/openapi_specs/SOFI/SOFI.yaml index 972a2d4d..9365b153 100644 --- a/openapi_specs/SOFI/SOFI.yaml +++ b/openapi_specs/SOFI/SOFI.yaml @@ -43,6 +43,20 @@ paths: tags: - workspaces x-openapi-router-controller: web.src.SAP.generated.controllers.workspaces_controller + /workspace/clone: + post: + operationId: clone_workspace + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CloneWorkspace" + responses: + '204': + description: "Workspace cloned." + tags: + - workspaces + x-openapi-router-controller: web.src.SAP.generated.controllers.workspaces_controller /workspace/{workspace_id}/{sample_id}: delete: description: Delete sample from workspace @@ -1550,6 +1564,19 @@ components: required: - name + CloneWorkspace: + properties: + name: + type: string + title: Name + id: + type: string + title: Id + type: object + required: + - name + - id + UpdateWorkspace: properties: samples: diff --git a/web/openapi_specs/SOFI/SOFI.yaml b/web/openapi_specs/SOFI/SOFI.yaml index 972a2d4d..9365b153 100644 --- a/web/openapi_specs/SOFI/SOFI.yaml +++ b/web/openapi_specs/SOFI/SOFI.yaml @@ -43,6 +43,20 @@ paths: tags: - workspaces x-openapi-router-controller: web.src.SAP.generated.controllers.workspaces_controller + /workspace/clone: + post: + operationId: clone_workspace + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CloneWorkspace" + responses: + '204': + description: "Workspace cloned." + tags: + - workspaces + x-openapi-router-controller: web.src.SAP.generated.controllers.workspaces_controller /workspace/{workspace_id}/{sample_id}: delete: description: Delete sample from workspace @@ -1550,6 +1564,19 @@ components: required: - name + CloneWorkspace: + properties: + name: + type: string + title: Name + id: + type: string + title: Id + type: object + required: + - name + - id + UpdateWorkspace: properties: samples: diff --git a/web/src/SAP/generated/controllers/workspaces_controller.py b/web/src/SAP/generated/controllers/workspaces_controller.py index e6d1c8cc..407d1b58 100644 --- a/web/src/SAP/generated/controllers/workspaces_controller.py +++ b/web/src/SAP/generated/controllers/workspaces_controller.py @@ -4,6 +4,21 @@ from .. import util from ...src.controllers import WorkspacesController +def clone_workspace(user, token_info, clone_workspace=None): # noqa: E501 + """clone_workspace + + # noqa: E501 + + :param clone_workspace: + :type clone_workspace: dict | bytes + + :rtype: None + """ + if connexion.request.is_json: + from ..models import CloneWorkspace + clone_workspace = CloneWorkspace.from_dict(connexion.request.get_json()) # noqa: E501 + return WorkspacesController.clone_workspace(user, token_info, clone_workspace) + def create_workspace(user, token_info, create_workspace=None): # noqa: E501 """create_workspace diff --git a/web/src/SAP/generated/models/__init__.py b/web/src/SAP/generated/models/__init__.py index ff71dcd7..b1040f27 100644 --- a/web/src/SAP/generated/models/__init__.py +++ b/web/src/SAP/generated/models/__init__.py @@ -15,6 +15,7 @@ from .base_metadata import BaseMetadata from .bio_api_status import BioApiStatus from .bulk_upload_request import BulkUploadRequest +from .clone_workspace import CloneWorkspace from .column import Column from .create_workspace import CreateWorkspace from .data_clearance import DataClearance diff --git a/web/src/SAP/generated/models/clone_workspace.py b/web/src/SAP/generated/models/clone_workspace.py new file mode 100644 index 00000000..e19120ca --- /dev/null +++ b/web/src/SAP/generated/models/clone_workspace.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +from __future__ import absolute_import +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from web.src.SAP.generated.models.base_model_ import Model +from web.src.SAP.generated import util + + +class CloneWorkspace(Model): + + + + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + Do not edit the class manually. + """ + + def __init__(self, name=None, id=None): # noqa: E501 + """CloneWorkspace - a model defined in OpenAPI + + :param name: The name of this CloneWorkspace. # noqa: E501 + :type name: str + :param id: The id of this CloneWorkspace. # noqa: E501 + :type id: str + """ + self.openapi_types = { + 'name': str, + 'id': str, + } + + self.attribute_map = { + 'name': 'name', + 'id': 'id', + } + + self._name = name + self._id = id + + @classmethod + def from_dict(cls, dikt): + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The CloneWorkspace of this CloneWorkspace. # noqa: E501 + :rtype: CloneWorkspace + """ + return util.deserialize_model(dikt, cls) + + @property + def name(self): + """Gets the name of this CloneWorkspace. + + + :return: The name of this CloneWorkspace. + :rtype: str + """ + return self._name + + @name.setter + def name(self, name): + """Sets the name of this CloneWorkspace. + + + :param name: The name of this CloneWorkspace. + :type name: str + """ + if name is None: + raise ValueError("Invalid value for `name`, must not be `None`") # noqa: E501 + + self._name = name + + @property + def id(self): + """Gets the id of this CloneWorkspace. + + + :return: The id of this CloneWorkspace. + :rtype: str + """ + return self._id + + @id.setter + def id(self, id): + """Sets the id of this CloneWorkspace. + + + :param id: The id of this CloneWorkspace. + :type id: str + """ + if id is None: + raise ValueError("Invalid value for `id`, must not be `None`") # noqa: E501 + + self._id = id diff --git a/web/src/SAP/generated/openapi/openapi.yaml b/web/src/SAP/generated/openapi/openapi.yaml index 035b5e01..dd48188b 100644 --- a/web/src/SAP/generated/openapi/openapi.yaml +++ b/web/src/SAP/generated/openapi/openapi.yaml @@ -575,6 +575,20 @@ paths: tags: - user x-openapi-router-controller: web.src.SAP.generated.controllers.user_controller + /workspace/clone: + post: + operationId: clone_workspace + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CloneWorkspace' + responses: + "204": + description: Workspace cloned. + tags: + - workspaces + x-openapi-router-controller: web.src.SAP.generated.controllers.workspaces_controller /workspace/{workspace_id}/{sample_id}: delete: description: Delete sample from workspace @@ -1555,6 +1569,21 @@ components: required: - name type: object + CloneWorkspace: + example: + name: name + id: id + properties: + name: + title: Name + type: string + id: + title: Id + type: string + required: + - id + - name + type: object UpdateWorkspace: example: samples: diff --git a/web/src/SAP/generated/test/test_workspaces_controller.py b/web/src/SAP/generated/test/test_workspaces_controller.py index b9e23d02..29a73fbe 100644 --- a/web/src/SAP/generated/test/test_workspaces_controller.py +++ b/web/src/SAP/generated/test/test_workspaces_controller.py @@ -5,6 +5,7 @@ from flask import json from six import BytesIO +from web.src.SAP.generated.models.clone_workspace import CloneWorkspace # noqa: E501 from web.src.SAP.generated.models.create_workspace import CreateWorkspace # noqa: E501 from web.src.SAP.generated.models.update_workspace import UpdateWorkspace # noqa: E501 from web.src.SAP.generated.models.workspace import Workspace # noqa: E501 @@ -15,6 +16,28 @@ class TestWorkspacesController(BaseTestCase): """WorkspacesController integration test stubs""" + def test_clone_workspace(self): + """Test case for clone_workspace + + + """ + clone_workspace = { + "name" : "name", + "id" : "id" +} + headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer special-key', + } + response = self.client.open( + '/api/workspace/clone', + method='POST', + headers=headers, + data=json.dumps(clone_workspace), + content_type='application/json') + self.assert200(response, + 'Response body is : ' + response.data.decode('utf-8')) + def test_create_workspace(self): """Test case for create_workspace diff --git a/web/src/SAP/src/controllers/WorkspacesController.py b/web/src/SAP/src/controllers/WorkspacesController.py index d3672eb1..b479eeac 100644 --- a/web/src/SAP/src/controllers/WorkspacesController.py +++ b/web/src/SAP/src/controllers/WorkspacesController.py @@ -4,6 +4,7 @@ from ..repositories.workspaces import delete_workspace as delete_workspace_db from ..repositories.workspaces import delete_workspace_sample as delete_workspace_sample_db from ..repositories.workspaces import create_workspace as create_workspace_db +from ..repositories.workspaces import clone_workspace as clone_workspace_db from ..repositories.workspaces import update_workspace as update_workspace_db from ..repositories.workspaces import get_workspace as get_workspace_db @@ -22,6 +23,14 @@ def create_workspace(user, token_info, body): return jsonify(body) +def clone_workspace(user, token_info, body): + res = clone_workspace_db(user, body) + + if not res.inserted_id: + return None + + return jsonify({"id": str(res.inserted_id)}) + def post_workspace(user, token_info, workspace_id: str, body): update_workspace_db(user, workspace_id, body) diff --git a/web/src/SAP/src/repositories/workspaces.py b/web/src/SAP/src/repositories/workspaces.py index 37116407..b97563b8 100644 --- a/web/src/SAP/src/repositories/workspaces.py +++ b/web/src/SAP/src/repositories/workspaces.py @@ -4,6 +4,7 @@ from ...src.repositories.analysis import get_analysis_with_metadata, get_single_analysis_by_object_id from ...common.database import get_collection, WORKSPACES_COL_NAME from ...generated.models import CreateWorkspace +from ...generated.models import CloneWorkspace def trim(item): item["id"] = str(item["_id"]) @@ -61,6 +62,22 @@ def create_workspace(user: str, workspace: CreateWorkspace): record = {**workspace.to_dict(), "created_by": user} return workspaces.update_one({'created_by': user, 'name': workspace.name}, {"$set": record}, upsert=True) +def clone_workspace(user: str, cloneWorkspaceInfo: CloneWorkspace): + workspaces = get_collection(WORKSPACES_COL_NAME) + + if ObjectId.is_valid(cloneWorkspaceInfo.id): + workspace = trim(workspaces.find_one({"created_by": user, "_id": ObjectId(cloneWorkspaceInfo.id)})) + else: + workspace = trim(workspaces.find_one({"created_by": user, "name": cloneWorkspaceInfo.id})) + + if workspace is None: + return None + + if workspace["samples"] is None: + workspace["samples"] = [] + + return workspaces.insert_one({'created_by': user, 'name': cloneWorkspaceInfo.name, 'samples': workspace["samples"]}) + def update_workspace(user: str, workspace_id: str, workspace: UpdateWorkspace): workspaces = get_collection(WORKSPACES_COL_NAME)