Ver código fonte

Resolve "open project via ui"

Eric Tröbs 4 anos atrás
pai
commit
9d042b4207

+ 2 - 2
.gitignore

@@ -33,6 +33,6 @@ __pycache__/
 .coverage
 
 # projects and models
-projects/
-models/
+/projects/
+/models/
 dist/

+ 5 - 0
app.py

@@ -1,11 +1,16 @@
 from pycs.ApplicationStatus import ApplicationStatus
 from pycs.frontend.WebServer import WebServer
+from pycs.projects.ProjectManager import ProjectManager
 
 if __name__ == '__main__':
     # load settings and initialize application status object
     print('- load settings')
     app_status = ApplicationStatus(path_to_settings_json='settings.json')
 
+    # load project manager
+    print('- load project manager')
+    project_manager = ProjectManager(app_status)
+
     # start web server
     print('- start web server')
     web_server = WebServer(app_status)

+ 2 - 2
pycs/ApplicationStatus.py

@@ -12,13 +12,13 @@ class ApplicationStatus(ObservableDict):
         if settings is not None:
             settings = settings
         elif path_to_settings_json is not None and exists(path_to_settings_json):
-            with open(path_to_settings_json) as settings_json:
+            with open(path_to_settings_json, 'r') as settings_json:
                 settings = load(settings_json)
         else:
             settings = {}
 
         # initialize data structure
         super().__init__({
-            'status': {},
+            'projects': [],
             'settings': settings
         })

+ 3 - 0
pycs/observable/ObservableList.py

@@ -9,6 +9,9 @@ class ObservableList(list, Observable):
         for element in lst:
             self.append(Observable.create(element, self))
 
+    def __getitem__(self, value):
+        return super().__getitem__(int(value))
+
     def __setitem__(self, key, value):
         super().__setitem__(key, Observable.create(value, self))
         Observable._notify(self)

+ 29 - 0
pycs/projects/ProjectManager.py

@@ -0,0 +1,29 @@
+from glob import glob
+from json import load
+from os import path
+
+from pycs import ApplicationStatus
+
+
+class ProjectManager:
+    def __init__(self, app_status: ApplicationStatus):
+        # TODO create projects folder if it does not exist
+
+        # find projects
+        for folder in glob('projects/*'):
+            # load project.json
+            with open(path.join(folder, 'project.json'), 'r') as file:
+                project = load(file)
+                project['status'] = 'close'
+
+                app_status['projects'].append(project)
+
+        # subscribe to changes
+        app_status['projects'].subscribe(self.update)
+
+    def update(self, data):
+        # 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'

+ 2 - 2
test/test_application_status.py

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

+ 1 - 0
test/test_observable.py

@@ -114,6 +114,7 @@ class TestObservable(unittest.TestCase):
 
     # TODO test subscription value
     # TODO test complex append
+    # TODO test subscription after adding
 
 
 if __name__ == '__main__':

+ 36 - 19
webui/src/App.vue

@@ -1,42 +1,59 @@
 <template>
   <div id="app">
-    <img alt="Ape" src="/logo.png">
-    <br>
+    <!-- <img alt="Ape" src="/logo.png"> -->
 
-    <button @click="click1">Erhöhen</button>
-    <button @click="click2">Array</button>
-    <button @click="click3">Erweitern</button>
+    <project-open-dialog v-if="showProjectOpenDialog"
+                         :status="status" :socket="socket"/>
+    <project-main-window v-if="showProjectMainWindow"
+                         :status="status" :socket="socket"/>
 
-    <pre style="text-align: left">{{ this.status }}</pre>
+    <pre style="text-align: left; font-size: 80%">{{ this.status }}</pre>
   </div>
 </template>
 
 <script>
 import io from "socket.io-client";
+import ProjectOpenDialog from "@/components/projects/project-open-dialog";
+import ProjectMainWindow from "@/components/projects/project-main-window";
 
 export default {
   name: 'App',
+  components: {
+    ProjectMainWindow,
+    ProjectOpenDialog
+  },
   data: function() {
     return {
       status: null,
       // initialize socket.io connection
-      socket: io('http://localhost:5000'),
-      test: 1
+      socket: {
+        io: io('http://' + window.location.hostname + ':5000'),
+        set: function(path, value) {
+          this.io.emit('set', {path: path, value: value});
+        },
+        add: function(path, value) {
+          this.io.emit('add', {path: path, value: value});
+        }
+      }
     }
   },
-  created() {
-    this.socket.on('app_status', status => this.status = status);
-  },
-  methods: {
-    click1: function() {
-      this.socket.emit('set', {path: 'settings/frontend/test2', value: this.test++})
-    },
-    click2: function() {
-      this.socket.emit('set', {path: 'settings/test-array', value: []})
+  computed: {
+    showProjectOpenDialog: function() {
+      return this.status != null && this.status.projects.filter(x => ['open', 'load'].includes(x.status)).length === 0;
     },
-    click3: function() {
-      this.socket.emit('add', {path: 'settings/test-array', value: this.test++})
+    showProjectMainWindow: function() {
+      return this.status != null && this.status.projects.filter(x => x.status === 'open').length > 0;
     }
+  },
+  created() {
+    this.socket.io.on('app_status', status => this.status = status);
   }
 }
 </script>
+
+<style scoped>
+#app {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 4 - 0
webui/src/assets/icons/three-bars.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
+    <path fill-rule="evenodd"
+          d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path>
+</svg>

+ 16 - 0
webui/src/assets/style/colors.css

@@ -0,0 +1,16 @@
+:root {
+    --primary: #6200ee;
+    --primary_variant: #3700B3;
+    --secondary: #03DAC6;
+    --secondary_variant: #018786;
+
+    --background: #FFFFFF;
+    --surface: #FFFFFF;
+    --error: #B00020;
+
+    --on_primary: #FFFFFF;
+    --on_secondary: #FFFFFF;
+    --on_background: #000000;
+    --on_surface: #000000;
+    --on_error: #FFFFFF;
+}

+ 5 - 0
webui/src/assets/style/main.css

@@ -6,4 +6,9 @@ body {
     margin: 0;
     padding: 0;
     font-family: "Roboto";
+
+    width: 100vw;
+    height: 100vh;
+
+    overflow: hidden;
 }

+ 152 - 0
webui/src/components/projects/project-main-window.vue

@@ -0,0 +1,152 @@
+<template>
+  <div class="project-main-window">
+    <div class="top-navigation">
+      <img class="burger"
+           src="@/assets/icons/three-bars.svg"
+           alt="Menü"
+           v-if="!wide"
+           :class="{ rotation: menu }"
+           @click="invertMenu">
+
+      <div class="name">
+        {{ project.name }}
+      </div>
+    </div>
+
+    <div class="bottom">
+      <div class="side-navigation"
+           :class="{ wide: wide, narrow: !wide, active: menu }">
+        <div class="item" @click="socket.set(projectPath, 'save')">
+          Projekt schließen
+        </div>
+      </div>
+
+      <div class="content">
+        .content
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "project-main-window",
+  props: ['status', 'socket'],
+  data: function() {
+    return {
+      wide: true,
+      menu: false
+    }
+  },
+  computed: {
+    project: function() {
+      return this.status.projects.filter(x => (x.status === 'open'))[0];
+    },
+    projectPath: function() {
+      for (let i = 0; i < this.status.projects.length; i++)
+        if (this.status.projects[i].status === 'open')
+          return 'projects/' + i + '/status';
+
+      return null;
+    }
+  },
+  methods: {
+    resize: function() {
+      this.wide = (document.body.offsetWidth > 1024);
+    },
+    invertMenu: function() {
+      this.menu = !this.menu;
+    }
+  },
+  created: function() {
+    window.addEventListener('resize', this.resize);
+    this.resize();
+  },
+  destroyed: function() {
+    window.removeEventListener('resize', this.resize);
+  }
+}
+</script>
+
+<style scoped>
+.project-main-window {
+  width: 100%;
+  height: 100%;
+
+  display: flex;
+  flex-direction: column;
+}
+
+.top-navigation {
+  background-color: #3f3f3f;
+  color: whitesmoke;
+  display: flex;
+  padding: 1rem;
+
+  flex-direction: row;
+  align-items: center;
+}
+
+.top-navigation .burger {
+  transition: transform 0.5s;
+  filter: invert(1);
+  width: 2rem;
+  margin-right: 0.75rem;
+}
+
+.top-navigation .burger.rotation {
+  transform: rotate(90deg);
+}
+
+.top-navigation .name {
+  font-weight: bold;
+  font-family: "Roboto Condensed";
+}
+
+.bottom {
+  display: flex;
+  flex-grow: 1;
+  flex-direction: row;
+  position: relative;
+}
+
+.side-navigation {
+  background-color: #2f2f2f;
+  color: whitesmoke;
+}
+
+.side-navigation.wide {
+  flex-grow: 0;
+  height: 100%;
+}
+
+.side-navigation.narrow {
+  width: 85vw;
+  left: -85vw;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  transition: 0.3s ease-out;
+}
+
+.side-navigation.narrow.active {
+  left: 0;
+}
+
+.side-navigation .item {
+  padding: 1rem 3rem 1rem 1rem;
+  cursor: pointer;
+}
+
+.side-navigation .item:hover {
+  background-color: #1f1f1f;
+}
+
+.side-navigation .item:not(:last-child) {
+  border-bottom: 1px solid rgba(255, 255, 255, 0.25);
+}
+
+.content {
+  flex-grow: 1;
+}
+</style>

+ 77 - 0
webui/src/components/projects/project-open-dialog.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="project-open-dialog">
+    <div class="title">
+      <h1>Projekt öffnen</h1>
+      Klicken Sie ein Projekt an, um es zu öffnen.
+    </div>
+
+    <div class="projects">
+      <div class="project"
+           v-for="(project, i) in status.projects"
+           :key="project.id"
+           @click="load(i)">
+        <h2>{{ project.name }}</h2>
+        <div>{{ project.description }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "project-open-dialog",
+  props: ['status', 'socket'],
+  methods: {
+    load: function(index) {
+      this.socket.set('projects/' + index + '/status', 'load');
+    }
+  }
+}
+</script>
+
+<style scoped>
+.project-open-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;
+}
+
+h1 {
+  margin: 0 0 0.4rem 0;
+}
+
+h2, h3 {
+  margin: 0;
+}
+
+.title {
+  margin: 0;
+  padding: 2rem;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+  flex-grow: 0;
+}
+
+.projects {
+  overflow: auto;
+  flex-grow: 1;
+}
+
+.project:not(:last-child) {
+  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+.project {
+  padding: 1rem 2rem;
+}
+</style>

+ 1 - 0
webui/src/main.js

@@ -1,5 +1,6 @@
 // import global styles
 import './assets/style/main.css'
+import './assets/style/colors.css'
 import './assets/style/fonts.css'
 
 // initialize vuejs app