6
0
فهرست منبع

Resolve "create project dialog"

Eric Tröbs 4 سال پیش
والد
کامیت
59cb6a01dd

+ 5 - 0
app.py

@@ -1,5 +1,6 @@
 from pycs.ApplicationStatus import ApplicationStatus
 from pycs.frontend.WebServer import WebServer
+from pycs.models.ModelManager import ModelManager
 from pycs.projects.ProjectManager import ProjectManager
 
 if __name__ == '__main__':
@@ -7,6 +8,10 @@ if __name__ == '__main__':
     print('- load settings')
     app_status = ApplicationStatus(path_to_settings_json='settings.json')
 
+    # load model manager
+    print('- load model manager')
+    model_manager = ModelManager(app_status)
+
     # load project manager
     print('- load project manager')
     project_manager = ProjectManager(app_status)

+ 1 - 0
pycs/ApplicationStatus.py

@@ -19,6 +19,7 @@ class ApplicationStatus(ObservableDict):
 
         # initialize data structure
         super().__init__({
+            'models': {},
             'projects': [],
             'settings': settings
         })

+ 18 - 0
pycs/models/ModelManager.py

@@ -0,0 +1,18 @@
+from glob import glob
+from json import load
+from os import path
+
+from pycs import ApplicationStatus
+
+
+class ModelManager:
+    def __init__(self, app_status: ApplicationStatus):
+        # TODO create models folder if it does not exist
+
+        # find models
+        for folder in glob('models/*'):
+            # load distribution.json
+            with open(path.join(folder, 'distribution.json'), 'r') as file:
+                model = load(file)
+                model_id = model['id']
+                app_status['models'][model_id] = model

+ 1 - 1
pycs/observable/ObservableDict.py

@@ -7,7 +7,7 @@ class ObservableDict(dict, Observable):
         Observable.__init__(self, parent)
 
         for key in obj.keys():
-            self[key] = Observable.create(obj[key], self)
+            dict.__setitem__(self, key, Observable.create(obj[key], self))
 
     def __setitem__(self, key, value):
         dict.__setitem__(self, key, Observable.create(value, self))

+ 31 - 2
pycs/projects/ProjectManager.py

@@ -1,6 +1,8 @@
 from glob import glob
-from json import load
-from os import path
+from json import load, dump
+from os import path, mkdir
+from time import time
+from uuid import uuid1
 
 from pycs import ApplicationStatus
 
@@ -22,8 +24,35 @@ class ProjectManager:
         app_status['projects'].subscribe(self.update)
 
     def update(self, data):
+        # detect project to create
+        for i in range(len(data)):
+            if data[i]['status'] == 'create':
+                # create dict representation
+                uuid = str(uuid1())
+
+                data[i] = {
+                    'id': uuid,
+                    'status': 'close',
+                    'name': data[i]['name'],
+                    'description': data[i]['description'],
+                    'created': int(time()),
+                    'access': 0,
+                    'pipeline': {
+                        'model-distribution': data[i]['model']
+                    }
+                }
+
+                # create project directory
+                folder = path.join('projects', uuid)
+                mkdir(folder)
+
+                # create project.json
+                with open(path.join(folder, 'project.json'), 'w') as file:
+                    dump(data[i], file, indent=4)
+
         # detect project to load
         to_load = list(filter(lambda x: x['status'] == 'load', data))
         for project in to_load:
             # TODO actually load pipeline
             project['status'] = 'open'
+            project['access'] = time()

+ 2 - 0
test/test_application_status.py

@@ -7,6 +7,7 @@ class TestApplicationStatus(unittest.TestCase):
     def test_load_default(self):
         aso = ApplicationStatus()
         self.assertEqual({
+            'models': {},
             'projects': [],
             'settings': {}
         }, aso)
@@ -19,6 +20,7 @@ class TestApplicationStatus(unittest.TestCase):
 
         aso = ApplicationStatus(settings=settings)
         self.assertEqual({
+            'models': {},
             'projects': [],
             'settings': settings
         }, aso)

+ 1 - 0
test/test_observable.py

@@ -115,6 +115,7 @@ class TestObservable(unittest.TestCase):
     # TODO test subscription value
     # TODO test complex append
     # TODO test subscription after adding
+    # TODO test multiple subscription calls when a child is added
 
 
 if __name__ == '__main__':

+ 14 - 4
webui/src/App.vue

@@ -3,7 +3,11 @@
     <!-- <img alt="Ape" src="/logo.png"> -->
 
     <project-open-dialog v-if="showProjectOpenDialog"
-                         :status="status" :socket="socket"/>
+                         :status="status" :socket="socket"
+                         @create="showProjectCreationDialog=true"/>
+    <project-creation-dialog v-if="showProjectCreationDialog"
+                             :status="status" :socket="socket"
+                             @cancel="showProjectCreationDialog=false"/>
     <project-main-window v-if="showProjectMainWindow"
                          :status="status" :socket="socket"/>
 
@@ -15,10 +19,12 @@
 import io from "socket.io-client";
 import ProjectOpenDialog from "@/components/projects/project-open-dialog";
 import ProjectMainWindow from "@/components/projects/project-main-window";
+import ProjectCreationDialog from "@/components/projects/project-creation-dialog";
 
 export default {
   name: 'App',
   components: {
+    ProjectCreationDialog,
     ProjectMainWindow,
     ProjectOpenDialog
   },
@@ -34,15 +40,19 @@ export default {
         add: function(path, value) {
           this.io.emit('add', {path: path, value: value});
         }
-      }
+      },
+      showProjectCreationDialog: false
     }
   },
   computed: {
     showProjectOpenDialog: function() {
-      return this.status != null && this.status.projects.filter(x => ['open', 'load'].includes(x.status)).length === 0;
+      return this.status != null
+          && this.status.projects.filter(x => ['open', 'load'].includes(x.status)).length === 0
+          && !this.showProjectCreationDialog;
     },
     showProjectMainWindow: function() {
-      return this.status != null && this.status.projects.filter(x => x.status === 'open').length > 0;
+      return this.status != null && this.status.projects.filter(x => x.status === 'open').length > 0
+          && !this.showProjectCreationDialog;
     }
   },
   created() {

+ 20 - 0
webui/src/components/base/button-input.vue

@@ -0,0 +1,20 @@
+<template>
+  <button @click="$emit('click', $event)">
+    <slot/>
+  </button>
+</template>
+
+<script>
+export default {
+  name: "button-input"
+}
+</script>
+
+<style scoped>
+button {
+  padding: 0.67rem 1.5rem;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 0.75rem;
+  cursor: pointer;
+}
+</style>

+ 31 - 0
webui/src/components/base/select-input.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="input">
+    <label>
+      <span class="label"><slot></slot></span>
+      <select v-bind:value="value"
+              v-on:input="$emit('input', $event.target.value)">
+        <option v-for="v in values"
+                :key="v.value"
+                :value="v.value">{{ v.name }}</option>
+      </select>
+    </label>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "text-input",
+  props: ['value', 'values']
+}
+</script>
+
+<style scoped>
+.label {
+  font-size: 80%;
+}
+
+select {
+  width: 100%;
+  margin-top: 0.2rem;
+}
+</style>

+ 41 - 0
webui/src/components/base/text-input.vue

@@ -0,0 +1,41 @@
+<template>
+  <div class="input">
+    <label>
+      <span class="label"><slot></slot></span>
+      <input type="text"
+             :placeholder="placeholder"
+             :class="{error: error}"
+             v-bind:value="value"
+             v-on:input="input">
+    </label>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "text-input",
+  props: ['value', 'placeholder', 'error'],
+  methods: {
+    input: function(event) {
+      this.$emit('clearError', null);
+      this.$emit('input', event.target.value)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.label {
+  font-size: 80%;
+}
+
+input {
+  width: 100%;
+  margin-top: 0.2rem;
+}
+
+input.error {
+  background-color: rgba(255, 0, 0, 0.3);
+  border-color: var(--error);
+}
+</style>

+ 41 - 0
webui/src/components/base/textarea-input.vue

@@ -0,0 +1,41 @@
+<template>
+  <div class="input">
+    <label>
+      <span class="label"><slot></slot></span>
+      <textarea :placeholder="placeholder"
+                :class="{error: error}"
+                v-bind:value="value"
+                v-on:input="input"></textarea>
+    </label>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "textarea-input",
+  props: ['value', 'placeholder', 'error'],
+  methods: {
+    input: function(event) {
+      this.$emit('clearError', null);
+      this.$emit('input', event.target.value)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.label {
+  font-size: 80%;
+}
+
+textarea {
+  width: 100%;
+  resize: none;
+  margin-top: 0.2rem;
+}
+
+textarea.error {
+  background-color: rgba(255, 0, 0, 0.3);
+  border-color: var(--error);
+}
+</style>

+ 152 - 0
webui/src/components/projects/project-creation-dialog.vue

@@ -0,0 +1,152 @@
+<template>
+  <div class="project-creation-dialog">
+    <h1 class="title">Create Project</h1>
+
+    <div class="content">
+      <text-input placeholder="Name"
+                  v-model="name"
+                  :error="nameError"
+                  @clearError="nameError = false">
+        Name
+      </text-input>
+
+      <textarea-input placeholder="Description"
+                      v-model="description"
+                      :error="descriptionError"
+                      @clearError="descriptionError = false">
+        Description
+      </textarea-input>
+
+      <select-input :values="models"
+                    v-model="model">
+        Model
+      </select-input>
+    </div>
+
+    <div class="footer">
+      <button-input @click="create">
+        Anlegen
+      </button-input>
+
+      <button-input @click="$emit('cancel', null)">
+        Abbrechen
+      </button-input>
+    </div>
+  </div>
+</template>
+
+<script>
+import TextInput from "@/components/base/text-input";
+import TextareaInput from "@/components/base/textarea-input";
+import SelectInput from "@/components/base/select-input";
+import ButtonInput from "@/components/base/button-input";
+
+export default {
+  name: "project-creation-dialog",
+  components: {ButtonInput, SelectInput, TextareaInput, TextInput},
+  props: ['status', 'socket'],
+  data: function() {
+    return {
+      name: '',
+      nameError: false,
+      description: '',
+      descriptionError: false,
+      model: null
+    }
+  },
+  computed: {
+    models: function() {
+      let result = [];
+
+      for (let o in this.status.models) {
+        result.push({
+          'name': this.status.models[o].name,
+          'value': this.status.models[o].id
+        });
+      }
+
+      return result;
+    }
+  },
+  created: function() {
+    this.model = this.models[0].value;
+  },
+  methods: {
+    create: function() {
+      this.nameError = !this.name;
+      this.descriptionError = !this.description;
+
+      if (this.nameError || this.descriptionError)
+        return;
+
+      this.socket.add(
+          'projects',
+          {
+            status: 'create',
+            name: this.name,
+            description: this.description,
+            model: this.model
+          }
+      );
+      this.$emit('cancel', null);
+    }
+  }
+}
+</script>
+
+<style scoped>
+.project-creation-dialog {
+  position: fixed;
+  top: 5vh;
+  left: 5vw;
+
+  width: 90vw;
+  height: 90vh;
+
+  background-color: var(--background);
+  border-radius: 1rem;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  box-shadow: 0.3rem 0.3rem 0.4rem rgba(0, 0, 0, 0.2);
+
+  display: flex;
+  flex-direction: column;
+}
+
+.title {
+  margin: 0;
+  padding: 2rem;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+  flex-grow: 0;
+  display: flex;
+}
+
+.content {
+  overflow: auto;
+  padding: 1rem;
+  flex-grow: 1;
+}
+
+.content > * {
+  margin-bottom: 0.6rem;
+}
+
+/deep/ .content textarea {
+  height: 4rem;
+}
+
+.footer {
+  flex-grow: 0;
+  padding: 2rem;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+}
+
+.footer > * {
+  margin-left: 0.5rem;
+}
+
+.cancel {
+  cursor: pointer;
+}
+</style>

+ 57 - 3
webui/src/components/projects/project-open-dialog.vue

@@ -2,7 +2,7 @@
   <div class="project-open-dialog">
     <div class="title">
       <h1>Projekt öffnen</h1>
-      Klicken Sie ein Projekt an, um es zu öffnen.
+      Klicken Sie ein Projekt an, um es zu öffnen oder neu anzulegen.
     </div>
 
     <div class="projects">
@@ -11,19 +11,57 @@
            :key="project.id"
            @click="load(i)">
         <h2>{{ project.name }}</h2>
-        <div>{{ project.description }}</div>
+
+        <div class="description">{{ project.description }}</div>
+
+        <div v-if="project.access === 0">
+          created on {{ datetime(project.created) }}
+        </div>
+        <div v-else>
+          {{ datetime(project.access) }}
+        </div>
       </div>
     </div>
+
+    <div class="footer">
+      <button-input @click="$emit('create', null)">
+        new project
+      </button-input>
+    </div>
   </div>
 </template>
 
 <script>
+import ButtonInput from "@/components/base/button-input";
+
 export default {
   name: "project-open-dialog",
+  components: {ButtonInput},
   props: ['status', 'socket'],
   methods: {
     load: function(index) {
       this.socket.set('projects/' + index + '/status', 'load');
+    },
+    datetime: function(timestamp) {
+      const date = new Date(timestamp * 1000);
+
+      const da = date.getDate();
+      const mo = date.getMonth() + 1;
+      const ye = date.getFullYear();
+      const ho = date.getHours();
+      const mi = date.getMinutes();
+
+      return [
+        [
+          da < 10 ? '0' + da : da,
+          mo < 10 ? '0' + mo : mo,
+          ye
+        ].join('.'),
+        [
+          ho < 10 ? '0' + ho : ho,
+          mi < 10 ? '0' + mi : mi
+        ].join(':')
+      ].join(' ');
     }
   }
 }
@@ -51,8 +89,10 @@ h1 {
   margin: 0 0 0.4rem 0;
 }
 
-h2, h3 {
+h2 {
   margin: 0;
+  font-size: 120%;
+  font-family: "Roboto Condensed";
 }
 
 .title {
@@ -73,5 +113,19 @@ h2, h3 {
 
 .project {
   padding: 1rem 2rem;
+  cursor: pointer;
+}
+
+.description {
+  text-overflow: ellipsis;
+  width: 100%;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+.footer {
+  padding: 2rem;
+  display: flex;
+  justify-content: flex-end;
 }
 </style>