Kaynağa Gözat

make detail views collapsible; cleanup

move app specific stuff out of util
fix port
Martin Kunz 1 yıl önce
ebeveyn
işleme
cb213146a9

+ 2 - 5
src/App.vue

@@ -4,11 +4,9 @@ import TheDetail from './components/TheDetail.vue'
 import TheParent from './components/TheParent.vue'
 import TheEditor from './components/TheEditor.vue'
 import TheDynamics from './components/TheDynamics.vue'
-import { ServerConfig } from '@/util/sconfig'
-
-
+import { ServerConfig } from '@/ServerConfig'
 import TheModels from './components/TheModels.vue'
-import { useStore } from './util/store';
+import { useStore } from './store';
 import { AddressSpace } from './ua/AddressSpace';
 import { assert } from './util/assert';
 import { ref } from 'vue'
@@ -16,7 +14,6 @@ import { storeToRefs } from 'pinia'
 const store = useStore();
 const { selectedNode } = storeToRefs(store);
 
-
 async function load(): Promise<AddressSpace> {
     const files=['nodesets/Opc.Ua.NodeSet2.xml',
                'nodesets/Opc.Ua.Di.NodeSet2.xml',

+ 1 - 1
src/util/sconfig.ts

@@ -15,7 +15,7 @@ export class ServerConfig {
         d.applicationUri=obj.applicationUri||"";
         d.productUri=obj.productUri||"";
         d.applicationName=obj.applicationName||"";
-        d.port=obj.port||4096;
+        d.port=obj.port||4840;
         d.allowAnonymous=obj.allowAnonymous||false;
         d.maxConnections=obj.maxConnections||1000;
         d.dynamics=obj.dynamics||[];

+ 11 - 0
src/assets/arrow-down.svg

@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="10.031" height="5.969" viewBox="0 0 10.031 5.969">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #c5c9d0;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <path id="Rounded_Rectangle_7_copy" data-name="Rounded Rectangle 7 copy" class="cls-1" d="M914.7,242.832l-3.836,3.836a1.407,1.407,0,0,1-1.732,0l-3.836-3.836a1.085,1.085,0,1,1,1.535-1.534L910,244.466l3.167-3.168A1.085,1.085,0,1,1,914.7,242.832Z" transform="translate(-905 -241)"/>
+</svg>

+ 2 - 2
src/components/TheContextMenu.vue

@@ -2,7 +2,7 @@
 import { coerceNodeId } from '@/ua/NodeId';
 import { UABaseNode } from '@/ua/UABaseNode';
 import { assert } from '@/util/assert';
-import { useStore } from '@/util/store';
+import { useStore } from '@/store';
 import { ref, reactive, onMounted, onUnmounted, computed, type CSSProperties } from 'vue';
 
 const isVisible = ref(false);
@@ -63,4 +63,4 @@ onUnmounted(() => {
             <button @click="newInstance">new instance</button>
         </div>
     </div>
-</template>
+</template>@/store

+ 30 - 34
src/components/TheDetail.vue

@@ -1,11 +1,10 @@
 <script setup lang="ts">
-import { useStore } from '@/util/store'
+import { useStore } from '@/store'
 import { storeToRefs } from 'pinia';
 import { computed } from 'vue';
+import VCollaps from './VCollaps.vue';
 const store = useStore()
-
 const { selectedNode } = storeToRefs(store)
-
 const nameSpaceName = computed(() => {
   if(!selectedNode.value)
     return "";
@@ -20,46 +19,43 @@ defineExpose({ okPressed })
 </script>
 
 <template>
-    <div class="card" v-if="selectedNode!=null">
-    <div class="card-body" >
-      <h5 class="card-title">Node Editor</h5>
-      <div class="card-text" >
-        <div class="input-group mb-3">
-          <div class="input-group-prepend">
-            <span class="input-group-text" id="inputGroup-sizing-default">DisplayName</span>
-          </div>
-          <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.displayName">
+  <VCollaps :selected=true name="Node">
+    <div class="card-text" v-if="selectedNode">
+      <div class="input-group mb-3">
+        <div class="input-group-prepend">
+          <span class="input-group-text" id="inputGroup-sizing-default">DisplayName</span>
         </div>
-        <div class="input-group mb-3">
-          <div class="input-group-prepend">
-            <span class="input-group-text" id="inputGroup-sizing-default">BrowseName</span>
-          </div>
-          <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.browseName">
-        </div>
-        <div class="input-group mb-3">
-          <div class="input-group-prepend">
-            <span class="input-group-text" id="inputGroup-sizing-default">NodeId</span>
-          </div>
-          <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.nodeId">
+        <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.displayName">
+      </div>
+      <div class="input-group mb-3">
+        <div class="input-group-prepend">
+          <span class="input-group-text" id="inputGroup-sizing-default">BrowseName</span>
         </div>
-        <div class="input-group mb-3">
-          <div class="input-group-prepend">
-            <span class="input-group-text" id="inputGroup-sizing-default">NodeClass</span>
-          </div>
-          <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.nodeClass">
+        <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.browseName">
+      </div>
+      <div class="input-group mb-3">
+        <div class="input-group-prepend">
+          <span class="input-group-text" id="inputGroup-sizing-default">NodeId</span>
         </div>
-        <div class="input-group mb-3">
-          <div class="input-group-prepend">
-            <span class="input-group-text" id="inputGroup-sizing-default">Namespace</span>
-          </div>
-          <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="nameSpaceName">
+        <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.nodeId">
+      </div>
+      <div class="input-group mb-3">
+        <div class="input-group-prepend">
+          <span class="input-group-text" id="inputGroup-sizing-default">NodeClass</span>
         </div>
+        <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.nodeClass">
       </div>
+      <div class="input-group mb-3">
+        <div class="input-group-prepend">
+          <span class="input-group-text" id="inputGroup-sizing-default">Namespace</span>
+        </div>
+        <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="nameSpaceName">
       </div>
     </div>
+  </VCollaps>
 </template>
 
 
 <style scoped>
 
-</style>
+</style>@/store

+ 2 - 2
src/components/TheDynamics.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { useStore } from '@/util/store'
+import { useStore } from '@/store'
 import { computed, ref, watch } from 'vue';
 import { UABaseNode } from '@/ua/UABaseNode';
 import { ObjectIds, ReferenceTypeIds } from '@/ua/opcua_node_ids';
@@ -215,4 +215,4 @@ defineExpose({ okPressed })
   margin: 0;
   padding: 0;
 }
-</style>
+</style>@/store

+ 12 - 14
src/components/TheEditor.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
-import { useStore } from '@/util/store'
+import { useStore } from '@/store'
 const store = useStore()
 import { computed, shallowRef } from 'vue'
+import VCollaps from './VCollaps.vue';
 
 //see https://github.com/imguolao/monaco-vue#vite
 import { loader } from "@guolao/vue-monaco-editor"
@@ -12,6 +13,7 @@ import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"
 import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"
 import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"
 import type { IMappingValue } from '@/ua/IAddressSpace';
+import { storeToRefs } from 'pinia';
 self.MonacoEnvironment = {
   getWorker(_, label) {
     if (label === "json") {
@@ -36,16 +38,15 @@ const MONACO_EDITOR_OPTIONS = {
   formatOnPaste: true,
 }
 
-const node = computed(() => {
-    return store.selectedNode;
-});
+const { selectedNode } = storeToRefs(store)
+
 
 const code = computed(():string => {
-    if(!node.value)
+    if(!selectedNode.value)
       return "";
-    if(!node.value.nodeId.value)
+    if(!selectedNode.value.nodeId.value)
       return "";
-    const m=store.addressSpace?.mapping.get(node.value.nodeId.toString());
+    const m=store.addressSpace?.mapping.get(selectedNode.value.nodeId.toString());
     if(m)
       return m.read;
     return "";
@@ -73,10 +74,8 @@ defineExpose({ okPressed })
 </script>
 
 <template>
-  <div class="card">
-    <div class="card-body" v-if="store.selectedNode != null" :elem="store.selectedNode">
-      <h5 class="card-title">Mapping Editor</h5>
-      <div class="card-text">
+  <VCollaps :selected=false name="Mapping">
+      <div class="card-text" v-if="selectedNode">
         <vue-monaco-editor
           v-model:value="code"
           height= "12em"
@@ -87,8 +86,7 @@ defineExpose({ okPressed })
           @change="onChange"
         />
       </div>
-    </div>
-  </div>
+  </VCollaps>
 </template>
 
-<style scoped></style>
+<style scoped></style>@/store

+ 3 - 3
src/components/TheModeler.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import type { UABaseNode } from '@/ua/UABaseNode';
 import TreeItem from './TreeItem.vue'
-import {useStore} from '@/util/store'
+import {useStore} from '@/store'
 import TheContextMenu from './TheContextMenu.vue';
 const store = useStore()
 
@@ -12,7 +12,7 @@ function selectNode(node: UABaseNode) {
 
 <template>
   <div class="card">
-  <div class="card-body" v-if="store.addressSpace" style="position: relative;">
+  <div class="card-body" v-if="store.addressSpace">
     <h5 class="card-title">Addressspace</h5>
     <p class="card-text">
       <ul>
@@ -36,4 +36,4 @@ function selectNode(node: UABaseNode) {
   #ul {
     margin-left: 10px;
   }
-</style>
+</style>@/store

+ 9 - 10
src/components/TheModels.vue

@@ -2,7 +2,7 @@
 <script setup lang="ts">
 import { AddressSpace } from '@/ua/AddressSpace';
 import { UANodeSet } from '@/ua/UANodeSet';
-import { useStore } from '@/util/store'
+import { useStore } from '@/store'
 import { ref } from 'vue';
 import VDialog from './VDialog.vue'
 import JSZip from "jszip";
@@ -10,7 +10,7 @@ import YAML from 'yaml'
 import { assert } from '@/util/assert';
 import { storeToRefs } from 'pinia';
 import type { IMappingValue } from '@/ua/IAddressSpace';
-import { ServerConfig } from '@/util/sconfig';
+import { ServerConfig } from '@/ServerConfig';
 
 const store = useStore()
 const newDialogOpen = ref(false);
@@ -107,13 +107,12 @@ async function  loadZip(file: File){
     <div class="card">
       <div class="card-body" v-if="store.addressSpace">
         <div class="d-flex justify-content-between align-items-center mb-3">
-
-        <h5 class="card-title d-inline">Models</h5>
-        <div class="btn-group">
-          <button class="btn btn-light" @click.prevent="exportProject()">Export</button>
-          <button class="btn btn-light" @click="newDialogOpen = true">New project</button>
-          <button class="btn btn-light" @click="newServerConfigOpen = true">Server Config</button>
-        </div>
+          <h5 class="card-title d-inline">Models</h5>
+          <div class="btn-group">
+            <button class="btn btn-light" @click.prevent="exportProject()">Export</button>
+            <button class="btn btn-light" @click="newDialogOpen = true">New project</button>
+            <button class="btn btn-light" @click="newServerConfigOpen = true">Server Config</button>
+          </div>
         </div>
         <p class="card-text">
         <ul class="list-group list-group-flush" v-for="nodeset in store.addressSpace.nodesets"
@@ -225,4 +224,4 @@ async function  loadZip(file: File){
 </template>
 
 
-<style></style>
+<style></style>@/util/ServerConfig@/store

+ 28 - 31
src/components/TheParent.vue

@@ -1,16 +1,16 @@
 <script setup lang="ts">
-import { useStore } from '@/util/store'
-import { computed, ref, watch } from 'vue';
+import { useStore } from '@/store'
+import { ref, watch } from 'vue';
 import TheTreeDialog from './TheTreeDialog.vue';
+import VCollaps from './VCollaps.vue';
 import { UABaseNode } from '@/ua/UABaseNode';
 import { ReferenceTypeIds } from '@/ua/opcua_node_ids';
 import type { UAReferenceType } from '@/ua/UAReferenceType';
+import { storeToRefs } from 'pinia';
 const store = useStore()
 
 const parentDialogOpen = ref(false);
-const node = computed(() => {
-  return store.selectedNode;
-});
+const { selectedNode } = storeToRefs(store)
 const selectedRefType = ref('')
 const selectedParent = ref(UABaseNode.nullBaseNode);
 
@@ -19,7 +19,7 @@ function clickNode(clickedNode: UABaseNode) {
   parentDialogOpen.value=false;
 }
 
-watch(node, async (newNode, _oldNode) => {
+watch(selectedNode, async (newNode, _oldNode) => {
   selectedRefType.value=newNode?.getParentRef()?.referenceType||"";
   selectedParent.value=newNode?.getParent()||UABaseNode.nullBaseNode;
 
@@ -27,12 +27,12 @@ watch(node, async (newNode, _oldNode) => {
 
 function okPressed() {
   //TODO: check if valid
-  node.value?.setParent(selectedParent.value, selectedRefType.value);
+  selectedNode.value?.setParent(selectedParent.value, selectedRefType.value);
 }
 defineExpose({ okPressed })
 
 function filter(fnode: UABaseNode) {
-  if(node.value?.nodeClass==="Variable") {
+  if(selectedNode.value?.nodeClass==="Variable") {
     if(fnode.nodeClass!="Object")
       return true;
   }
@@ -46,32 +46,29 @@ function getRefTypes():UABaseNode[] {
 </script>
 
 <template>
-  <div class="card">
-    <div class="card-body" v-if="node != null">
-      <h5 class="card-title">Parent</h5>
-      <div class="card-text">
-        <div class="input-group mb-3">
-          <div class="input-group-prepend">
-            <span class="input-group-text" id="inputGroup-sizing-default">Name</span>
-          </div>
-          <input readonly type="text" class="form-control" aria-label="Default"
-            aria-describedby="inputGroup-sizing-default" :value="selectedParent.displayName">
-            <button class="btn btn-light" @click="parentDialogOpen = true">...</button>
-
+  <VCollaps :selected=false name="Parent">
+    <div class="card-text"  v-if="selectedNode">
+      <div class="input-group mb-3">
+        <div class="input-group-prepend">
+          <span class="input-group-text" id="inputGroup-sizing-default">Name</span>
         </div>
-        <div class="input-group mb-3">
-          <div class="input-group-prepend">
-            <span class="input-group-text" id="inputGroup-sizing-default">Reference</span>
-          </div>
-          <select class="form-select" aria-label="Selected ref. type" v-model="selectedRefType" >
-            <option v-for="option in getRefTypes()" :value="option.browseName" v-bind:key="option.browseName">
-              {{ option.displayName }}
-            </option>
-          </select>
+        <input readonly type="text" class="form-control" aria-label="Default"
+          aria-describedby="inputGroup-sizing-default" :value="selectedParent.displayName">
+          <button class="btn btn-light" @click="parentDialogOpen = true">...</button>
+
+      </div>
+      <div class="input-group mb-3">
+        <div class="input-group-prepend">
+          <span class="input-group-text" id="inputGroup-sizing-default">Reference</span>
         </div>
+        <select class="form-select" aria-label="Selected ref. type" v-model="selectedRefType" >
+          <option v-for="option in getRefTypes()" :value="option.browseName" v-bind:key="option.browseName">
+            {{ option.displayName }}
+          </option>
+        </select>
       </div>
     </div>
-  </div>
+  </VCollaps>
   <TheTreeDialog 
     :open="parentDialogOpen" 
     :filter-func="(node: UABaseNode) => filter(node)"
@@ -81,4 +78,4 @@ function getRefTypes():UABaseNode[] {
 </template>
 
 
-<style scoped></style>
+<style scoped></style>@/store

+ 2 - 2
src/components/TheTreeDialog.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { useStore } from '@/util/store'
+import { useStore } from '@/store'
 import { ref } from 'vue';
 import VDialog from './VDialog.vue'
 import TreeItem from './TreeItem.vue'
@@ -42,4 +42,4 @@ defineProps({
   width: 30em;
   height: 40em;
 }
-</style>
+</style>@/store

+ 62 - 0
src/components/VCollaps.vue

@@ -0,0 +1,62 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+
+const active=ref(false);
+const props=defineProps({
+  selected: Boolean,
+  name: String
+});
+const emit = defineEmits(['collapse-open']);
+
+onMounted(() => {
+    active.value = props.selected
+    if (active.value) {
+      emit('collapse-open')
+    }
+});
+
+function toggle () {
+    active.value=!active.value;
+    if (active.value) {
+        emit('collapse-open')
+    }
+}
+</script>
+
+<template>
+  <div class="card" >
+    <div class="card-body">
+      <div class="d-flex justify-content-between align-items-center" @click.prevent="toggle">
+        <h5 class="card-title">{{ props.name }}</h5>
+        <div class="collapse-header" :class="{ 'is-active': active }"></div>
+      </div>
+      <Transition name="fade">
+        <div class="collapse-content" v-if="active">
+          <div class="collapse-content-box">
+            <slot></slot>
+          </div>
+        </div>
+      </Transition>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+
+.card-title {
+    display: flex;
+}
+.collapse-header {
+    display: flex;
+    content: url('../assets/arrow-down.svg');
+    transform: rotate(-90deg);
+}
+
+.is-active  {
+    transform: rotate(0deg);
+}
+
+
+
+
+</style>

+ 1 - 1
src/util/store.ts

@@ -3,7 +3,7 @@ import { type UABaseNode } from '@/ua/UABaseNode'
 import type { UANodeSet } from '@/ua/UANodeSet'
 import { defineStore } from 'pinia'
 import { ref } from 'vue'
-import { ServerConfig } from './sconfig'
+import { ServerConfig } from './ServerConfig'
 import type { IAddressSpace } from '@/ua/IAddressSpace'
 
 export const useStore = defineStore('user', {