6
0
Просмотр исходного кода

Resolve "add button to perform predictions"

Eric Tröbs 4 лет назад
Родитель
Сommit
83b21e07b0

+ 1 - 1
pycs/ApplicationStatus.py

@@ -21,7 +21,7 @@ class ApplicationStatus(ObservableDict):
         super().__init__({
         super().__init__({
             'settings': settings,
             'settings': settings,
             'models': {},
             'models': {},
-            'projects': [],
+            'projects': {},
             'jobs': []
             'jobs': []
         })
         })
 
 

+ 30 - 63
pycs/frontend/WebServer.py

@@ -17,14 +17,11 @@ from pycs.util.RecursiveDictionary import set_recursive
 
 
 
 
 class WebServer:
 class WebServer:
-    def __init__(self, app_status: ApplicationStatus, pipeline_manager: PipelineManager):
+    def __init__(self, app_status: ApplicationStatus):
         # initialize web server
         # initialize web server
         if exists('webui/index.html'):
         if exists('webui/index.html'):
             print('production build')
             print('production build')
 
 
-            # TODO update file upload
-            # files = FileProvider(app_status)
-
             # find svg icons and add them as separate static files to
             # find svg icons and add them as separate static files to
             # set their correct mime type / content_type
             # set their correct mime type / content_type
             static_files = {'/': 'webui/'}
             static_files = {'/': 'webui/'}
@@ -42,9 +39,6 @@ class WebServer:
         else:
         else:
             print('development build')
             print('development build')
 
 
-            # TODO update file upload
-            # files = FileProvider(app_status, cors=True)
-
             self.__sio = socketio.Server(cors_allowed_origins='*')
             self.__sio = socketio.Server(cors_allowed_origins='*')
             self.__flask = Flask(__name__)
             self.__flask = Flask(__name__)
             self.__app = socketio.WSGIApp(self.__sio, self.__flask)
             self.__app = socketio.WSGIApp(self.__sio, self.__flask)
@@ -60,7 +54,6 @@ class WebServer:
         # define events
         # define events
         @self.__sio.event
         @self.__sio.event
         def connect(id, msg):
         def connect(id, msg):
-            print('connect')
             self.__sio.emit('app_status', self.__status, to=id)
             self.__sio.emit('app_status', self.__status, to=id)
 
 
         @self.__flask.route('/settings', methods=['POST'])
         @self.__flask.route('/settings', methods=['POST'])
@@ -68,56 +61,40 @@ class WebServer:
             data = request.get_json(force=True)
             data = request.get_json(force=True)
             set_recursive(data, app_status['settings'])
             set_recursive(data, app_status['settings'])
 
 
-            response = make_response()
-            response.headers['Access-Control-Allow-Origin'] = '*'
-            return response
+            return response()
 
 
         @self.__flask.route('/projects', methods=['POST'])
         @self.__flask.route('/projects', methods=['POST'])
         def create_project():
         def create_project():
             data = request.get_json(force=True)
             data = request.get_json(force=True)
+            app_status['projects'].create_project(data['name'], data['description'], data['model'])
 
 
-            # TODO move to project manager
-            app_status['projects'].append({
-                'status': 'create',
-                'name': data['name'],
-                'description': data['description'],
-                'model': data['model']
-            })
-
-            response = make_response()
-            response.headers['Access-Control-Allow-Origin'] = '*'
-            return response
+            return response()
 
 
         @self.__flask.route('/projects/<identifier>', methods=['POST'])
         @self.__flask.route('/projects/<identifier>', methods=['POST'])
         def edit_project(identifier):
         def edit_project(identifier):
             data = request.get_json(force=True)
             data = request.get_json(force=True)
 
 
-            # TODO move to project manager
-            for project in app_status['projects']:
-                if project['id'] == identifier:
-                    # delete
-                    if 'delete' in data.keys():
-                        project['action'] = 'delete'
-                    # update
-                    else:
-                        set_recursive(data, project)
-                        project['action'] = 'update'
+            if 'delete' in data.keys():
+                app_status['projects'].delete_project(identifier)
+            elif 'predict' in data.keys():
+                app_status['projects'].predict(identifier, data['predict'])
+            else:
+                app_status['projects'].update_project(identifier, data)
 
 
             return response()
             return response()
 
 
         @self.__flask.route('/projects/<identifier>/data', methods=['POST'])
         @self.__flask.route('/projects/<identifier>/data', methods=['POST'])
         def upload_file(identifier):
         def upload_file(identifier):
-            # TODO move to project manager
-            file_uuid = str(uuid1())
+            # abort if project id is not valid
+            if identifier not in app_status['projects'].keys():
+                # TODO return 404
+                return make_response('project does not exist', 500)
 
 
-            # get current project path
-            opened_projects = list(filter(lambda x: x['id'] == identifier, app_status['projects']))
-            if len(opened_projects) == 0:
-                return make_response('no open project available', 500)
-
-            current_project = opened_projects[0]
-            upload_path = path.join('projects', current_project['id'], 'data')
+            # get project and upload path
+            project = app_status['projects'][identifier]
+            upload_path, file_uuid = project.new_media_file_path()
 
 
+            # prepare wrapper objects
             job = GenericWrapper()
             job = GenericWrapper()
             file_name = GenericWrapper()
             file_name = GenericWrapper()
             file_extension = GenericWrapper()
             file_extension = GenericWrapper()
@@ -167,10 +144,7 @@ class WebServer:
             job['finished'] = int(time())
             job['finished'] = int(time())
 
 
             # add to project files
             # add to project files
-            if 'data' not in current_project:
-                current_project['data'] = []
-
-            current_project['data'].append({
+            project.add_media_file({
                 'id': file_uuid,
                 'id': file_uuid,
                 'name': file_name.value,
                 'name': file_name.value,
                 'extension': file_extension.value,
                 'extension': file_extension.value,
@@ -183,32 +157,25 @@ class WebServer:
 
 
         @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', methods=['GET'])
         @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', methods=['GET'])
         def get_file(project_identifier, file_identifier):
         def get_file(project_identifier, file_identifier):
-            # get current project
-            opened_projects = list(filter(lambda x: x['id'] == project_identifier, app_status['projects']))
-            if len(opened_projects) == 0:
-                return make_response('no open project available', 500)
+            # abort if project id is not valid
+            if project_identifier not in app_status['projects'].keys():
+                return make_response('project does not exist', 500)
 
 
-            current_project = opened_projects[0]
-            file_directory = path.join(getcwd(), 'projects', current_project['id'], 'data')
+            project = app_status['projects'][project_identifier]
 
 
-            print(current_project)
+            # abort if file id is not valid
+            if file_identifier not in project['data'].keys():
+                return make_response('file does not exist', 500)
 
 
-            # get object
-            data_objects = list(filter(lambda x: x['id'] == file_identifier, current_project['data']))
-            if len(data_objects) == 0:
-                return make_response('data object not avilable', 500)
+            target_object = project['data'][file_identifier]
 
 
-            target_object = data_objects[0]
+            # construct directory and filename
+            file_directory = path.join(getcwd(), 'projects', project['id'], 'data')
+            file_name = target_object['id'] + target_object['extension']
 
 
             # return data
             # return data
-            file_name = target_object['id'] + target_object['extension']
             return send_from_directory(file_directory, file_name)
             return send_from_directory(file_directory, file_name)
 
 
-        @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', methods=['POST'])
-        def run_prediction(project_identifier, file_identifier):
-            pipeline_manager.run(project_identifier, file_identifier)
-            return response()
-
         # finally start web server
         # finally start web server
         host = app_status['settings']['frontend']['host']
         host = app_status['settings']['frontend']['host']
         port = app_status['settings']['frontend']['port']
         port = app_status['settings']['frontend']['port']

+ 10 - 6
pycs/observable/Observable.py

@@ -4,6 +4,10 @@ class Observable:
         from . import ObservableDict
         from . import ObservableDict
         from . import ObservableList
         from . import ObservableList
 
 
+        if isinstance(source, ObservableDict):
+            return source
+        if isinstance(source, ObservableList):
+            return source
         if isinstance(source, dict):
         if isinstance(source, dict):
             return ObservableDict(source, parent)
             return ObservableDict(source, parent)
         if isinstance(source, list):
         if isinstance(source, list):
@@ -12,18 +16,18 @@ class Observable:
             return source
             return source
 
 
     def __init__(self, parent):
     def __init__(self, parent):
-        self.__parent = parent
-        self.__subscriptions = []
+        self.parent = parent
+        self.subscriptions = []
 
 
     def subscribe(self, handler, immediate=False):
     def subscribe(self, handler, immediate=False):
-        self.__subscriptions.append(handler)
+        self.subscriptions.append(handler)
 
 
         if immediate:
         if immediate:
             handler(self)
             handler(self)
 
 
     def _notify(self):
     def _notify(self):
-        for s in self.__subscriptions:
+        for s in self.subscriptions:
             s(self)
             s(self)
 
 
-        if self.__parent is not None:
-            self.__parent._notify()
+        if self.parent is not None:
+            self.parent._notify()

+ 46 - 36
pycs/pipeline/PipelineManager.py

@@ -6,47 +6,43 @@ from eventlet import tpool
 from pycs.ApplicationStatus import ApplicationStatus
 from pycs.ApplicationStatus import ApplicationStatus
 from pycs.pipeline.Job import Job
 from pycs.pipeline.Job import Job
 from pycs.pipeline.tf1.pipeline import Pipeline as TF1Pipeline
 from pycs.pipeline.tf1.pipeline import Pipeline as TF1Pipeline
+from pycs.projects.Project import Project
 
 
 
 
 class PipelineManager:
 class PipelineManager:
-    def __init__(self, app_status: ApplicationStatus):
-        self.app_status = app_status
-        # app_status['projects'].subscribe(self.__update)
+    def __init__(self, project: Project):
+        self.project = project
+        self.pipeline = tpool.execute(self.__load_pipeline, project['pipeline']['model-distribution'])
 
 
-    def run(self, project_identifier, file_identifier):
-        # find project
-        opened_projects = list(filter(lambda x: x['id'] == project_identifier, self.app_status['projects']))
-        if len(opened_projects) == 0:
-            return
+    def __enter__(self):
+        return self
 
 
-        current_project = opened_projects[0]
+    def __exit__(self, type, value, traceback):
+        self.pipeline.close()
 
 
-        # find data object
-        data_objects = list(filter(lambda x: x['id'] == file_identifier, current_project['data']))
-        if len(data_objects) == 0:
-            return
+    def run(self, media_file):
+        print('>>>', media_file)
 
 
-        target_objects = list(filter(lambda o: 'predictionResults' not in o.keys(), current_project['data']))
-        if len(target_objects) == 0:
-            return
+        # create job list
+        # TODO update job progress
+        job = Job('detect-faces', self.project['id'], media_file)
+        result = tpool.execute(lambda p, j: p.execute(j), self.pipeline, job)
+        media_file['predictionResults'] = result.predictions
 
 
-        # load pipeline
-        pipeline = tpool.execute(self.__load_pipeline, current_project['pipeline']['model-distribution'])
+        print('<<<', media_file)
 
 
-        for target_object in target_objects:
-            print('>>>', target_object)
+    def __load_pipeline(self, pipeline_identifier):
+        model_distribution = self.project.parent.parent['models'][pipeline_identifier]
 
 
-            # create job list
-            # TODO update job progress
-            job = Job('detect-faces', current_project['id'], target_object)
-            result = tpool.execute(lambda p, j: p.execute(j), pipeline, job)
-            target_object['predictionResults'] = result.predictions
+        if model_distribution['mode'] == 'tf1':
+            model_root = path.join(getcwd(), 'models', model_distribution['name'])
 
 
-            print('<<<', target_object)
+            pipeline = TF1Pipeline()
+            pipeline.load(model_root, model_distribution['pipeline'])
 
 
-        # close pipeline
-        pipeline.close()
+            return pipeline
 
 
+    '''
     def __update(self, data):
     def __update(self, data):
         # get current project path
         # get current project path
         opened_projects = list(filter(lambda x: x['status'] == 'open', data))
         opened_projects = list(filter(lambda x: x['status'] == 'open', data))
@@ -73,14 +69,28 @@ class PipelineManager:
 
 
         # close pipeline
         # close pipeline
         pipeline.close()
         pipeline.close()
+    '''
 
 
-    def __load_pipeline(self, pipeline_identifier):
-        model_distribution = self.app_status['models'][pipeline_identifier]
-
-        if model_distribution['mode'] == 'tf1':
-            model_root = path.join(getcwd(), 'models', model_distribution['name'])
+    '''
+    def __update(self, data):
+        for current_project in data:
+            print('>>>>>>>>>>')
+            # find images to predict
+            if 'data' not in current_project.keys() or len(current_project['data']) == 0:
+                return
 
 
-            pipeline = TF1Pipeline()
-            pipeline.load(model_root, model_distribution['pipeline'])
+            # load pipeline
+            pipeline = tpool.execute(self.__load_pipeline, current_project['pipeline']['model-distribution'])
 
 
-            return pipeline
+            # create job list
+            for d in current_project['data']:
+                print('keys:', d.keys())
+                if 'result' not in d.keys():
+                    # TODO update job progress
+                    job = Job('detect-faces', current_project['id'], d)
+                    result = tpool.execute(lambda p, j: p.execute(j), pipeline, job)
+                    d['result'] = result.predictions
+
+            # close pipeline
+            pipeline.close()
+    '''

+ 6 - 0
pycs/projects/MediaFile.py

@@ -0,0 +1,6 @@
+from pycs.observable import ObservableDict
+
+
+class MediaFile(ObservableDict):
+    def __init__(self, obj, parent):
+        super().__init__(obj, parent)

+ 26 - 0
pycs/projects/Project.py

@@ -0,0 +1,26 @@
+from uuid import uuid1
+
+from pycs.observable import ObservableDict
+from pycs.projects.MediaFile import MediaFile
+from pycs.util.RecursiveDictionary import set_recursive
+from os import path
+
+
+class Project(ObservableDict):
+    def __init__(self, obj: dict, parent):
+        # save data as MediaFile objects
+        for key in obj['data'].keys():
+            obj['data'][key] = MediaFile(obj['data'][key], self)
+
+        # initialize super
+        super().__init__(obj, parent)
+
+    def update_properties(self, update):
+        set_recursive(update, self)
+
+    def new_media_file_path(self):
+        return path.join('projects', self['id'], 'data'), str(uuid1())
+
+    def add_media_file(self, file):
+        file = MediaFile(file, self)
+        self['data'][file['id']] = file

+ 73 - 63
pycs/projects/ProjectManager.py

@@ -6,75 +6,85 @@ from time import time
 from uuid import uuid1
 from uuid import uuid1
 
 
 from pycs import ApplicationStatus
 from pycs import ApplicationStatus
+from pycs.observable import ObservableDict
+from pycs.pipeline.PipelineManager import PipelineManager
+from pycs.projects.Project import Project
 
 
 
 
-class ProjectManager:
+class ProjectManager(ObservableDict):
     def __init__(self, app_status: ApplicationStatus):
     def __init__(self, app_status: ApplicationStatus):
         # TODO create projects folder if it does not exist
         # TODO create projects folder if it does not exist
+        self.app_status = app_status
+
+        # initialize observable dict with no keys and
+        # app_status object as parent
+        super().__init__({}, app_status)
+        app_status['projects'] = self
 
 
         # find projects
         # find projects
         for folder in glob('projects/*'):
         for folder in glob('projects/*'):
             # load project.json
             # load project.json
             with open(path.join(folder, 'project.json'), 'r') as file:
             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 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'] = int(time())
-
-        # detect project to update
-        for i in range(len(data)):
-            if 'action' in data[i] and data[i]['action'] == 'update':
-                del data[i]['action']
-
-                prj = data[i].copy()
-                del prj['status']
-
-                with open(path.join('projects', data[i]['id'], 'project.json'), 'w') as file:
-                    dump(prj, file, indent=4)
-
-        # detect project to delete
-        for i in range(len(data)):
-            if 'action' in data[i] and data[i]['action'] == 'delete':
-                folder = path.join('projects', data[i]['id'])
-
-                del data[i]
-                rmtree(folder)
-
-                break
+                project = Project(load(file), self)
+                self[project['id']] = project
+
+    def __write_project(self, uuid):
+        with open(path.join('projects', uuid, 'project.json'), 'w') as file:
+            dump(self[uuid], file, indent=4)
+
+    def create_project(self, name, description, model):
+        # create dict representation
+        uuid = str(uuid1())
+        self[uuid] = Project({
+            'id': uuid,
+            'name': name,
+            'description': description,
+            'created': int(time()),
+            'pipeline': {
+                'model-distribution': model
+            },
+            'data': {}
+        }, self)
+
+        # create project directory
+        folder = path.join('projects', uuid)
+        mkdir(folder)
+
+        # create project.json
+        self.__write_project(uuid)
+
+    def update_project(self, uuid, update):
+        # abort if uuid is no valid key
+        if uuid not in self.keys():
+            return
+
+        # set values and write to disk
+        self[uuid].update_properties(update)
+        self.__write_project(uuid)
+
+    def delete_project(self, uuid):
+        # abort if uuid is no valid key
+        if uuid not in self.keys():
+            return
+
+        # delete project folder
+        folder = path.join('projects', uuid)
+        rmtree(folder)
+
+        # delete project data
+        del self[uuid]
+
+    def predict(self, uuid, identifiers):
+        # abort if uuid is no valid key
+        if uuid not in self.keys():
+            return
+
+        project = self[uuid]
+
+        # load pipeline
+        with PipelineManager(project) as pm:
+            # TODO add jobs to list
+            # run predictions
+            for file_id in identifiers:
+                if file_id in project['data'].keys():
+                    pm.run(project['data'][file_id])

+ 2 - 2
test/test_application_status.py

@@ -9,7 +9,7 @@ class TestApplicationStatus(unittest.TestCase):
         self.assertEqual({
         self.assertEqual({
             'settings': {},
             'settings': {},
             'models': {},
             'models': {},
-            'projects': [],
+            'projects': {},
             'jobs': []
             'jobs': []
         }, aso)
         }, aso)
 
 
@@ -23,7 +23,7 @@ class TestApplicationStatus(unittest.TestCase):
         self.assertEqual({
         self.assertEqual({
             'settings': settings,
             'settings': settings,
             'models': {},
             'models': {},
-            'projects': [],
+            'projects': {},
             'jobs': []
             'jobs': []
         }, aso)
         }, aso)
 
 

+ 4 - 1
webui/src/App.vue

@@ -103,7 +103,10 @@ export default {
   },
   },
   computed: {
   computed: {
     projects: function() {
     projects: function() {
-      return this.status == null ? [] : this.status.projects;
+      if (this.status == null)
+        return [];
+
+      return Object.keys(this.status.projects).map(key => this.status.projects[key]);
     },
     },
     currentProject: function() {
     currentProject: function() {
       for (let i = 0; i < this.projects.length; i++)
       for (let i = 0; i < this.projects.length; i++)

+ 1 - 1
webui/src/components/media/annotated-image.vue

@@ -8,7 +8,7 @@
                     :image="image"
                     :image="image"
                     :position="current"/>
                     :position="current"/>
 
 
-    <annotation-box v-for="result in data.result"
+    <annotation-box v-for="result in data.predictionResults"
                     v-bind:key="result"
                     v-bind:key="result"
                     :image="image"
                     :image="image"
                     :position="result"/>
                     :position="result"/>

+ 16 - 6
webui/src/components/media/annotated-media-view.vue

@@ -6,12 +6,14 @@
 
 
     <div class="control">
     <div class="control">
       <media-control :hasPrevious="hasPrevious" :hasNext="hasNext"
       <media-control :hasPrevious="hasPrevious" :hasNext="hasNext"
-                     @previous="previous" @next="next"/>
+                     @previous="previous" @next="next"
+                     @predict="predict"/>
     </div>
     </div>
 
 
     <div class="selector">
     <div class="selector">
-      <media-selector :project="currentProject"
-                      :current="currentMedia"
+      <media-selector :projectId="currentProject.id"
+                      :media="media"
+                      :current="current"
                       :socket="socket"
                       :socket="socket"
                       @click="current = $event"/>
                       @click="current = $event"/>
     </div>
     </div>
@@ -37,11 +39,14 @@ export default {
       return this.current > 0;
       return this.current > 0;
     },
     },
     hasNext: function() {
     hasNext: function() {
-      return this.current < this.currentProject.data.length - 1;
+      return this.current < this.media.length - 1;
+    },
+    media: function() {
+      return Object.keys(this.currentProject.data).map(key => this.currentProject.data[key]);
     },
     },
     currentMedia: function() {
     currentMedia: function() {
-      if (this.current < this.currentProject.data.length)
-        return this.currentProject.data[this.current];
+      if (this.current < this.media.length)
+        return this.media[this.current];
       else
       else
         return false;
         return false;
     }
     }
@@ -54,6 +59,11 @@ export default {
     next: function() {
     next: function() {
       if (this.hasNext)
       if (this.hasNext)
         this.current += 1;
         this.current += 1;
+    },
+    predict: function() {
+      this.socket.post('/projects/' + this.currentProject.id, {
+        'predict': [this.currentMedia.id]
+      })
     }
     }
   }
   }
 }
 }

+ 17 - 7
webui/src/components/media/media-control.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <button-row class="media-control">
+  <div class="media-control">
     <button-input type="transparent"
     <button-input type="transparent"
                   style="color: var(--on_error)"
                   style="color: var(--on_error)"
                   :class="{disabled: !hasPrevious}"
                   :class="{disabled: !hasPrevious}"
@@ -7,10 +7,17 @@
       &lt;
       &lt;
     </button-input>
     </button-input>
 
 
-    <button-input type="transparent"
-                  style="color: var(--on_error)">
-      Reset
-    </button-input>
+    <button-row class="media-control">
+      <button-input type="primary"
+                    style="color: var(--on_error)">
+        Reset
+      </button-input>
+      <button-input type="primary"
+                    style="color: var(--on_error)"
+                    @click="$emit('predict', 'current')">
+        Predict
+      </button-input>
+    </button-row>
 
 
     <button-input type="transparent"
     <button-input type="transparent"
                   style="color: var(--on_error)"
                   style="color: var(--on_error)"
@@ -18,7 +25,8 @@
                   @click="next">
                   @click="next">
       &gt;
       &gt;
     </button-input>
     </button-input>
-  </button-row>
+  </div>
+
 </template>
 </template>
 
 
 <script>
 <script>
@@ -44,7 +52,9 @@ export default {
 
 
 <style scoped>
 <style scoped>
 .media-control {
 .media-control {
-  text-align: center;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 }
 }
 
 
 .disabled {
 .disabled {

+ 11 - 24
webui/src/components/media/media-selector.vue

@@ -4,7 +4,8 @@
          v-for="(m, index) in media"
          v-for="(m, index) in media"
          v-bind:key="m.id"
          v-bind:key="m.id"
          @click="$emit('click', index)">
          @click="$emit('click', index)">
-      <img alt="media" :src="socket.media(project.id, m.id)">
+      <img alt="media" :src="socket.media(projectId, m.id)">
+      <div class="active" v-if="index === current"/>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
@@ -12,21 +13,7 @@
 <script>
 <script>
 export default {
 export default {
   name: "media-selector",
   name: "media-selector",
-  props: ['project', 'current', 'socket'],
-  computed: {
-    media: function() {
-      return this.project.data;
-    },
-    currentIndex: function() {
-      for (let i = 0; i < this.media.length; i++) {
-        if (this.current.id === this.project.data[i].id) {
-          return i;
-        }
-      }
-
-      return false;
-    }
-  }
+  props: ['projectId', 'media', 'current', 'socket']
 }
 }
 </script>
 </script>
 
 
@@ -40,6 +27,7 @@ export default {
 }
 }
 
 
 .element {
 .element {
+  position: relative;
   width: 5rem;
   width: 5rem;
   height: 5rem;
   height: 5rem;
   display: flex;
   display: flex;
@@ -55,15 +43,14 @@ export default {
   border-right: none;
   border-right: none;
 }
 }
 
 
-/*
-.element:first-child {
-  border-left: 2px solid transparent;
-}
-
-.element:last-child {
-  border-right: 2px solid transparent;
+.active {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  box-shadow: 0 0 20px -5px var(--primary) inset;
 }
 }
-*/
 
 
 img {
 img {
   max-width: 100%;
   max-width: 100%;

+ 4 - 11
webui/src/components/projects/project-creation-window.vue

@@ -23,7 +23,7 @@
       </select-input>
       </select-input>
     </div>
     </div>
 
 
-    <div class="footer">
+    <button-row class="footer">
       <button-input @click="create" type="primary">
       <button-input @click="create" type="primary">
         Create Project
         Create Project
       </button-input>
       </button-input>
@@ -31,7 +31,7 @@
       <button-input @click="$emit('cancel', null)">
       <button-input @click="$emit('cancel', null)">
         Cancel
         Cancel
       </button-input>
       </button-input>
-    </div>
+    </button-row>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -40,10 +40,11 @@ import TextInput from "@/components/base/text-input";
 import TextareaInput from "@/components/base/textarea-input";
 import TextareaInput from "@/components/base/textarea-input";
 import SelectInput from "@/components/base/select-input";
 import SelectInput from "@/components/base/select-input";
 import ButtonInput from "@/components/base/button-input";
 import ButtonInput from "@/components/base/button-input";
+import ButtonRow from "@/components/base/button-row";
 
 
 export default {
 export default {
   name: "project-creation-window",
   name: "project-creation-window",
-  components: {ButtonInput, SelectInput, TextareaInput, TextInput},
+  components: {ButtonRow, ButtonInput, SelectInput, TextareaInput, TextInput},
   props: ['status', 'socket'],
   props: ['status', 'socket'],
   data: function() {
   data: function() {
     return {
     return {
@@ -126,12 +127,4 @@ export default {
   flex-direction: row;
   flex-direction: row;
   justify-content: flex-end;
   justify-content: flex-end;
 }
 }
-
-.footer > * {
-  margin-left: 0.5rem;
-}
-
-.cancel {
-  cursor: pointer;
-}
 </style>
 </style>

+ 3 - 14
webui/src/components/projects/project-open-window.vue

@@ -19,11 +19,8 @@
 
 
           <div class="description">{{ 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>
+            {{ datetime(project.created) }}
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -55,15 +52,7 @@ export default {
       return [...this.projects].map((e, i) => {
       return [...this.projects].map((e, i) => {
         e.index = i;
         e.index = i;
         return e;
         return e;
-      }).sort((a, b) => {
-        const x = a.access === 0 ? a.created : a.access;
-        const y = b.access === 0 ? b.created : b.access;
-
-        if (x < y)
-          return 1;
-        else
-          return -1;
-      });
+      }).sort((a, b) => a.name < b.name ? -1 : 1);
     }
     }
   },
   },
   methods: {
   methods: {

+ 2 - 2
webui/src/components/projects/project-settings-window.vue

@@ -65,13 +65,13 @@ export default {
     nameF: function(value) {
     nameF: function(value) {
       // TODO then / error
       // TODO then / error
       this.socket.post(this.path, {
       this.socket.post(this.path, {
-        'name': value
+        name: value
       });
       });
     },
     },
     descriptionF: function(value) {
     descriptionF: function(value) {
       // TODO then / error
       // TODO then / error
       this.socket.post(this.path, {
       this.socket.post(this.path, {
-        'description': value
+        description: value
       });
       });
     },
     },
     deleteProject: function() {
     deleteProject: function() {

+ 4 - 1
webui/src/components/window/side-navigation-bar.vue

@@ -29,7 +29,7 @@
       </div>
       </div>
 
 
       <div class="item"
       <div class="item"
-           :class="{active: window.content === 'view_data', inactive: !currentProject}"
+           :class="{active: window.content === 'view_data', inactive: !currentProject || !mediaAvailable}"
            @click="ifProjectIsOpened(show, 'view_data')">
            @click="ifProjectIsOpened(show, 'view_data')">
         <img src="@/assets/icons/file-media.svg">
         <img src="@/assets/icons/file-media.svg">
         <span>View Data</span>
         <span>View Data</span>
@@ -68,6 +68,9 @@ export default {
         return false;
         return false;
 
 
       return this.status.settings.frontend.collapse;
       return this.status.settings.frontend.collapse;
+    },
+    mediaAvailable: function() {
+      return this.currentProject && 'data' in this.currentProject && Object.keys(this.currentProject.data).length > 0;
     }
     }
   },
   },
   methods: {
   methods: {