Skip to content

Commit

Permalink
feat(load-config): added upload config feature
Browse files Browse the repository at this point in the history
Closes #107

Signed-off-by: Lukas Mertens <[email protected]>

commit-id:0bb94152
  • Loading branch information
lukas-mertens committed Apr 15, 2024
1 parent 53ea31a commit c8b7d71
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 23 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ pnpm-debug.log*
# cypress
cypress/videos
cypress/screenshots

# schemas
/public/schemas/*.json
1 change: 0 additions & 1 deletion public/schemas/.gitignore

This file was deleted.

115 changes: 105 additions & 10 deletions src/components/CreateConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@

<template>
<div class="btn-container">
<v-tooltip location="right" open-delay="500" v-if="state == ComponentStates.DEFAULT">
<template v-slot:activator="{ props }">
<v-btn color="default"
variant="flat"
density="compact"
icon="mdi-upload"
data-cy="upload-config-btn"
v-bind="props"
@click="uploadConfigPrompt()"
></v-btn>
</template>
<span>Create Config</span>
</v-tooltip>
<v-tooltip location="right" open-delay="500" v-if="state == ComponentStates.DEFAULT">
<template v-slot:activator="{ props }">
<v-btn color="default"
Expand All @@ -24,7 +37,7 @@
data-cy="abort-create-config-btn"
icon="mdi-close"
v-bind="props"
@click="abortConfigCreation()"
@click="resetDialog()"
></v-btn>
</template>
<span>Abort</span>
Expand All @@ -38,7 +51,7 @@
data-cy="accept-create-config-btn"
v-bind="props"
:disabled="!configNameValid"
@click="createConfig()"
@click="onAcceptBtnClick()"
></v-btn>
</template>
<span>Create Config</span>
Expand All @@ -52,38 +65,80 @@
placeholder="config name"
:rules="[validateConfigName]"
></v-text-field>
<v-dialog v-model="showErrorDialog" @click:outside="resetDialog()">
<v-card color="danger">
<v-card-title>Couldn't load config</v-card-title>
<v-card-text>
<pre><code>{{ errors }}</code></pre>
</v-card-text>
<v-card-actions>
<v-btn color="primary" @click="resetDialog()">OK</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script setup lang="ts">
import {computed, ref} from "vue";
import {useEvbcStore} from "@/store/evbc";
import {storeToRefs} from "pinia";
import yaml from "js-yaml";
import Ajv from "ajv";
import {EverestConfig} from "@/modules/evbc";

enum ComponentStates {
DEFAULT,
ASK_USER_FOR_CONFIG_NAME,
DEFAULT,
ASK_USER_FOR_CONFIG_NAME,
}

const evbcStore = useEvbcStore();
const state = ref<ComponentStates>(ComponentStates.DEFAULT);
const configName = ref<string>("");
const configNameValid = computed<boolean>(() => validateConfigName() === true);
const emit = defineEmits<{
createConfig: [name: string],
createConfig: [name: string, content?: EverestConfig],
}>();
const {available_configs} = storeToRefs(evbcStore);
const configContent = ref<EverestConfig>(null);
const errors = ref<string>(null);
const showErrorDialog = computed<boolean>(() => !!errors.value);

function createConfig() {
function onAcceptBtnClick() {
if (validateConfigName() === true) {
emit("createConfig", configName.value);
state.value = ComponentStates.DEFAULT;
configName.value = "";
emit("createConfig", configName.value, configContent.value ?? undefined);
resetDialog();
}
}

function abortConfigCreation() {
function resetDialog() {
state.value = ComponentStates.DEFAULT;
configName.value = "";
configContent.value = null;
errors.value = null;
}

function uploadConfigPrompt() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json,.yaml,.yml";
input.click();
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
const parseResult = await parseConfig(e.target?.result as string);
if (!parseResult.errors) {
configContent.value = parseResult.config;
configName.value = file.name.replace(/\.[^.]+$/, ""); // remove file extension
state.value = ComponentStates.ASK_USER_FOR_CONFIG_NAME;
} else {
errors.value = parseResult.errors;
}
};
reader.readAsText(file);
}
};
}

/**
Expand All @@ -102,6 +157,46 @@ const {available_configs} = storeToRefs(evbcStore);
return true;
}
}

/**
* Validates that the config content is a valid JSON or YAML config
*/
async function validateConfigContent(content: object): Promise<true | string> {
const ajv = new Ajv();
const schema = await getConfigJsonSchema();
const validate = ajv.compile(schema);
const valid = validate(content);
if (valid) {
return true;
} else {
return JSON.stringify(validate.errors, null, 2);
}
}

async function getConfigJsonSchema(): Promise<object> {
const response = await fetch(`${import.meta.env.BASE_URL}/public/schemas/config.json`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}

/**
* Parse config
*/
async function parseConfig(content: string): Promise<{ errors: string, config: EverestConfig }> {
try {
const config = yaml.load(content);
const validationResult = await validateConfigContent(config);
if (validationResult === true) {
return {errors: null, config: config as EverestConfig};
} else {
return {errors: validationResult, config: null};
}
} catch (e) {
return {errors: e.toString(), config: null};
}
}
</script>


Expand Down
7 changes: 4 additions & 3 deletions src/components/EvModuleList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<template v-slot:activator="{ props }">
<v-list-item :title="config" v-bind="props" @click="load_config_if_empty(config)" data-cy="config-list-item">
<template v-slot:append>
<v-icon>mdi-upload</v-icon>
<v-icon>mdi-file-document-arrow-right</v-icon>
</template>
</v-list-item>
</template>
Expand Down Expand Up @@ -86,6 +86,7 @@ import EvDialog from "@/components/EvDialog.vue";
import EVConfigModel from "@/modules/evbc/config_model";
import {Notyf} from "notyf";
import CreateConfig from "@/components/CreateConfig.vue";
import {EverestConfig} from "@/modules/evbc";

let evbcStore: ReturnType<typeof useEvbcStore>;
let evbc: EVBackendClient;
Expand Down Expand Up @@ -171,8 +172,8 @@ export default defineComponent({
evbcStore.get_config_context().clicked_terminal(terminalToClick, added_module_id);
}
},
create_config(name: string) {
const new_config = evbc.create_empty_config(name);
create_config(name: string, content?: EverestConfig) {
const new_config = evbc.create_config_model(name, content);
evbcStore.setOpenedConfig(new_config);
this.expansionPanelState = ["modules"];
},
Expand Down
12 changes: 3 additions & 9 deletions src/modules/evbc/client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest

import {
EventHandler,
EverestConfigList,
EverestDefinitions,
EverestInterfaceDefinitionList,
EverestModuleDefinitionList,
} from ".";
import {EventHandler, EverestConfig, EverestDefinitions,} from ".";
import EVConfigModel from "./config_model";
import EVBackendConnection, {ConnectionStatus} from "./connection";
import {useEvbcStore} from "@/store/evbc";
Expand Down Expand Up @@ -78,8 +72,8 @@ class EVBackendClient {
return new EVConfigModel(this.everest_definitions, name, config);
}

create_empty_config(name: string): EVConfigModel {
return new EVConfigModel(this.everest_definitions, name);
create_config_model(name: string, config?: EverestConfig): EVConfigModel {
return new EVConfigModel(this.everest_definitions, name, config ?? undefined);
}

async save_config(config: EVConfigModel) {
Expand Down

0 comments on commit c8b7d71

Please sign in to comment.