mirror of
https://github.com/logos-storage/logos-storage-installer.git
synced 2026-01-02 13:33:11 +00:00
wip: install menu
This commit is contained in:
parent
8365e556a0
commit
c8d96425d8
@ -14,3 +14,7 @@ export const mockMenuLoop = {
|
||||
showLoop: vi.fn(),
|
||||
stopLoop: vi.fn(),
|
||||
};
|
||||
|
||||
export const mockDataDirMover = {
|
||||
moveDataDir: vi.fn(),
|
||||
};
|
||||
|
||||
@ -99,7 +99,7 @@ export async function main() {
|
||||
const fsService = new FsService();
|
||||
const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService);
|
||||
const numberSelector = new NumberSelector(uiService);
|
||||
const installMenu = new InstallMenu(uiService, configService);
|
||||
const installMenu = new InstallMenu(uiService, configService, pathSelector);
|
||||
const configMenu = new ConfigMenu(
|
||||
uiService,
|
||||
new MenuLoop(),
|
||||
@ -112,6 +112,7 @@ export async function main() {
|
||||
new MenuLoop(),
|
||||
installMenu,
|
||||
configMenu,
|
||||
new DataDirMover(fsService, uiService)
|
||||
);
|
||||
|
||||
await mainMenu.show();
|
||||
|
||||
@ -38,4 +38,8 @@ export class FsService {
|
||||
makeDir = (dir) => {
|
||||
fs.mkdirSync(dir);
|
||||
};
|
||||
|
||||
moveDir = (oldPath, newPath) => {
|
||||
fs.moveSync(oldPath, newPath);
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,18 +5,21 @@ export class ConfigMenu {
|
||||
configService,
|
||||
pathSelector,
|
||||
numberSelector,
|
||||
dataDirMover,
|
||||
) {
|
||||
this.ui = uiService;
|
||||
this.loop = menuLoop;
|
||||
this.configService = configService;
|
||||
this.pathSelector = pathSelector;
|
||||
this.numberSelector = numberSelector;
|
||||
this.dataDirMover = dataDirMover;
|
||||
|
||||
this.loop.initialize(this.showConfigMenu);
|
||||
}
|
||||
|
||||
show = async () => {
|
||||
this.config = this.configService.get();
|
||||
this.originalDataDir = this.config.dataDir;
|
||||
this.ui.showInfoMessage("Codex Configuration");
|
||||
await this.loop.showLoop();
|
||||
};
|
||||
@ -76,46 +79,10 @@ export class ConfigMenu {
|
||||
};
|
||||
|
||||
editDataDir = async () => {
|
||||
// todo
|
||||
// function updateDataDir(config, newDataDir) {
|
||||
// if (config.dataDir == newDataDir) return config;
|
||||
// // The Codex dataDir is a little strange:
|
||||
// // If the old one is empty: The new one should not exist, so that codex creates it
|
||||
// // with the correct security permissions.
|
||||
// // If the old one does exist: We move it.
|
||||
// if (isDir(config.dataDir)) {
|
||||
// console.log(
|
||||
// showInfoMessage(
|
||||
// "Moving Codex data folder...\n" +
|
||||
// `From: "${config.dataDir}"\n` +
|
||||
// `To: "${newDataDir}"`,
|
||||
// ),
|
||||
// );
|
||||
// try {
|
||||
// fs.moveSync(config.dataDir, newDataDir);
|
||||
// } catch (error) {
|
||||
// console.log(
|
||||
// showErrorMessage("Error while moving dataDir: " + error.message),
|
||||
// );
|
||||
// throw error;
|
||||
// }
|
||||
// } else {
|
||||
// // Old data dir does not exist.
|
||||
// if (isDir(newDataDir)) {
|
||||
// console.log(
|
||||
// showInfoMessage(
|
||||
// "Warning: the selected data path already exists.\n" +
|
||||
// `New data path = "${newDataDir}"\n` +
|
||||
// "Codex may overwrite data in this folder.\n" +
|
||||
// "Codex will fail to start if this folder does not have the required\n" +
|
||||
// "security permissions.",
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// config.dataDir = newDataDir;
|
||||
// return config;
|
||||
// }
|
||||
this.config.dataDir = await this.pathSelector.show(
|
||||
this.config.dataDir,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
editLogsDir = async () => {
|
||||
@ -181,6 +148,15 @@ export class ConfigMenu {
|
||||
};
|
||||
|
||||
saveChangesAndExit = async () => {
|
||||
if (this.config.dataDir !== this.originalDataDir) {
|
||||
// The Codex data-dir is a little special.
|
||||
// Use a dedicated module to move it.
|
||||
await this.dataDirMover.moveDataDir(
|
||||
this.originalDataDir,
|
||||
this.config.dataDir,
|
||||
);
|
||||
}
|
||||
|
||||
this.configService.saveConfig();
|
||||
this.ui.showInfoMessage("Configuration changes saved.");
|
||||
this.loop.stopLoop();
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
mockPathSelector,
|
||||
mockNumberSelector,
|
||||
mockMenuLoop,
|
||||
mockDataDirMover,
|
||||
} from "../__mocks__/utils.mocks.js";
|
||||
|
||||
describe("ConfigMenu", () => {
|
||||
@ -31,6 +32,7 @@ describe("ConfigMenu", () => {
|
||||
mockConfigService,
|
||||
mockPathSelector,
|
||||
mockNumberSelector,
|
||||
mockDataDirMover,
|
||||
);
|
||||
});
|
||||
|
||||
@ -57,6 +59,11 @@ describe("ConfigMenu", () => {
|
||||
expect(configMenu.config).toEqual(config);
|
||||
});
|
||||
|
||||
it("sets the original datadir field", async () => {
|
||||
await configMenu.show();
|
||||
expect(configMenu.originalDataDir).toEqual(config.dataDir);
|
||||
});
|
||||
|
||||
describe("config menu options", () => {
|
||||
beforeEach(() => {
|
||||
configMenu.config = config;
|
||||
@ -103,6 +110,15 @@ describe("ConfigMenu", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("edits the logs directory", async () => {
|
||||
const originalPath = config.dataDir;
|
||||
mockPathSelector.show.mockResolvedValue("/new-data");
|
||||
await configMenu.editDataDir();
|
||||
|
||||
expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, false);
|
||||
expect(configMenu.config.dataDir).toEqual("/new-data");
|
||||
});
|
||||
|
||||
it("edits the logs directory", async () => {
|
||||
const originalPath = config.logsDir;
|
||||
mockPathSelector.show.mockResolvedValue("/new-logs");
|
||||
@ -219,8 +235,11 @@ describe("ConfigMenu", () => {
|
||||
);
|
||||
expect(configMenu.config.ports.apiPort).toEqual(originalPort);
|
||||
});
|
||||
});
|
||||
|
||||
describe("save and discard changes", () => {
|
||||
it("saves changes and exits", async () => {
|
||||
await configMenu.show();
|
||||
await configMenu.saveChangesAndExit();
|
||||
|
||||
expect(mockConfigService.saveConfig).toHaveBeenCalled();
|
||||
@ -230,6 +249,19 @@ describe("ConfigMenu", () => {
|
||||
expect(mockMenuLoop.stopLoop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls the dataDirMover when the new datadir is not equal to the original dataDir when saving changes", async () => {
|
||||
config.dataDir = "/original-data";
|
||||
await configMenu.show();
|
||||
|
||||
configMenu.config.dataDir = "/new-data";
|
||||
await configMenu.saveChangesAndExit();
|
||||
|
||||
expect(mockDataDirMover.moveDataDir).toHaveBeenCalledWith(
|
||||
configMenu.originalDataDir,
|
||||
configMenu.config.dataDir,
|
||||
);
|
||||
});
|
||||
|
||||
it("discards changes and exits", async () => {
|
||||
await configMenu.discardChangesAndExit();
|
||||
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
export class InstallMenu {
|
||||
constructor(uiService, configService) {
|
||||
constructor(uiService, configService, pathSelector) {
|
||||
this.ui = uiService;
|
||||
this.configService = configService;
|
||||
this.config = configService.get();
|
||||
this.pathSelector = pathSelector;
|
||||
}
|
||||
|
||||
show = async () => {
|
||||
await this.ui.askMultipleChoice("Configure your Codex installation", [
|
||||
{
|
||||
label: "Install path: " + this.config.codexPath,
|
||||
action: async function () {
|
||||
console.log("run path selector");
|
||||
},
|
||||
action: this.selectInstallPath,
|
||||
},
|
||||
{
|
||||
label: "Storage provider module: Disabled (todo)",
|
||||
@ -22,17 +22,25 @@ export class InstallMenu {
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
action: async function () {},
|
||||
action: this.doNothing,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
selectInstallPath = async () => {
|
||||
this.config.codexPath = await this.pathSelector.show(
|
||||
this.config.codexPath,
|
||||
false,
|
||||
);
|
||||
this.configService.saveConfig();
|
||||
};
|
||||
|
||||
storageProviderOption = async () => {
|
||||
this.ui.showInfoMessage("This option is not currently available.");
|
||||
await this.show();
|
||||
};
|
||||
|
||||
performInstall = async () => {
|
||||
console.log("todo");
|
||||
};
|
||||
performInstall = async () => {};
|
||||
|
||||
doNothing = async () => {};
|
||||
}
|
||||
|
||||
71
src/ui/installMenu.test.js
Normal file
71
src/ui/installMenu.test.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { describe, beforeEach, it, expect, vi } from "vitest";
|
||||
import { InstallMenu } from "./installMenu.js";
|
||||
import { mockUiService } from "../__mocks__/service.mocks.js";
|
||||
import { mockConfigService } from "../__mocks__/service.mocks.js";
|
||||
import { mockPathSelector } from "../__mocks__/utils.mocks.js";
|
||||
|
||||
describe("InstallMenu", () => {
|
||||
const config = {
|
||||
codexPath: "/codex",
|
||||
};
|
||||
let installMenu;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockConfigService.get.mockReturnValue(config);
|
||||
|
||||
installMenu = new InstallMenu(
|
||||
mockUiService,
|
||||
mockConfigService,
|
||||
mockPathSelector,
|
||||
);
|
||||
});
|
||||
|
||||
it("displays the install menu", async () => {
|
||||
await installMenu.show();
|
||||
expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith(
|
||||
"Configure your Codex installation",
|
||||
[
|
||||
{
|
||||
label: "Install path: " + config.codexPath,
|
||||
action: installMenu.selectInstallPath,
|
||||
},
|
||||
{
|
||||
label: "Storage provider module: Disabled (todo)",
|
||||
action: installMenu.storageProviderOption,
|
||||
},
|
||||
{
|
||||
label: "Install!",
|
||||
action: installMenu.performInstall,
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
action: installMenu.doNothing,
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it("allows selecting the install path", async () => {
|
||||
const originalPath = config.codexPath;
|
||||
const newPath = "/new/path";
|
||||
mockPathSelector.show.mockResolvedValue(newPath);
|
||||
|
||||
await installMenu.selectInstallPath();
|
||||
|
||||
expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, false);
|
||||
expect(config.codexPath).toBe(newPath);
|
||||
expect(mockConfigService.saveConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows storage provider option is unavailable", async () => {
|
||||
const showMock = vi.fn();
|
||||
installMenu.show = showMock;
|
||||
|
||||
await installMenu.storageProviderOption();
|
||||
|
||||
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
|
||||
"This option is not currently available.",
|
||||
);
|
||||
});
|
||||
});
|
||||
53
src/utils/dataDirMover.js
Normal file
53
src/utils/dataDirMover.js
Normal file
@ -0,0 +1,53 @@
|
||||
export class DataDirMover {
|
||||
constructor(fsService, uiService) {
|
||||
this.fs = fsService;
|
||||
this.ui = uiService;
|
||||
}
|
||||
|
||||
moveDataDir = (oldPath, newPath) => {
|
||||
if (oldPath === newPath) return;
|
||||
|
||||
// The Codex dataDir is a little strange:
|
||||
// If the old one is empty: The new one should not exist, so that codex creates it with the correct security permissions.
|
||||
// If the old one does exist: We move it.
|
||||
|
||||
if (this.fs.isDir(oldPath)) {
|
||||
this.moveDir(oldPath, newPath);
|
||||
} else {
|
||||
this.ensureDoesNotExist(newPath);
|
||||
}
|
||||
};
|
||||
|
||||
moveDir = (oldPath, newPath) => {
|
||||
this.ui.showInfoMessage(
|
||||
"Moving Codex data folder...\n" +
|
||||
`From: "${oldPath}"\n` +
|
||||
`To: "${newPath}"`,
|
||||
);
|
||||
|
||||
try {
|
||||
this.fs.moveDir(oldPath, newPath);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
this.ui.showErrorMessage(
|
||||
"Error while moving dataDir: " + error.message,
|
||||
),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
ensureDoesNotExist = (path) => {
|
||||
if (this.fs.isDir(path)) {
|
||||
console.log(
|
||||
this.ui.showInfoMessage(
|
||||
"Warning: the selected data path already exists.\n" +
|
||||
`New data path = "${path}"\n` +
|
||||
"Codex may overwrite data in this folder.\n" +
|
||||
"Codex will fail to start if this folder does not have the required\n" +
|
||||
"security permissions.",
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -55,13 +55,13 @@ export class PathSelector {
|
||||
splitPath = (str) => {
|
||||
var result = this.dropEmptyParts(str.replaceAll("\\", "/").split("/"));
|
||||
if (str.startsWith("/") && this.roots.includes("/")) {
|
||||
result = ["/", ...result];
|
||||
result = ["/", ...result];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
dropEmptyParts = (parts) => {
|
||||
return parts.filter(part => part.length > 0);
|
||||
return parts.filter((part) => part.length > 0);
|
||||
};
|
||||
|
||||
combine = (parts) => {
|
||||
@ -141,7 +141,7 @@ export class PathSelector {
|
||||
getSubDirOptions = () => {
|
||||
const fullPath = this.combine(this.currentPath);
|
||||
const entries = this.fs.readDir(fullPath);
|
||||
return entries.filter(entry => this.isSubDir(entry));
|
||||
return entries.filter((entry) => this.isSubDir(entry));
|
||||
};
|
||||
|
||||
downOne = async () => {
|
||||
|
||||
@ -20,7 +20,9 @@ describe("PathSelector", () => {
|
||||
|
||||
describe("initialization", () => {
|
||||
it("initializes the menu loop", () => {
|
||||
expect(mockMenuLoop.initialize).toHaveBeenCalledWith(pathSelector.showPathSelector);
|
||||
expect(mockMenuLoop.initialize).toHaveBeenCalledWith(
|
||||
pathSelector.showPathSelector,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -92,9 +94,9 @@ describe("PathSelector", () => {
|
||||
it("shows down directory navigation", async () => {
|
||||
mockFsService.readDir.mockReturnValue(["subdir1", "subdir2"]);
|
||||
mockFsService.isDir.mockReturnValue(true);
|
||||
|
||||
|
||||
await pathSelector.downOne();
|
||||
|
||||
|
||||
expect(mockUiService.askMultipleChoice).toHaveBeenCalled();
|
||||
expect(mockFsService.readDir).toHaveBeenCalledWith(mockStartPath);
|
||||
});
|
||||
@ -106,7 +108,7 @@ describe("PathSelector", () => {
|
||||
options[0].action(); // Select the first option
|
||||
});
|
||||
await pathSelector.downOne();
|
||||
|
||||
|
||||
expect(pathSelector.currentPath).toEqual(["/", "home", "user", subdir]);
|
||||
});
|
||||
|
||||
@ -114,9 +116,11 @@ describe("PathSelector", () => {
|
||||
const newDir = "newdir";
|
||||
mockUiService.askPrompt.mockResolvedValue(newDir);
|
||||
await pathSelector.createSubDir();
|
||||
|
||||
|
||||
expect(mockUiService.askPrompt).toHaveBeenCalledWith("Enter name:");
|
||||
expect(mockFsService.makeDir).toHaveBeenCalled(mockStartPath + "/" + newDir);
|
||||
expect(mockFsService.makeDir).toHaveBeenCalled(
|
||||
mockStartPath + "/" + newDir,
|
||||
);
|
||||
expect(pathSelector.currentPath).toEqual(["/", "home", "user", newDir]);
|
||||
});
|
||||
});
|
||||
@ -135,7 +139,9 @@ describe("PathSelector", () => {
|
||||
it("validates full paths", () => {
|
||||
mockFsService.isDir.mockReturnValue(false);
|
||||
pathSelector.updateCurrentIfValidFull("/invalid/path");
|
||||
expect(mockUiService.showErrorMessage).toHaveBeenCalledWith("The path does not exist.");
|
||||
expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(
|
||||
"The path does not exist.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user