diff --git a/.gitignore b/.gitignore
index 45a81dc0..8a0e4af3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,5 +23,7 @@ client/public/javascripts/L.Path.Drag.js
/installer/archive/Scripts
/installer/archive/Mods
/installer/installer/DCSOlympus*.exe
-client/public/javascripts/L.Path.Drag.js
-client/public/stylesheets/leaflet/leaflet-gesture-handling.css
+
+L.Path.Drag.js
+leaflet-gesture-handling.css
+package-lock.json
diff --git a/client/configurator.js b/client/configurator.js
index 0a954e8c..540ac0dd 100644
--- a/client/configurator.js
+++ b/client/configurator.js
@@ -3,7 +3,11 @@ const path = require('path')
const yargs = require('yargs');
const prompt = require('prompt-sync')({sigint: true});
const sha256 = require('sha256');
-const jsonPath = path.join('..', 'olympus.json');
+var jsonPath = path.join('..', 'olympus.json');
+var regedit = require('regedit')
+
+const shellFoldersKey = 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders'
+const saveGamesKey = '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}'
/* Set the acceptable values */
yargs.alias('a', 'address').describe('a', 'Backend address').string('a');
@@ -12,6 +16,7 @@ yargs.alias('c', 'clientPort').describe('c', 'Client port').number('c');
yargs.alias('p', 'gameMasterPassword').describe('p', 'Game Master password').string('p');
yargs.alias('bp', 'blueCommanderPassword').describe('bp', 'Blue Commander password').string('bp');
yargs.alias('rp', 'redCommanderPassword').describe('rp', 'Red Commander password').string('rp');
+yargs.alias('d', 'directory').describe('d', 'Directory where the DCS Olympus configurator is located').string('rp');
args = yargs.argv;
async function run() {
@@ -30,21 +35,6 @@ async function run() {
if (args.address === undefined && args.clientPort === undefined && args.backendPort === undefined &&
args.gameMasterPassword === undefined && args.blueCommanderPassword === undefined && args.redCommanderPassword === undefined) {
- console.log('\x1b[36m%s\x1b[0m', "*********************************************************************");
- console.log('\x1b[36m%s\x1b[0m', "* _____ _____ _____ ____ _ *");
- console.log('\x1b[36m%s\x1b[0m', "* | __ \\ / ____|/ ____| / __ \\| | *");
- console.log('\x1b[36m%s\x1b[0m', "* | | | | | | (___ | | | | |_ _ _ __ ___ _ __ _ _ ___ *");
- console.log('\x1b[36m%s\x1b[0m', "* | | | | | \\___ \\ | | | | | | | | '_ ` _ \\| '_ \\| | | / __| *");
- console.log('\x1b[36m%s\x1b[0m', "* | |__| | |____ ____) | | |__| | | |_| | | | | | | |_) | |_| \\__ \\ *");
- console.log('\x1b[36m%s\x1b[0m', "* |_____/ \\_____|_____/ \\____/|_|\\__, |_| |_| |_| .__/ \\__,_|___/ *");
- console.log('\x1b[36m%s\x1b[0m', "* __/ | | | *");
- console.log('\x1b[36m%s\x1b[0m', "* |___/ |_| *");
- console.log('\x1b[36m%s\x1b[0m', "*********************************************************************");
- console.log('\x1b[36m%s\x1b[0m', "");
-
- console.log("DCS Olympus configurator {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}");
- console.log("");
-
var newValue;
var result;
@@ -113,5 +103,65 @@ async function run() {
await new Promise(resolve => setTimeout(resolve, 3000));
}
+console.log('\x1b[36m%s\x1b[0m', "*********************************************************************");
+console.log('\x1b[36m%s\x1b[0m', "* _____ _____ _____ ____ _ *");
+console.log('\x1b[36m%s\x1b[0m', "* | __ \\ / ____|/ ____| / __ \\| | *");
+console.log('\x1b[36m%s\x1b[0m', "* | | | | | | (___ | | | | |_ _ _ __ ___ _ __ _ _ ___ *");
+console.log('\x1b[36m%s\x1b[0m', "* | | | | | \\___ \\ | | | | | | | | '_ ` _ \\| '_ \\| | | / __| *");
+console.log('\x1b[36m%s\x1b[0m', "* | |__| | |____ ____) | | |__| | | |_| | | | | | | |_) | |_| \\__ \\ *");
+console.log('\x1b[36m%s\x1b[0m', "* |_____/ \\_____|_____/ \\____/|_|\\__, |_| |_| |_| .__/ \\__,_|___/ *");
+console.log('\x1b[36m%s\x1b[0m', "* __/ | | | *");
+console.log('\x1b[36m%s\x1b[0m', "* |___/ |_| *");
+console.log('\x1b[36m%s\x1b[0m', "*********************************************************************");
+console.log('\x1b[36m%s\x1b[0m', "");
+
+console.log("DCS Olympus configurator {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}");
+console.log("");
+
/* Run the configurator */
-run();
\ No newline at end of file
+if (args.directory) {
+ jsonPath = path.join(args.directory, "olympus.json");
+}
+else {
+ /* Automatically detect possible DCS installation folders */
+ regedit.list(shellFoldersKey, function(err, result) {
+ if (err) {
+ console.log(err);
+ }
+ else {
+ if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined)
+ {
+ const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value'];
+ const folders = fs.readdirSync(searchpath);
+ var options = [];
+ folders.forEach((folder) => {
+ if (fs.existsSync(path.join(searchpath, folder, "Logs", "dcs.log"))) {
+ options.push(folder);
+ }
+ })
+ console.log("The following DCS Saved Games folders have been automatically detected.")
+ options.forEach((folder, index) => {
+ console.log(`(${index + 1}) ${folder}`)
+ });
+ while (true) {
+ var newValue = prompt(`Please choose a folder onto which the configurator shall operate by typing the associated number: `)
+ result = Number(newValue);
+
+ if (!isNaN(result) && Number.isInteger(result) && result > 0 && result <= options.length) {
+ jsonPath = path.join(searchpath, options[result - 1], "Config", "olympus.json");
+ break;
+ }
+ else {
+ console.log(`Please type a number between 1 and ${options.length}`);
+ }
+ }
+
+ } else {
+ console.error("An error occured while trying to fetch the location of the DCS folder. Please type the folder location manually.")
+ jsonPath = path.join(prompt(`DCS Saved Games folder location: `), "olympus.json");
+ }
+ console.log(`Configurator will run on ${jsonPath}, if this is incorrect please restart the configurator`)
+ run();
+ }
+ })
+}
diff --git a/client/package.json b/client/package.json
index 69866012..361af86f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -14,15 +14,18 @@
"watch": "watchify .\\src\\index.ts --debug -o .\\public\\javascripts\\bundle.js -t [ babelify --global true --presets [ @babel/preset-env ] --extensions '.js'] -p [ tsify --noImplicitAny ]"
},
"dependencies": {
+ "appjs": "^0.0.20",
"body-parser": "^1.20.2",
"debug": "~2.6.9",
"ejs": "^3.1.8",
+ "electron": "^28.0.0",
"express": "~4.16.1",
"express-basic-auth": "^1.2.1",
"http-proxy-middleware": "^2.0.6",
"js-sha256": "^0.10.1",
"morgan": "~1.9.1",
"prompt-sync": "^4.2.0",
+ "regedit": "^5.1.2",
"save": "^2.9.0",
"sha256": "^0.2.0",
"srtm-elevation": "^2.1.2",
@@ -31,10 +34,9 @@
"yargs": "^17.7.2"
},
"devDependencies": {
- "leaflet-gesture-handling": "^1.2.2",
- "@turf/turf": "^6.5.0",
"@babel/preset-env": "^7.21.4",
"@tanem/svg-injector": "^10.1.68",
+ "@turf/turf": "^6.5.0",
"@types/formatcoords": "^1.1.0",
"@types/geojson": "^7946.0.10",
"@types/leaflet": "^1.9.0",
@@ -50,6 +52,7 @@
"geodesy": "^1.1.2",
"leaflet": "^1.9.3",
"leaflet-control-mini-map": "^0.4.0",
+ "leaflet-gesture-handling": "^1.2.2",
"leaflet-path-drag": "*",
"leaflet.nauticscale": "^1.1.0",
"nodemon": "^2.0.20",
diff --git a/manager/.vscode/launch.json b/manager/.vscode/launch.json
new file mode 100644
index 00000000..064d058c
--- /dev/null
+++ b/manager/.vscode/launch.json
@@ -0,0 +1,13 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Launch Program",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": [ "start" ],
+ "port": 9229
+ }
+ ]
+}
\ No newline at end of file
diff --git a/manager/app.js b/manager/app.js
new file mode 100644
index 00000000..c6db5299
--- /dev/null
+++ b/manager/app.js
@@ -0,0 +1,33 @@
+const { app, BrowserWindow } = require('electron/main')
+const path = require('node:path')
+
+function createWindow() {
+ const win = new BrowserWindow({
+ width: 1310,
+ height: 800,
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ nodeIntegration: true, // like here
+ },
+ icon: "./../img/olympus.ico"
+ })
+
+ win.loadFile('index.html');
+ win.setMenuBarVisibility(false);
+}
+
+app.whenReady().then(() => {
+ createWindow()
+
+ app.on('activate', () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow()
+ }
+ })
+})
+
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit()
+ }
+})
\ No newline at end of file
diff --git a/manager/icons/check-solid.svg b/manager/icons/check-solid.svg
new file mode 100644
index 00000000..b22c3099
--- /dev/null
+++ b/manager/icons/check-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/manager/icons/circle-info-solid.svg b/manager/icons/circle-info-solid.svg
new file mode 100644
index 00000000..589da70e
--- /dev/null
+++ b/manager/icons/circle-info-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/manager/icons/folder-open-solid.svg b/manager/icons/folder-open-solid.svg
new file mode 100644
index 00000000..847b3122
--- /dev/null
+++ b/manager/icons/folder-open-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/manager/icons/plus-solid.svg b/manager/icons/plus-solid.svg
new file mode 100644
index 00000000..4d70bcf5
--- /dev/null
+++ b/manager/icons/plus-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/manager/icons/trash-can-regular.svg b/manager/icons/trash-can-regular.svg
new file mode 100644
index 00000000..641a8a31
--- /dev/null
+++ b/manager/icons/trash-can-regular.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/manager/index.html b/manager/index.html
new file mode 100644
index 00000000..70c56dc2
--- /dev/null
+++ b/manager/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+ DCS Olympus Manager v{{OLYMPUS_VERSION_NUMBER}}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/manager/instanceDiv.ejs b/manager/instanceDiv.ejs
new file mode 100644
index 00000000..a0e7f588
--- /dev/null
+++ b/manager/instanceDiv.ejs
@@ -0,0 +1,51 @@
+
+
+
+
+
+ " tabindex="<%= index + 6 %>" title="Clicking on this will install all the necessary files in your DCS instance. Remember to close DCS before doing this!">Install Olympus to instance
+ " tabindex="<%= index + 7 %>" title="Clicking on this will apply your changes to the configuration. Remember to restart any running mission!">Apply changes
+ " tabindex="<%= index + 8 %>" title="Clicking on this will remove Olympus from your DCS folder.">Remove Olympus
+
+
+
\ No newline at end of file
diff --git a/manager/package.json b/manager/package.json
new file mode 100644
index 00000000..462f269a
--- /dev/null
+++ b/manager/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "dcsolympus_manager",
+ "version": "1.0.0",
+ "description": "",
+ "main": "app.js",
+ "scripts": {
+ "start": "electron ."
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "ejs": "^3.1.9",
+ "electron": "^28.0.0",
+ "portfinder": "^1.0.32",
+ "regedit": "^5.1.2",
+ "sha256": "^0.2.0"
+ }
+}
diff --git a/manager/preload.js b/manager/preload.js
new file mode 100644
index 00000000..7873b573
--- /dev/null
+++ b/manager/preload.js
@@ -0,0 +1,253 @@
+var regedit = require('regedit')
+var fs = require('fs')
+var path = require('path')
+const ejs = require('ejs')
+const portfinder = require('portfinder')
+const sha256 = require('sha256')
+
+const shellFoldersKey = 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders'
+const saveGamesKey = '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}'
+
+var instanceDivs = [];
+
+function checkPort(port, callback) {
+ portfinder.getPort({port: port, stopPort: port}, (err, res) => {
+ if (err !== null) {
+ console.error(`Port ${port} already in use`);
+ callback(false);
+ } else {
+ callback(true);
+ }
+ });
+}
+
+function installOlympus(folder) {
+ console.log(`Installing Olympus in ${folder}`);
+ fs.cpSync(path.join("..", "Mod"), path.join(folder, "Mods", "Services", "Olympus"), {recursive: true});
+ fs.cpSync(path.join("..", "Scripts", "OlympusHook.lua"), path.join(folder, "Scripts", "OlympusHook.lua"), {recursive: true});
+ fs.cpSync(path.join("..", "olympus.json"), path.join(folder, "Config", "olympus.json"), {recursive: true});
+ loadDivs();
+}
+
+function uninstallOlympus(folder) {
+ console.log(`Uninstalling Olympus from ${folder}`);
+ fs.rmSync(path.join(folder, "Mods", "Services", "Olympus"), {recursive: true});
+ fs.rmSync(path.join(folder, "Config", "olympus.json"), {recursive: true});
+ loadDivs();
+}
+
+function applyConfiguration(folder, data) {
+ console.log(`Applying configuration to Olympus from ${folder}`);
+
+ if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) {
+ var config = JSON.parse(fs.readFileSync(path.join(folder, "Config", "olympus.json")));
+
+ config["client"]["port"] = data["clientPort"];
+ config["server"]["port"] = data["backendPort"];
+ config["server"]["address"] = data["backendAddress"];
+ config["authentication"]["gameMasterPassword"] = sha256(data["gameMasterPassword"]);
+ config["authentication"]["blueCommanderPassword"] = sha256(data["blueCommanderPassword"]);
+ config["authentication"]["redCommanderPassword"] = sha256(data["redCommanderPassword"]);
+
+ fs.writeFileSync(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4));
+ }
+}
+
+class InstanceDiv {
+ element = null;
+ parent = null;
+ folder = "";
+
+ constructor(parent, folder) {
+ this.element = parent;
+ this.folder = folder;
+ this.render();
+ }
+
+ render() {
+ this.element = document.createElement("div");
+
+ var data = {
+ folder: this.folder,
+ installed: false,
+ index: instanceDivs.length * 9
+ };
+
+
+ if (fs.existsSync(path.join(this.folder, "Config", "olympus.json"))) {
+ var config = JSON.parse(fs.readFileSync(path.join(this.folder, "Config", "olympus.json")));
+ data = {
+ ...data,
+ ...config
+ }
+ data["installed"] = true;
+ }
+
+ ejs.renderFile("./instanceDiv.ejs", data, {}, (err, str) => {
+ this.element.innerHTML = str;
+ this.element.querySelector(".add").addEventListener("click", (e) => {
+ if (!e.srcElement.classList.contains("disabled"))
+ installOlympus(this.folder);
+ });
+
+ this.element.querySelector(".remove").addEventListener("click", (e) => {
+ if (!e.srcElement.classList.contains("disabled"))
+ uninstallOlympus(this.folder);
+ });
+
+ this.element.querySelector(".apply").addEventListener("click", (e) => {
+ if (!e.srcElement.classList.contains("disabled"))
+ applyConfiguration(this.folder, this.getFields());
+ });
+
+ var inputs = this.element.querySelectorAll("input");
+ for (let i = 0; i < inputs.length; i++) {
+ inputs[i].addEventListener("change", () => {
+ inputs[i].classList.remove("error");
+ instanceDivs.forEach((instanceDiv) => instanceDiv.checkFields())
+ })
+ }
+ });
+ }
+
+ getDiv() {
+ return this.element;
+ }
+
+ getFields() {
+ return {
+ clientPort: Number(this.element.querySelector("#client-port").value),
+ backendPort: Number(this.element.querySelector("#backend-port").value),
+ backendAddress: this.element.querySelector("#backend-address").value,
+ gameMasterPassword: this.element.querySelector("#game-master-password").value,
+ blueCommanderPassword: this.element.querySelector("#blue-commander-password").value,
+ redCommanderPassword: this.element.querySelector("#red-commander-password").value,
+ }
+ }
+
+ checkFields() {
+ var data = this.getFields();
+
+ /* Clear existing errors */
+ var inputs = this.element.querySelectorAll("input");
+ for (let i = 0; i < inputs.length; i++) {
+ inputs[i].classList.remove("error");
+ }
+ var messages = this.element.querySelectorAll(".error");
+ for (let i = 0; i < messages.length; i++) {
+ messages[i].innerText = "";
+ }
+
+ /* Enable the button */
+ this.element.querySelector(".apply").classList.remove("disabled");
+
+ if (data["clientPort"] !== 0 && data["backendPort"] !== 0) {
+ if (data["clientPort"] === data["backendPort"]) {
+ this.element.querySelector("#client-port").classList.add("error");
+ this.element.querySelector("#client-port-error").innerText = "Ports must be different";
+ this.element.querySelector("#backend-port").classList.add("error");
+ this.element.querySelector("#backend-port-error").innerText = "Ports must be different";
+ this.element.querySelector(".apply").classList.add("disabled");
+ }
+ else {
+ checkPort(data["clientPort"], (res) => {
+ var otherInstanceUsesPort = instanceDivs.find((instanceDiv) => {
+ if (instanceDiv != this) {
+ var fields = instanceDiv.getFields();
+ if (fields["clientPort"] === data["clientPort"] || fields["backendPort"] === data["clientPort"]) {
+ return true;
+ }
+ }
+ })
+
+ if (!res || otherInstanceUsesPort) {
+ this.element.querySelector("#client-port").classList.add("error");
+ this.element.querySelector("#client-port-error").innerText = "Port already in use";
+ this.element.querySelector(".apply").classList.add("disabled");
+ }
+ });
+
+ checkPort(data["backendPort"], (res) => {
+ var otherInstanceUsesPort = instanceDivs.find((instanceDiv) => {
+ if (instanceDiv != this) {
+ var fields = instanceDiv.getFields();
+ if (fields["clientPort"] === data["backendPort"] || fields["backendPort"] === data["backendPort"]) {
+ return true;
+ }
+ }
+ })
+
+ if (!res || otherInstanceUsesPort) {
+ this.element.querySelector("#backend-port").classList.add("error");
+ this.element.querySelector("#backend-port-error").innerText = "Port already in use";
+ this.element.querySelector(".apply").classList.add("disabled");
+ }
+ });
+ }
+ }
+
+ if (data["gameMasterPassword"] !== "" && data["blueCommanderPassword"] !== "" && data["gameMasterPassword"] === data["blueCommanderPassword"]) {
+ this.element.querySelector("#game-master-password").classList.add("error");
+ this.element.querySelector("#game-master-password-error").innerText = "Passwords must be different";
+ this.element.querySelector("#blue-commander-password").classList.add("error");
+ this.element.querySelector("#blue-commander-password-error").innerText = "Passwords must be different";
+ this.element.querySelector(".apply").classList.add("disabled");
+ }
+
+ if (data["gameMasterPassword"] !== "" && data["redCommanderPassword"] !== "" && data["gameMasterPassword"] === data["redCommanderPassword"]) {
+ this.element.querySelector("#game-master-password").classList.add("error");
+ this.element.querySelector("#game-master-password-error").innerText = "Passwords must be different";
+ this.element.querySelector("#red-commander-password").classList.add("error");
+ this.element.querySelector("#red-commander-password-error").innerText = "Passwords must be different";
+ this.element.querySelector(".apply").classList.add("disabled");
+ }
+
+ if (data["blueCommanderPassword"] !== "" && data["redCommanderPassword"] !== "" && data["blueCommanderPassword"] === data["redCommanderPassword"]) {
+ this.element.querySelector("#blue-commander-password").classList.add("error");
+ this.element.querySelector("#blue-commander-password-error").innerText = "Passwords must be different";
+ this.element.querySelector("#red-commander-password").classList.add("error");
+ this.element.querySelector("#red-commander-password-error").innerText = "Passwords must be different";
+ this.element.querySelector(".apply").classList.add("disabled");
+ }
+
+ if (data["gameMasterPassword"] === "" || data["blueCommanderPassword"] === "" || data["redCommanderPassword"] === "") {
+ this.element.querySelector(".apply").classList.add("disabled");
+ }
+ }
+}
+
+function loadDivs() {
+ regedit.list(shellFoldersKey, function(err, result) {
+ if (err) {
+ console.log(err);
+ }
+ else {
+ if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined)
+ {
+ const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value'];
+ const folders = fs.readdirSync(searchpath);
+ instanceDivs = [];
+ const mainDiv = document.getElementById("main-div");
+
+ folders.forEach((folder) => {
+ if (fs.existsSync(path.join(searchpath, folder, "Logs", "dcs.log"))) {
+ instanceDivs.push(new InstanceDiv(mainDiv, path.join(searchpath, folder)));
+ }
+ });
+
+ mainDiv.replaceChildren(...instanceDivs.map((instanceDiv) => {
+ return instanceDiv.getDiv();
+ }));
+
+ instanceDivs.forEach((instanceDiv) => instanceDiv.checkFields())
+
+ } else {
+ console.error("An error occured while trying to fetch the location of the DCS folders.")
+ }
+ }
+ })
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+ loadDivs();
+})
\ No newline at end of file
diff --git a/manager/style.css b/manager/style.css
new file mode 100644
index 00000000..0c9d6f3a
--- /dev/null
+++ b/manager/style.css
@@ -0,0 +1,226 @@
+* {
+ font-family: "Open Sans", sans-serif;
+ box-sizing: border-box;
+}
+
+body {
+ background-color: #181e25;
+}
+
+#header {
+ display: flex;
+ justify-content: start;
+ align-items: center;
+ color: #F2F2F2;
+ font-weight: bold;
+ font-size: 16px;
+ padding: 10px;
+}
+
+#header>div:first-child>div:last-child {
+ color: green;
+}
+
+.main-icon {
+ width: 60px;
+ height: 60px;
+ margin: 10px;
+}
+
+#main-div {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ row-gap: 30px;
+ column-gap: 30px;
+ flex-wrap: wrap;
+ padding: 15px;
+}
+
+body {
+ overflow-y: auto;
+ scrollbar-color: white transparent;
+ scrollbar-width: thin;
+}
+
+::-webkit-scrollbar {
+ width: 10px;
+}
+
+::-webkit-scrollbar-track {
+ background-color: transparent;
+ border-bottom-right-radius: 10px;
+ border-top-right-radius: 10px;
+ margin-top: 0px;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: white;
+ border-radius: 100px;
+ margin-top: 10px;
+ opacity: 0.8;
+}
+
+.instance-div {
+ display: flex;
+ flex-direction: column;
+ row-gap: 10px;
+ background-color: #3d4651;
+ height: fit-content;
+ padding: 20px 40px;
+ border-radius: 5px;
+ border-left: 5px solid #017DC1;
+ width: 600px;
+}
+
+.instance-content {
+ display: flex;
+ flex-direction: column;
+ row-gap: 15px;
+}
+
+.folder-name {
+ color: #F2F2F2;
+ font-weight: bold;
+ display: flex;
+ border-bottom: 1px solid #F2F2F2;
+ padding-bottom: 10px;
+ width: 100%;
+}
+
+.folder-name span {
+ width: 500px;
+ overflow: hidden;
+ text-wrap: nowrap;
+ text-overflow: ellipsis;
+ direction: rtl;
+ text-align: left;
+}
+
+.folder {
+ width: 20px;
+ height: 20px;
+ background-image: url("./icons/folder-open-solid.svg");
+ margin-right: 15px;
+}
+
+.input-table {
+ padding: 0px;
+ margin: 0px;
+ border-width: 0px;
+ border-collapse: collapse;
+}
+
+.input-table td {
+ color: #F2F2F2;
+ padding: 5px 0px;
+ font-size: 13px;
+ vertical-align: top;
+}
+
+.input-table td>* {
+ margin: 2px 0px;
+}
+
+.action-buttons {
+ display: flex;
+ flex-direction: row;
+ column-gap: 12px;
+ justify-content: start;
+}
+
+.label {
+ display: flex;
+ align-items: center;
+ column-gap: 5px;
+}
+
+.icon {
+ background-size: 100% 100%;
+ background-repeat: no-repeat;
+}
+
+.button {
+ width: fit-content;
+ height: 40px;
+ color: #F2F2F2;
+ border: 1px solid #F2F2F2;
+ background-size: 40px 60%;
+ background-position: 0px 50%;
+ background-repeat: no-repeat;
+ border-radius: 5px;
+ padding: 5px 15px 5px 45px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ background-color: transparent;
+}
+
+.button:not(.disabled) {
+ cursor: pointer;
+}
+
+.apply {
+ background-image: url("./icons/check-solid.svg");
+ background-color: #017DC1;
+ border: 1px solid transparent;
+}
+
+.add {
+ padding: 5px 15px 5px 15px;
+}
+
+.update {
+ padding: 5px 15px 5px 15px;
+}
+
+.remove {
+ background-image: url("./icons/trash-can-regular.svg");
+}
+
+.disabled {
+ background-color: #797E83;
+}
+
+.hide {
+ display: none;
+}
+
+.message {
+ font-weight: 600;
+ height: 20px;
+ font-size: 13px;
+ color: #F2F2F2;
+}
+
+input {
+ font-weight: 600;
+ font-size: 13px;
+ width: 240px;
+ border-radius: 4px;
+}
+
+input:focus{
+ outline: none;
+}
+
+input.error {
+ border-color: #FF5858;
+}
+
+.error {
+ font-weight: bold;
+ color: #FF5858;
+}
+
+.accent-green {
+ color: #8bff63;
+}
+
+.info {
+ width: 12px;
+ height: 12px;
+ background-image: url("./icons/circle-info-solid.svg");
+ background-position: 50% 50%;
+}
\ No newline at end of file