From 5bc46a3daa9ad7e2a56157433b990027b72ad000 Mon Sep 17 00:00:00 2001 From: Michael Bauer Date: Wed, 4 Sep 2019 12:02:46 +0200 Subject: [PATCH 1/2] Add clone method to workspace --- src/main/types/workspaces/Workspace.js | 35 +++++++ .../workspaces/clone-workspace.subtest.js | 98 +++++++++++++++++++ test/mocha/workspaces/workspaces.test.js | 1 + 3 files changed, 134 insertions(+) create mode 100644 test/mocha/workspaces/clone-workspace.subtest.js diff --git a/src/main/types/workspaces/Workspace.js b/src/main/types/workspaces/Workspace.js index 2765ea7a7..35db7fd2c 100644 --- a/src/main/types/workspaces/Workspace.js +++ b/src/main/types/workspaces/Workspace.js @@ -1,5 +1,6 @@ import path from "path"; import fse from "fs-extra"; +import UUID from "uuid"; import WorkspaceSettings from "../settings/WorkspaceSettings"; import ContractCache from "../contracts/ContractCache"; @@ -117,6 +118,40 @@ class Workspace { // a solution here } } + + clone(cloneName) { + const sanitizedName = Workspace.getSanitizedName(cloneName); + const configDirectory = path.join(this.workspaceDirectory, "..", ".."); + const cloneDirectory = path.join( + configDirectory, + "workspaces", + sanitizedName, + ); + + try { + // copy workspace directory, make sure not to overwrite any existing data + fse.copySync(this.workspaceDirectory, cloneDirectory, { + overwrite: false, + errorOnExist: true, + }); + } catch (e) { + // Failed to copy directory. Most likely target already exists + // TODO: Handle this error more gracefully/provide user feedback + return; + } + + // update settings of cloned workspace to match new location + const db_path = path.join(cloneDirectory, "chaindata"); + let settings = new WorkspaceSettings(cloneDirectory, db_path); + settings.bootstrap(); + // set new db_path + settings.set("server.db_path", db_path); + // generate new uuid + settings.set("uuid", UUID.v4()); + // set new name + settings.set("name", cloneName); + return new Workspace(cloneName, configDirectory); + } } export default Workspace; diff --git a/test/mocha/workspaces/clone-workspace.subtest.js b/test/mocha/workspaces/clone-workspace.subtest.js new file mode 100644 index 000000000..c6ed79cef --- /dev/null +++ b/test/mocha/workspaces/clone-workspace.subtest.js @@ -0,0 +1,98 @@ +import Workspace from "../../../src/main/types/workspaces/Workspace"; +import temp from "temp"; +import assert from "assert"; +import fs from "fs"; +import path from "path"; +import ganacheLib from "ganache-core"; +import Web3 from "web3"; + +describe("Clone Workspace", () => { + let configDirectory = "/"; + let workspace; + let clonedWorkspace; + const cloneName = "cloned workspace"; + + before("create folder where workspace will live", async () => { + temp.track(); + configDirectory = temp.mkdirSync("ganache-temp-workspaces"); + fs.mkdirSync(path.join(configDirectory, "workspaces")); + }); + + before("created and bootstrapped new workspace", async () => { + workspace = new Workspace("Temp Workspace", configDirectory); + workspace.bootstrap(); + }); + + it("did not clone workspace into existing directory", async () => { + clonedWorkspace = workspace.clone(workspace.name); + assert.strictEqual( + clonedWorkspace, + undefined, + "Workspace was cloned into existing directory", + ); + }); + + it("cloned workspace without error", async () => { + clonedWorkspace = workspace.clone(cloneName); + assert( + fs.existsSync(clonedWorkspace.workspaceDirectory), + "Cloned workspace directory wasn't created", + ); + const settingsFile = path.join( + clonedWorkspace.workspaceDirectory, + "Settings", + ); + assert( + fs.existsSync(settingsFile), + "Cloned Workspace Settings file wasn't created", + ); + }); + + it("applied correct settings to cloned workspace", async () => { + assert.notEqual( + clonedWorkspace.settings.get("uuid"), + workspace.settings.get("uuid"), + "The uuid of the cloned workspace should be different from the original", + ); + assert.equal( + clonedWorkspace.name, + cloneName, + "Cloned workspace should have the correct name set", + ); + assert.equal( + clonedWorkspace.settings.get("name"), + cloneName, + "Cloned workspace settings should have correct name set", + ); + assert.notEqual( + workspace.settings.get("server.db_path"), + clonedWorkspace.settings.get("server.db_path"), + "Cloned workspace db_path is the same like original workspace", + ); + assert.equal( + clonedWorkspace.settings.get("server.db_path"), + path.join(clonedWorkspace.workspaceDirectory, "chaindata"), + "Cloned workspace db_path is not correct", + ); + assert.equal( + clonedWorkspace.settings.get("server.mnemonic"), + workspace.settings.get("server.mnemonic"), + "Mnemeonic of cloned workspace is different from original", + ); + }); + + it("started and stopped cloned workspace with no errors", done => { + var web3 = new Web3(); + web3.setProvider(ganacheLib.provider(clonedWorkspace.settings.getAll())); + + web3.eth.getAccounts(function(err, result) { + if (err) return done(err); + assert( + result.length, + 10, + "The number of accounts created should be 10 (the default)", + ); + done(); + }); + }); +}); diff --git a/test/mocha/workspaces/workspaces.test.js b/test/mocha/workspaces/workspaces.test.js index d55dd92a0..e21978ef9 100644 --- a/test/mocha/workspaces/workspaces.test.js +++ b/test/mocha/workspaces/workspaces.test.js @@ -1,4 +1,5 @@ describe("Workspaces", function() { require("./new-workspace.subtest"); + require("./clone-workspace.subtest"); require("./workspace-manager.subtest"); }); From bf615e1a2bc69c85f5cd47a9b9efae4acd679693 Mon Sep 17 00:00:00 2001 From: Michael Bauer Date: Mon, 21 Oct 2019 09:14:10 +0200 Subject: [PATCH 2/2] Add UI to clone workspace --- src/common/redux/workspaces/actions.js | 9 +++++++ src/main/index.js | 15 ++++++++++++ src/renderer/icons/clone-regular.svg | 1 + src/renderer/screens/startup/HomeScreen.js | 25 +++++++++++++++++++- src/renderer/screens/startup/HomeScreen.scss | 15 +++++++++--- 5 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/renderer/icons/clone-regular.svg diff --git a/src/common/redux/workspaces/actions.js b/src/common/redux/workspaces/actions.js index eb7da0b31..0f872949d 100644 --- a/src/common/redux/workspaces/actions.js +++ b/src/common/redux/workspaces/actions.js @@ -82,6 +82,15 @@ export const deleteWorkspace = function(name) { }; }; +export const CLONE_WORKSPACE = `${prefix}/CLONE_WORKSPACE`; +export const cloneWorkspace = function(name, cloneName) { + return function(dispatch) { + dispatch({ type: CLONE_WORKSPACE, name, cloneName }); + + ipcRenderer.send(CLONE_WORKSPACE, name, cloneName); + }; +}; + export const SET_CURRENT_WORKSPACE = `${prefix}/SET_CURRENT_WORKSPACE`; export const setCurrentWorkspace = function(workspace, contractCache) { return { type: SET_CURRENT_WORKSPACE, workspace, contractCache }; diff --git a/src/main/index.js b/src/main/index.js index b0e570f18..b47610aac 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -40,6 +40,7 @@ import { GET_CONTRACT_DETAILS, OPEN_NEW_WORKSPACE_CONFIG, PROJECT_UPDATED, + CLONE_WORKSPACE, } from "../common/redux/workspaces/actions"; import { @@ -481,6 +482,20 @@ app.on('ready', () => { ); }); + ipcMain.on(CLONE_WORKSPACE, async (event, name, cloneName) => { + const sourceWorkspace = workspaceManager.get(name); + if (sourceWorkspace) { + sourceWorkspace.clone(cloneName); + + workspaceManager.bootstrap(); + + mainWindow.webContents.send( + SET_WORKSPACES, + workspaceManager.getNonDefaultNames(), + ); + } + }); + ipcMain.on(DELETE_WORKSPACE, async (event, name) => { const tempWorkspace = workspaceManager.get(name); if (tempWorkspace) { diff --git a/src/renderer/icons/clone-regular.svg b/src/renderer/icons/clone-regular.svg new file mode 100644 index 000000000..c14f573e8 --- /dev/null +++ b/src/renderer/icons/clone-regular.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/screens/startup/HomeScreen.js b/src/renderer/screens/startup/HomeScreen.js index 2f96aea1d..dfc53a7e5 100644 --- a/src/renderer/screens/startup/HomeScreen.js +++ b/src/renderer/screens/startup/HomeScreen.js @@ -9,6 +9,7 @@ import { openDefaultWorkspace, openNewWorkspaceConfig, deleteWorkspace, + cloneWorkspace, } from "../../../common/redux/workspaces/actions"; import UpdateNotification from "../auto-update/UpdateNotification"; import ErrorModal from "../../components/modal/ErrorModal"; @@ -22,6 +23,7 @@ import Logo from "../../icons/logo.svg"; import ChainIcon from "../../icons/chain.svg"; import MenuIcon from "../../icons/list.svg"; import TrashIcon from "../../icons/trash-icon.svg"; +import CloneIcon from "../../icons/clone-regular.svg"; class HomeScreen extends Component { constructor(props) { @@ -33,8 +35,21 @@ class HomeScreen extends Component { this.props.dispatch(openWorkspace(workspaceName)); } + handleCloneWorkspace(e) { + const workspaceName = e.currentTarget.parentElement.querySelector("span").innerText; + e.stopPropagation(); + e.preventDefault(); + + document.activeElement.blur(); + + // Future improvement: Create modal dialog asking for new name + // For now, just go with hardcoded extension + const cloneName = workspaceName + "-clone"; + this.props.dispatch(cloneWorkspace(workspaceName, cloneName)); + } + handleDeleteWorkspace(e) { - const workspaceName = e.currentTarget.previousSibling.innerText; + const workspaceName = e.currentTarget.parentElement.querySelector("span").innerText; e.stopPropagation(); e.preventDefault(); @@ -79,8 +94,16 @@ class HomeScreen extends Component {