MULTICLOUD-1257 updated gui flow 95/116195/1
authorvikaskumar <vkumar@aarnanetworks.com>
Thu, 26 Nov 2020 07:41:45 +0000 (13:11 +0530)
committervikaskumar <vkumar@aarnanetworks.com>
Fri, 27 Nov 2020 07:16:26 +0000 (12:46 +0530)
Issue-ID: MULTICLOUD-1257
Change-Id: I5c1432c037952abeed6066cb067192076031f9cd
Signed-off-by: vikaskumar <vkumar@aarnanetworks.com>
64 files changed:
src/tools/emcoui/Dockerfile
src/tools/emcoui/README.md
src/tools/emcoui/default.conf [new file with mode: 0644]
src/tools/emcoui/helm/emcoui/templates/configmap.yaml
src/tools/emcoui/helm/emcoui/templates/deployment.yaml
src/tools/emcoui/helm/emcoui/templates/service.yaml
src/tools/emcoui/helm/emcoui/values.yaml
src/tools/emcoui/middle_end/Dockerfile [new file with mode: 0644]
src/tools/emcoui/middle_end/Makefile [new file with mode: 0644]
src/tools/emcoui/middle_end/app/app.go [new file with mode: 0644]
src/tools/emcoui/middle_end/app/compositeapp.go [new file with mode: 0644]
src/tools/emcoui/middle_end/app/digp.go [new file with mode: 0644]
src/tools/emcoui/middle_end/app/intents.go [new file with mode: 0644]
src/tools/emcoui/middle_end/app/profile.go [new file with mode: 0644]
src/tools/emcoui/middle_end/app/projects.go [new file with mode: 0644]
src/tools/emcoui/middle_end/authproxy/README.md [new file with mode: 0644]
src/tools/emcoui/middle_end/authproxy/authproxy.go [new file with mode: 0644]
src/tools/emcoui/middle_end/db/dbconnection.go [new file with mode: 0644]
src/tools/emcoui/middle_end/go.mod [new file with mode: 0644]
src/tools/emcoui/middle_end/go.sum [new file with mode: 0644]
src/tools/emcoui/middle_end/main/main.go [new file with mode: 0644]
src/tools/emcoui/public/robots.txt
src/tools/emcoui/src/App.js
src/tools/emcoui/src/admin/AdminNavigator.js
src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx
src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx
src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx
src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx
src/tools/emcoui/src/admin/controllers/Controllers.jsx
src/tools/emcoui/src/admin/projects/ProjectForm.jsx
src/tools/emcoui/src/admin/projects/ProjectsTable.jsx
src/tools/emcoui/src/appbase/AppBase.js
src/tools/emcoui/src/appbase/Content.js
src/tools/emcoui/src/appbase/Header.js
src/tools/emcoui/src/appbase/Navigator.js
src/tools/emcoui/src/assets/icons/empty.svg [new file with mode: 0644]
src/tools/emcoui/src/common/ExpandableCard.jsx [new file with mode: 0644]
src/tools/emcoui/src/common/FileUpload.jsx
src/tools/emcoui/src/common/Form.jsx
src/tools/emcoui/src/compositeApps/CompositeApp.jsx
src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx
src/tools/emcoui/src/compositeApps/CompositeApps.jsx
src/tools/emcoui/src/compositeApps/apps/Apps.jsx
src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx
src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx
src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx [new file with mode: 0644]
src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx [new file with mode: 0644]
src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx [new file with mode: 0644]
src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx [new file with mode: 0644]
src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx
src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx [new file with mode: 0644]
src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx
src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx
src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx
src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx
src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx
src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx [new file with mode: 0644]
src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx [new file with mode: 0644]
src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx [new file with mode: 0644]
src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx [new file with mode: 0644]
src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx
src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx
src/tools/emcoui/src/services/apiService.js
src/tools/emcoui/startup.sh

index 6f0cc2f..8224d92 100644 (file)
@@ -1,3 +1,18 @@
+#=======================================================================
+# Copyright (c) 2017-2020 Aarna Networks, Inc.
+# All rights reserved.
+# ======================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#           http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================================================================
+
 # => Build container
 FROM node:alpine as builder
 WORKDIR /app
@@ -7,11 +22,14 @@ RUN npm install
 COPY src ./src
 COPY public ./public
 # => Pass the reuired version
-RUN REACT_APP_VERSION=v1.0.0 npm run build
+RUN REACT_APP_VERSION=v1.2.0 npm run build
 
 # => Run container
 FROM nginx:1.15.2-alpine
 
+# Nginx config
+COPY default.conf /etc/nginx/conf.d/
+
 # Static build
 COPY --from=builder /app/build /usr/share/nginx/html/
 
index 05a4323..945dde0 100644 (file)
@@ -1,30 +1,11 @@
-# EMCOUI
-
-This is a web app for EMCO V2 api's. This is a reactjs based UI app created using google material ui library.
-
 ## Local setup
 
-for running the app in a local setup first install the dependencies by running
-
-```bash
-npm install
-```
-
-Then run
-
-```bash
-startup.sh
-```
+for running the app in a local setup first install the dependencies by running `npm install`.
+Then run `startup.sh`
 
 ## Production build
 
-for creating a production build, run
-
-```bash
-npm run build
-```
-
-A production ready build will be available at /build directory
+for creating a production build, run `npm run build`. A production ready build will be available at /build directory
 
 ## Available scripts
 
@@ -48,17 +29,4 @@ The build is minified and the filenames include the hashes.<br />
 ## Building docker image
 
 To build a docker image run the below command
-
-```bash
-docker build -t <image_name>:<image_version> .
-```
-
-## Installing with helm
-
-All the helm chars are in `helm` directory.
-
-Update values.yaml in `helm` directory with the required emcoui image and then run
-
-```bash
-helm install --name <app name> --namespace < namespace >
-```
+`docker build -t image_name:version .`
diff --git a/src/tools/emcoui/default.conf b/src/tools/emcoui/default.conf
new file mode 100644 (file)
index 0000000..4387a61
--- /dev/null
@@ -0,0 +1,34 @@
+server {
+    listen       9080;
+    server_name  localhost;
+    location / {
+        root   /usr/share/nginx/html;
+        index  index.html;
+        try_files $uri $uri/ /index.html;
+    }
+    error_page   500 502 503 504  /50x.html;
+    location = /50x.html {
+        root   /usr/share/nginx/html;
+    }
+
+    location /middleend {
+      proxy_pass  http://middleend.onap4k8s.svc.cluster.local:9081;
+    }
+    location /v2/controllers {
+      proxy_pass  http://orchestrator.onap4k8s.svc.cluster.local:9015;
+    }
+    location /v2/projects {
+      proxy_pass  http://orchestrator.onap4k8s.svc.cluster.local:9015;
+    }
+    location /v2/cluster-providers {
+      proxy_pass  http://clm.onap4k8s.svc.cluster.local:9061;
+    }
+    location /v2/ovnaction {
+      rewrite ^/v2/ovnaction/(.*) /v2/projects/$1  break;
+      proxy_pass  http://ovnaction.onap4k8s.svc.cluster.local:9051;
+    }
+    location /v2/ncm {
+      rewrite ^/v2/ncm/(.*) /v2/cluster-providers/$1  break;
+      proxy_pass  http://ncm.onap4k8s.svc.cluster.local:9031;
+    }
+}
index a9ba34a..b62f35d 100644 (file)
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-# ========================================================================  
+# ========================================================================
+# middleend config
+apiVersion: v1
+kind: ConfigMap
+metadata:
+        name: middleend-config
+data:
+  middleend.conf: |-
+    {
+      "ownport": "{{ .Values.middleend.service.internalPort }}",
+      "orchestrator": "orchestrator.{{ .Values.namespace }}.svc.cluster.local:9015",
+      "clm": "clm.{{ .Values.namespace }}.svc.cluster.local:9061",
+      "ovnaction": "ovnaction.{{ .Values.namespace }}.svc.cluster.local:9051",
+      "issuer": "{{ .Values.authproxy.issuer }}",
+      "redirect_uri": "{{ .Values.authproxy.redirect_uri }}",
+      "client_id": "{{ .Values.authproxy.client_id }}",
+      "mongo": "mongo.{{ .Values.namespace }}.svc.cluster.local:27017"
+    }   
+
+---
+# emcoui config
 apiVersion: v1
 kind: ConfigMap
 metadata:
@@ -19,7 +39,7 @@ metadata:
 data:
   my-nginx-config.conf: |
     server {
-      listen       {{ .Values.service.internalPort }};
+      listen       {{ .Values.emcoui.service.internalPort }};
       server_name  localhost;
       location / {
           root   /usr/share/nginx/html;
@@ -30,6 +50,9 @@ data:
       location = /50x.html {
           root   /usr/share/nginx/html;
       }
+      location /middleend {
+          proxy_pass   http://middleend.{{ .Values.namespace }}.svc.cluster.local:9081;
+      }
       location /v2/controllers {
           proxy_pass   http://orchestrator.{{ .Values.namespace }}.svc.cluster.local:9015;
       }
index 11ab6f5..f1609cd 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ========================================================================  
+# middleend Deployment
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Values.middleend.service.name }} 
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Values.middleend.service.label }} 
+  template:
+    metadata:
+      labels:
+        app: {{ .Values.middleend.service.label }} 
+    spec:
+      containers:
+        - name: {{ .Values.middleend.service.name }} 
+          image: "{{ .Values.middleend.image.repository }}:{{ .Values.middleend.image.tag }}"
+          imagePullPolicy: Always
+          ports:
+          - containerPort: {{ .Values.middleend.service.internalPort }} 
+          volumeMounts:
+          - mountPath: /opt/emco/config
+            readOnly: true
+            name: config 
+      volumes:
+      - name: config 
+        configMap:
+          name: middleend-config
+
+---
 # GUI Deployment
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: {{ .Values.service.name }} 
+  name: {{ .Values.emcoui.service.name }} 
 spec:
   replicas: 1
   selector:
     matchLabels:
-      app: {{ .Values.service.label }} 
+      app: {{ .Values.emcoui.service.label }} 
   template:
     metadata:
       labels:
-        app: {{ .Values.service.label }} 
+        app: {{ .Values.emcoui.service.label }} 
     spec:
       containers:
-        - name: {{ .Values.service.name }} 
-          image: {{ .Values.image }} 
+        - name: {{ .Values.emcoui.service.name }} 
+          image: "{{ .Values.emcoui.image.repository }}:{{ .Values.emcoui.image.tag }}"
           imagePullPolicy: Always
           ports:
-          - containerPort: {{ .Values.service.internalPort }} 
+          - containerPort: {{ .Values.emcoui.service.internalPort }} 
           volumeMounts:
           - mountPath: /etc/nginx/conf.d 
             readOnly: true
index 2c09a7d..9c88b8f 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ========================================================================  
-# GUI Service
+# middleend Service
 apiVersion: v1
 kind: Service
 metadata:
-  name: {{ .Values.service.name}} 
+  name: {{ .Values.middleend.service.name}}
   labels:
-    app: {{ .Values.service.label }} 
+    app: {{ .Values.middleend.service.label }}
 spec:
   selector:
-    app: {{ .Values.service.name }} 
-  type: {{ .Values.service.type }} 
+    app: {{ .Values.middleend.service.name }} 
+  type: {{ .Values.middleend.service.type }} 
   ports:
-  - name: {{ .Values.service.PortName }}
-    {{if eq .Values.service.type "NodePort" -}}
-    port: {{ .Values.service.internalPort }}
-    nodePort: {{ .Values.global.nodePortPrefixExt | default "302" }}{{ .Values.service.nodePort }}
+  - name: {{ .Values.middleend.service.PortName }}
+    {{if eq .Values.middleend.service.type "NodePort" -}}
+    port: {{ .Values.middleend.service.internalPort }}
+    nodePort: {{ .Values.global.nodePortPrefixExt | default "302" }}{{ .Values.middleend.service.nodePort }}
     {{- else -}}
-    port: {{ .Values.service.externalPort }}
-    targetPort: {{ .Values.service.internalPort }}
+    port: {{ .Values.middleend.externalPort }}
+    targetPort: {{ .Values.middleend.internalPort }}
+    {{- end}}
+    protocol: TCP
+
+
+---
+# emcoui service
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Values.emcoui.service.name}} 
+  labels:
+    app: {{ .Values.emcoui.service.label }} 
+spec:
+  selector:
+    app: {{ .Values.emcoui.service.name }} 
+  type: {{ .Values.emcoui.service.type }} 
+  ports:
+  - name: {{ .Values.emcoui.service.PortName }}
+    {{if eq .Values.emcoui.service.type "NodePort" -}}
+    port: {{ .Values.emcoui.service.internalPort }}
+    nodePort: {{ .Values.global.nodePortPrefixExt | default "302" }}{{ .Values.emcoui.service.nodePort }}
+    {{- else -}}
+    port: {{ .Values.emcoui.service.externalPort }}
+    targetPort: {{ .Values.emcoui.service.internalPort }}
     {{- end}}
     protocol: TCP
index f52c071..d764f72 100644 (file)
@@ -11,7 +11,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-# ========================================================================
+# ========================================================================  
 
 global:
   nodePortPrefixExt: 304
@@ -19,11 +19,6 @@ global:
 #################################################################
 # Application configuration defaults.
 #################################################################
-# application image
-repository: registry.hub.docker.com
-image: emcov2/emcoui:stable
-pullPolicy: Always
-
 # default number of instances
 replicaCount: 1
 
@@ -44,14 +39,43 @@ readiness:
   initialDelaySeconds: 10
   periodSeconds: 30
 
-service:
-  type: NodePort
-  name: emcoui
-  portName: emcoui
-  internalPort: 9080
-  externalPort: 9080
-  nodePort: 80
-  label: emcoui
+middleend:
+  service:
+    type: NodePort
+    name: middleend 
+    portName: middleend 
+    internalPort: 9081
+    externalPort: 9081
+    nodePort: 81
+    label: middleend
+
+  image:
+    registry: registry.hub.docker.com
+    repository: amcop/middleend
+    tag: master
+    pullPolicy: Always
+
+emcoui:
+  service:
+    type: NodePort
+    name: emcoui
+    portName: emcoui
+    internalPort: 9080
+    externalPort: 9080
+    nodePort: 80
+    label: emcoui
+
+  image:
+    registry: registry.hub.docker.com
+    repository: amcop/emcoui
+    tag: master
+    pullPolicy: Always
+
+authproxy:
+  # These values should be updated at the time of deployment
+  issuer: http://192.168.122.224:31064/auth/realms/EMCO/
+  redirect_uri: http://192.168.122.224:30481/middleend/callback
+  client_id: emcoapp
 
 ingress:
   enabled: false
diff --git a/src/tools/emcoui/middle_end/Dockerfile b/src/tools/emcoui/middle_end/Dockerfile
new file mode 100644 (file)
index 0000000..4e35322
--- /dev/null
@@ -0,0 +1,32 @@
+#=======================================================================
+# Copyright (c) 2017-2020 Aarna Networks, Inc.
+# All rights reserved.
+# ======================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#           http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================================================================
+
+FROM golang:1.14.1
+
+# Set the Current Working Directory inside the container
+WORKDIR /src
+COPY ./ ./
+RUN make all 
+
+# Build the Go app
+FROM ubuntu:16.04
+WORKDIR /opt/emco
+RUN groupadd -r emco && useradd -r -g emco emco
+RUN chown emco:emco /opt/emco -R
+RUN mkdir ./config
+COPY --chown=emco --from=0 /src/middleend ./
+
+# Command to run the executable
+CMD ["./middleend"]
diff --git a/src/tools/emcoui/middle_end/Makefile b/src/tools/emcoui/middle_end/Makefile
new file mode 100644 (file)
index 0000000..a44fc78
--- /dev/null
@@ -0,0 +1,24 @@
+#=======================================================================
+# Copyright (c) 2017-2020 Aarna Networks, Inc.
+# All rights reserved.
+# ======================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#           http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================================================================
+
+export GO111MODULE=on
+
+all: clean
+       CGO_ENABLED=1 GOOS=linux GOARCH=amd64
+       @go build -tags netgo -o ./middleend ./main/main.go 
+
+clean:
+       @find . -name "*so" -delete
+       @rm -f middleend 
diff --git a/src/tools/emcoui/middle_end/app/app.go b/src/tools/emcoui/middle_end/app/app.go
new file mode 100644 (file)
index 0000000..a851169
--- /dev/null
@@ -0,0 +1,1014 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package app
+
+import (
+       "bytes"
+       "context"
+       "encoding/json"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "log"
+       "mime/multipart"
+       "net/http"
+
+       "github.com/gorilla/mux"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/client-go/kubernetes"
+       "k8s.io/client-go/tools/clientcmd"
+)
+
+type deployServiceData struct {
+       Name        string `json:"name"`
+       Description string `json:"description"`
+       Spec        struct {
+               ProjectName string     `json:"projectName"`
+               Apps        []appsData `json:"appsData"`
+       } `json:"spec"`
+}
+
+type deployDigData struct {
+       Name                string `json:"name"`
+       Description         string `json:"description"`
+       CompositeAppName    string `json:"compositeApp"`
+       CompositeProfile    string `json:"compositeProfile"`
+       DigVersion          string `json:"version"`
+       CompositeAppVersion string `json:"compositeAppVersion"`
+       Spec                struct {
+               ProjectName string     `json:"projectName"`
+               Apps        []appsData `json:"appsData"`
+       } `json:"spec"`
+}
+
+// Exists is for mongo $exists filter
+type Exists struct {
+       Exists string `json:"$exists"`
+}
+
+// This is the json payload that the  orchesration API expexts.
+type appsData struct {
+       Metadata struct {
+               Name        string `json:"name"`
+               Description string `json:"description"`
+               FileName    string `json:"filename"`
+       } `json:"metadata"`
+       ProfileMetadata struct {
+               Name     string `json:"name"`
+               FileName string `json:"filename"`
+       } `json:"profileMetadata"`
+       Clusters []struct {
+               Provider         string `json:"provider"`
+               SelectedClusters []struct {
+                       Name       string `json:"name"`
+                       Interfaces []struct {
+                               NetworkName string `json:"networkName"`
+                               IP          string `json:"ip"`
+                               Subnet      string `json:"subnet"`
+                       } `json:"interfaces"`
+               } `json:"selectedClusters"`
+       } `json:"clusters"`
+}
+
+type DigsInProject struct {
+       Metadata struct {
+               Name                string `json:"name"`
+               CompositeAppName    string `json:"compositeAppName"`
+               CompositeAppVersion string `json:"compositeAppVersion"`
+               Description         string `json:"description"`
+               UserData1           string `userData1:"userData1"`
+               UserData2           string `userData2:"userData2"`
+       } `json:"metadata"`
+       Spec struct {
+               DigIntentsData    []DigDeployedIntents `json:"deployedIntents"`
+               Profile           string               `json:"profile"`
+               Version           string               `json:"version"`
+               Lcloud            string               `json:"logicalCloud"`
+               OverrideValuesObj []OverrideValues     `json:"overrideValues"`
+               GpintArray        []*DigsGpint         `json:"GenericPlacementIntents,omitempty"`
+               NwintArray        []*DigsNwint         `json:"networkCtlIntents,omitempty"`
+       } `json:"spec"`
+}
+
+type DigsGpint struct {
+       Metadata apiMetaData `json:"metadata,omitempty"`
+       Spec     struct {
+               AppIntentArray []PlacementIntentExport `json:"placementIntent,omitempty"`
+       } `json:"spec,omitempty"`
+}
+
+type DigsNwint struct {
+       Metadata apiMetaData `json:"metadata,omitempty"`
+       Spec     struct {
+               WorkloadIntentsArray []*WorkloadIntents `json:"WorkloadIntents,omitempty"`
+       } `json:"spec,omitempty"`
+}
+type WorkloadIntents struct {
+       Metadata apiMetaData `json:"metadata,omitempty"`
+       Spec     struct {
+               Interfaces []NwInterface `json:"interfaces,omitempty"`
+       } `json:"spec,omitempty"`
+}
+
+// Project Tree
+type ProjectTree struct {
+       Metadata        ProjectMetadata
+       compositeAppMap map[string]*CompositeAppTree
+}
+
+type treeTraverseFilter struct {
+       compositeAppName    string
+       compositeAppVersion string
+       digName             string
+}
+
+// Composite app tree
+type CompositeAppTree struct {
+       Metadata         CompositeApp
+       AppsDataArray    map[string]*AppsData
+       ProfileDataArray map[string]*ProfilesData
+       DigMap           map[string]*DigReadData
+}
+
+type DigReadData struct {
+       DigpData       DeploymentIGP
+       DigIntentsData DigpIntents
+       GpintMap       map[string]*GpintData
+       NwintMap       map[string]*NwintData
+}
+
+type GpintData struct {
+       Gpint          GenericPlacementIntent
+       AppIntentArray []PlacementIntent
+}
+
+type NwintData struct {
+       Nwint     NetworkCtlIntent
+       WrkintMap map[string]*WrkintData
+}
+
+type WrkintData struct {
+       Wrkint     NetworkWlIntent
+       Interfaces []NwInterface
+}
+
+type AppsData struct {
+       App              CompositeApp
+       CompositeProfile ProfileMeta
+}
+
+type ProfilesData struct {
+       Profile     ProfileMeta
+       AppProfiles []ProfileMeta
+}
+
+type ClusterMetadata struct {
+       Metadata apiMetaData `json:"Metadata"`
+}
+
+type apiMetaData struct {
+       Name        string `json:"name"`
+       Description string `json:"description"`
+       UserData1   string `userData1:"userData1"`
+       UserData2   string `userData2:"userData2"`
+}
+
+// The interface
+type orchWorkflow interface {
+       createAnchor() interface{}
+       createObject() interface{}
+       getObject() (interface{}, interface{})
+       getAnchor() (interface{}, interface{})
+       deleteObject() interface{}
+       deleteAnchor() interface{}
+}
+
+// MiddleendConfig The configmap of the middleent
+type MiddleendConfig struct {
+       OwnPort     string `json:"ownport"`
+       Clm         string `json:"clm"`
+       OrchService string `json:"orchestrator"`
+       OvnService  string `json:"ovnaction"`
+       Mongo       string `json:"mongo"`
+}
+
+// OrchestrationHandler interface, handling the composite app APIs
+type OrchestrationHandler struct {
+       MiddleendConf    MiddleendConfig
+       client           http.Client
+       compositeAppName string
+       compositeAppDesc string
+       AppName          string
+       meta             []appsData
+       DigData          deployDigData
+       file             map[string]*multipart.FileHeader
+       dataRead         *ProjectTree
+       treeFilter       *treeTraverseFilter
+       DigpReturnJson   []DigsInProject
+       projectName      string
+       projectDesc      string
+       version          string
+       response         struct {
+               payload map[string][]byte
+               status  map[string]int
+       }
+       digpIntents  map[string]string
+       nwCtlIntents map[string]string
+}
+
+// NewAppHandler interface implementing REST callhandler
+func NewAppHandler() *OrchestrationHandler {
+       return &OrchestrationHandler{}
+}
+
+// GetHealth to check connectivity
+func (h OrchestrationHandler) GetHealth(w http.ResponseWriter, r *http.Request) {
+       w.WriteHeader(http.StatusOK)
+
+}
+
+func (h OrchestrationHandler) apiGet(url string, statusKey string) (interface{}, []byte, error) {
+       // prepare and DEL API
+       request, err := http.NewRequest("GET", url, nil)
+       if err != nil {
+               return nil, nil, err
+       }
+       resp, err := h.client.Do(request)
+       if err != nil {
+               return nil, nil, err
+       }
+       defer resp.Body.Close()
+
+       // Prepare the response
+       data, _ := ioutil.ReadAll(resp.Body)
+       h.response.payload[statusKey] = data
+       h.response.status[statusKey] = resp.StatusCode
+
+       return resp.StatusCode, data, nil
+}
+
+func (h OrchestrationHandler) apiDel(url string, statusKey string) (interface{}, error) {
+       // prepare and DEL API
+       request, err := http.NewRequest("DELETE", url, nil)
+       if err != nil {
+               return nil, err
+       }
+       resp, err := h.client.Do(request)
+       if err != nil {
+               return nil, err
+       }
+       defer resp.Body.Close()
+
+       // Prepare the response
+       data, _ := ioutil.ReadAll(resp.Body)
+       h.response.payload[statusKey] = data
+       h.response.status[statusKey] = resp.StatusCode
+
+       return resp.StatusCode, nil
+}
+
+func (h OrchestrationHandler) apiPost(jsonLoad []byte, url string, statusKey string) (interface{}, error) {
+       // prepare and POST API
+       request, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonLoad))
+       if err != nil {
+               return nil, err
+       }
+       resp, err := h.client.Do(request)
+       if err != nil {
+               return nil, err
+       }
+       defer resp.Body.Close()
+
+       // Prepare the response
+       data, _ := ioutil.ReadAll(resp.Body)
+       h.response.payload[statusKey] = data
+       h.response.status[statusKey] = resp.StatusCode
+
+       return resp.StatusCode, nil
+}
+
+func (h OrchestrationHandler) apiPostMultipart(jsonLoad []byte,
+       fh *multipart.FileHeader, url string, statusKey string, fileName string) (interface{}, error) {
+       // Open the file
+       file, err := fh.Open()
+       if err != nil {
+               return nil, err
+       }
+       // Close the file later
+       defer file.Close()
+       // Buffer to store our request body as bytes
+       var requestBody bytes.Buffer
+       // Create a multipart writer
+       multiPartWriter := multipart.NewWriter(&requestBody)
+       // Initialize the file field. Arguments are the field name and file name
+       // It returns io.Writer
+       fileWriter, err := multiPartWriter.CreateFormFile("file", fileName)
+       if err != nil {
+               return nil, err
+       }
+       // Copy the actual file content to the field field's writer
+       _, err = io.Copy(fileWriter, file)
+       if err != nil {
+               return nil, err
+       }
+       // Populate other fields
+       fieldWriter, err := multiPartWriter.CreateFormField("metadata")
+       if err != nil {
+               return nil, err
+       }
+
+       _, err = fieldWriter.Write([]byte(jsonLoad))
+       if err != nil {
+               return nil, err
+       }
+
+       // We completed adding the file and the fields, let's close the multipart writer
+       // So it writes the ending boundary
+       multiPartWriter.Close()
+
+       // By now our original request body should have been populated,
+       // so let's just use it with our custom request
+       req, err := http.NewRequest("POST", url, &requestBody)
+       if err != nil {
+               return nil, err
+       }
+       // We need to set the content type from the writer, it includes necessary boundary as well
+       req.Header.Set("Content-Type", multiPartWriter.FormDataContentType())
+
+       // Do the request
+       resp, err := h.client.Do(req)
+       if err != nil {
+               log.Fatalln(err)
+               return nil, err
+       }
+       defer resp.Body.Close()
+       // Prepare the response
+       data, _ := ioutil.ReadAll(resp.Body)
+       h.response.payload[statusKey] = data
+       h.response.status[statusKey] = resp.StatusCode
+
+       return resp.StatusCode, nil
+}
+func (h *OrchestrationHandler) prepTreeReq(vars map[string]string) {
+       // Initialise the project tree with target composite application.
+       h.treeFilter = &treeTraverseFilter{}
+       h.treeFilter.compositeAppName = vars["composite-app-name"]
+       h.treeFilter.compositeAppVersion = vars["version"]
+       h.treeFilter.digName = vars["deployment-intent-group-name"]
+}
+
+func (h *OrchestrationHandler) DelDig(w http.ResponseWriter, r *http.Request) {
+       vars := mux.Vars(r)
+       h.projectName = vars["project-name"]
+       h.treeFilter = nil
+
+       dataPoints := []string{"projectHandler", "compAppHandler",
+               "digpHandler",
+               "placementIntentHandler",
+               "networkIntentHandler"}
+       h.response.status = make(map[string]int)
+       h.response.payload = make(map[string][]byte)
+
+       // Initialise the project tree with target composite application.
+       h.prepTreeReq(vars)
+
+       h.dataRead = &ProjectTree{}
+       retcode := h.constructTree(dataPoints)
+       if retcode != nil {
+               if intval, ok := retcode.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               return
+       }
+
+       // 1. Call DIG delte workflow
+       fmt.Printf("Delete wflow start")
+       deleteDataPoints := []string{"networkIntentHandler",
+               "placementIntentHandler",
+               "digpHandler"}
+       retcode = h.deleteTree(deleteDataPoints)
+       if retcode != nil {
+               if intval, ok := retcode.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               return
+       }
+       w.WriteHeader(204)
+}
+
+// Delete service workflow
+func (h *OrchestrationHandler) DelSvc(w http.ResponseWriter, r *http.Request) {
+       vars := mux.Vars(r)
+       h.projectName = vars["project-name"]
+       h.treeFilter = nil
+
+       dataPoints := []string{"projectHandler", "compAppHandler",
+               "digpHandler",
+               "ProfileHandler"}
+       h.response.status = make(map[string]int)
+       h.response.payload = make(map[string][]byte)
+
+       // Initialise the project tree with target composite application.
+       h.prepTreeReq(vars)
+
+       h.dataRead = &ProjectTree{}
+       retcode := h.constructTree(dataPoints)
+       if retcode != nil {
+               if intval, ok := retcode.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               return
+       }
+       fmt.Printf("tree %+v\n", h.dataRead)
+       // Check if a dig is present in this composite application
+       if len(h.dataRead.compositeAppMap[vars["composite-app-name"]].DigMap) != 0 {
+               w.WriteHeader(409)
+               w.Write([]byte("Non emtpy DIG in service\n"))
+               return
+       }
+
+       // 1. Call delte workflow
+       fmt.Printf("Delete wflow start")
+       deleteDataPoints := []string{"ProfileHandler",
+               "compAppHandler"}
+       retcode = h.deleteTree(deleteDataPoints)
+       if retcode != nil {
+               if intval, ok := retcode.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               return
+       }
+       w.WriteHeader(204)
+}
+
+func (h *OrchestrationHandler) getData(I orchWorkflow) (interface{}, interface{}) {
+       _, retcode := I.getAnchor()
+       if retcode != 200 {
+               return nil, retcode
+       }
+       dataPointData, retcode := I.getObject()
+       if retcode != 200 {
+               return nil, retcode
+       }
+       return dataPointData, retcode
+}
+
+func (h *OrchestrationHandler) deleteData(I orchWorkflow) (interface{}, interface{}) {
+       _ = I.deleteObject()
+       _ = I.deleteAnchor()
+       return nil, 204 //FIXME
+}
+
+func (h *OrchestrationHandler) deleteTree(dataPoints []string) interface{} {
+       //1. Fetch App data
+       var I orchWorkflow
+       for _, dataPoint := range dataPoints {
+               switch dataPoint {
+               case "projectHandler":
+                       temp := &projectHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.deleteData(I)
+                       if retcode != 204 {
+                               return retcode
+                       }
+                       break
+               case "compAppHandler":
+                       temp := &compAppHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.deleteData(I)
+                       if retcode != 204 {
+                               return retcode
+                       }
+                       break
+               case "ProfileHandler":
+                       temp := &ProfileHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.deleteData(I)
+                       if retcode != 204 {
+                               return retcode
+                       }
+                       break
+               case "digpHandler":
+                       temp := &digpHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       fmt.Printf("delete digp\n")
+                       _, retcode := h.deleteData(I)
+                       if retcode != 204 {
+                               return retcode
+                       }
+                       break
+               case "placementIntentHandler":
+                       temp := &placementIntentHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.deleteData(I)
+                       if retcode != 204 {
+                               return retcode
+                       }
+                       break
+               case "networkIntentHandler":
+                       temp := &networkIntentHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.deleteData(I)
+                       if retcode != 204 {
+                               return retcode
+                       }
+                       break
+               default:
+                       fmt.Printf("%s\n", dataPoint)
+               }
+       }
+       return nil
+}
+
+func (h *OrchestrationHandler) constructTree(dataPoints []string) interface{} {
+       //1. Fetch App data
+       var I orchWorkflow
+       for _, dataPoint := range dataPoints {
+               switch dataPoint {
+               case "projectHandler":
+                       temp := &projectHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.getData(I)
+                       if retcode != 200 {
+                               return retcode
+                       }
+                       break
+               case "compAppHandler":
+                       temp := &compAppHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.getData(I)
+                       if retcode != 200 {
+                               return retcode
+                       }
+                       break
+               case "ProfileHandler":
+                       temp := &ProfileHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.getData(I)
+                       if retcode != 200 {
+                               return retcode
+                       }
+                       break
+               case "digpHandler":
+                       temp := &digpHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.getData(I)
+                       if retcode != 200 {
+                               return retcode
+                       }
+                       break
+               case "placementIntentHandler":
+                       temp := &placementIntentHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.getData(I)
+                       if retcode != 200 {
+                               return retcode
+                       }
+                       break
+               case "networkIntentHandler":
+                       temp := &networkIntentHandler{}
+                       temp.orchInstance = h
+                       I = temp
+                       _, retcode := h.getData(I)
+                       if retcode != 200 {
+                               return retcode
+                       }
+                       break
+               default:
+                       fmt.Printf("%s\n", dataPoint)
+               }
+       }
+       return nil
+}
+
+// This function partest he compositeapp tree read and populates the
+// Dig tree
+func (h *OrchestrationHandler) copyDigTree() {
+       dataRead := h.dataRead
+       h.DigpReturnJson = nil
+
+       for compositeAppName, value := range dataRead.compositeAppMap {
+               for _, digValue := range dataRead.compositeAppMap[compositeAppName].DigMap {
+                       Dig := DigsInProject{}
+                       SourceDigMetadata := digValue.DigpData.Metadata
+
+                       // Copy the metadata
+                       Dig.Metadata.Name = SourceDigMetadata.Name
+                       Dig.Metadata.CompositeAppName = compositeAppName
+                       Dig.Metadata.CompositeAppVersion = value.Metadata.Spec.Version
+                       Dig.Metadata.Description = SourceDigMetadata.Description
+                       Dig.Metadata.UserData1 = SourceDigMetadata.UserData1
+                       Dig.Metadata.UserData2 = SourceDigMetadata.UserData2
+
+                       // Populate the Spec of dig
+                       SourceDigSpec := digValue.DigpData.Spec
+                       Dig.Spec.DigIntentsData = digValue.DigIntentsData.Intent
+                       Dig.Spec.Profile = SourceDigSpec.Profile
+                       Dig.Spec.Version = SourceDigSpec.Version
+                       Dig.Spec.Lcloud = SourceDigSpec.Lcloud
+                       Dig.Spec.OverrideValuesObj = SourceDigSpec.OverrideValuesObj
+
+                       // Pupolate the generic placement intents
+                       SourceGpintMap := digValue.GpintMap
+                       for t, gpintValue := range SourceGpintMap {
+                               fmt.Printf("gpName value %s\n", t)
+                               localGpint := DigsGpint{}
+                               localGpint.Metadata = gpintValue.Gpint.Metadata
+                               //localGpint.Spec.AppIntentArray = gpintValue.AppIntentArray
+                               localGpint.Spec.AppIntentArray = make([]PlacementIntentExport, len(gpintValue.AppIntentArray))
+                               for k, _ := range gpintValue.AppIntentArray {
+                                       localGpint.Spec.AppIntentArray[k].Metadata = gpintValue.AppIntentArray[k].Metadata
+                                       localGpint.Spec.AppIntentArray[k].Spec.AppName =
+                                               gpintValue.AppIntentArray[k].Spec.AppName
+                                       localGpint.Spec.AppIntentArray[k].Spec.Intent.AllofCluster =
+                                               make([]AllofExport, len(gpintValue.AppIntentArray[k].Spec.Intent.AllofCluster))
+                                       for i, _ := range gpintValue.AppIntentArray[k].Spec.Intent.AllofCluster {
+                                               localGpint.Spec.AppIntentArray[k].Spec.Intent.AllofCluster[i].ProviderName =
+                                                       gpintValue.AppIntentArray[k].Spec.Intent.AllofCluster[i].ProviderName
+                                               localGpint.Spec.AppIntentArray[k].Spec.Intent.AllofCluster[i].ClusterName =
+                                                       gpintValue.AppIntentArray[k].Spec.Intent.AllofCluster[i].ClusterName
+                                       }
+                               }
+
+                               Dig.Spec.GpintArray = append(Dig.Spec.GpintArray, &localGpint)
+                       }
+                       // Populate the Nwint intents
+                       SourceNwintMap := digValue.NwintMap
+                       for _, nwintValue := range SourceNwintMap {
+                               localNwint := DigsNwint{}
+                               localNwint.Metadata = nwintValue.Nwint.Metadata
+                               for _, wrkintValue := range nwintValue.WrkintMap {
+                                       localWrkint := WorkloadIntents{}
+                                       localWrkint.Metadata = wrkintValue.Wrkint.Metadata
+                                       localWrkint.Spec.Interfaces = wrkintValue.Interfaces
+                                       localNwint.Spec.WorkloadIntentsArray = append(localNwint.Spec.WorkloadIntentsArray,
+                                               &localWrkint)
+                               }
+                               Dig.Spec.NwintArray = append(Dig.Spec.NwintArray, &localNwint)
+                       }
+                       h.DigpReturnJson = append(h.DigpReturnJson, Dig)
+               }
+       }
+}
+
+// GetSvc get the entrire tree under project/<composite app>/<version>
+func (h *OrchestrationHandler) GetAllDigs(w http.ResponseWriter, r *http.Request) {
+       vars := mux.Vars(r)
+       h.version = vars["version"]
+       h.projectName = vars["project-name"]
+       h.response.status = make(map[string]int)
+       h.response.payload = make(map[string][]byte)
+       dataPoints := []string{"projectHandler", "compAppHandler",
+               "digpHandler",
+               "placementIntentHandler",
+               "networkIntentHandler"}
+
+       h.dataRead = &ProjectTree{}
+       h.treeFilter = nil
+       retcode := h.constructTree(dataPoints)
+       if retcode != nil {
+               if intval, ok := retcode.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               return
+       }
+       // copy dig tree
+       h.copyDigTree()
+       retval, _ := json.Marshal(h.DigpReturnJson)
+       w.Header().Set("Content-Type", "application/json")
+       w.WriteHeader(200)
+       w.Write(retval)
+}
+
+// GetSvc get the entrire tree under project/<composite app>/<version>
+func (h *OrchestrationHandler) GetSvc(w http.ResponseWriter, r *http.Request) {
+       vars := mux.Vars(r)
+       h.treeFilter = nil
+       h.compositeAppName = vars["composite-app-name"]
+       h.version = vars["version"]
+       h.projectName = vars["project-name"]
+       h.response.status = make(map[string]int)
+       h.response.payload = make(map[string][]byte)
+
+       dataPoints := []string{"compAppHandler", "ProfileHandler",
+               "digpHandler",
+               "placementIntentHandler",
+               "networkIntentHandler"}
+       h.dataRead = &ProjectTree{}
+       retcode := h.constructTree(dataPoints)
+       if retcode != nil {
+               if intval, ok := retcode.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               return
+       }
+
+       w.Header().Set("Content-Type", "application/json")
+       w.WriteHeader(200)
+}
+
+// CreateApp exported function which creates the composite application
+func (h *OrchestrationHandler) CreateDig(w http.ResponseWriter, r *http.Request) {
+       var jsonData deployDigData
+
+       decoder := json.NewDecoder(r.Body)
+       err := decoder.Decode(&jsonData)
+       if err != nil {
+               log.Printf("Failed to parse json")
+               log.Fatalln(err)
+       }
+
+       h.DigData = jsonData
+
+       if len(h.DigData.Spec.Apps) == 0 {
+               w.WriteHeader(400)
+               w.Write([]byte("Bad request, no app metadata\n"))
+               return
+       }
+
+       h.client = http.Client{}
+
+       // These maps will get populated by the return status and respones of each V2 API
+       // that is called during the execution of the workflow.
+       h.response.payload = make(map[string][]byte)
+       h.response.status = make(map[string]int)
+
+       // 4. Create DIG
+       h.digpIntents = make(map[string]string)
+       h.nwCtlIntents = make(map[string]string)
+       igHandler := &digpHandler{}
+       igHandler.orchInstance = h
+       igpStatus := createDInents(igHandler)
+       if igpStatus != nil {
+               if intval, ok := igpStatus.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               w.Write(h.response.payload[h.compositeAppName+"_digp"])
+               return
+       }
+
+       // 3. Create intents
+       intentHandler := &placementIntentHandler{}
+       intentHandler.orchInstance = h
+       intentStatus := addPlacementIntent(intentHandler)
+       if intentStatus != nil {
+               if intval, ok := intentStatus.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               w.Write(h.response.payload[h.compositeAppName+"_gpint"])
+               return
+       }
+
+       // If the metadata contains network interface request then call the
+       // network intent related part of the workflow.
+       if len(h.DigData.Spec.Apps[0].Clusters[0].SelectedClusters[0].Interfaces) != 0 {
+               nwHandler := &networkIntentHandler{}
+               nwHandler.orchInstance = h
+               nwIntentStatus := addNetworkIntent(nwHandler)
+               if nwIntentStatus != nil {
+                       if intval, ok := nwIntentStatus.(int); ok {
+                               w.WriteHeader(intval)
+                       } else {
+                               w.WriteHeader(500)
+                       }
+                       w.Write(h.response.payload[h.compositeAppName+"_nwctlint"])
+                       return
+               }
+       }
+
+       w.WriteHeader(201)
+       w.Write(h.response.payload[h.DigData.Name])
+}
+
+func (h *OrchestrationHandler) CreateApp(w http.ResponseWriter, r *http.Request) {
+       var jsonData deployServiceData
+
+       err := r.ParseMultipartForm(16777216)
+       if err != nil {
+               log.Fatalln(err)
+       }
+
+       // Populate the multipart.FileHeader MAP. The key will be the
+       // filename itself. The metadata Map will be keyed on the application
+       // name. The metadata has a field file name, so later we can parse the metadata
+       // Map, and fetch the file headers from this file Map with keys as the filename.
+       h.file = make(map[string]*multipart.FileHeader)
+       for _, v := range r.MultipartForm.File {
+               fh := v[0]
+               h.file[fh.Filename] = fh
+       }
+
+       jsn := ([]byte(r.FormValue("servicePayload")))
+       err = json.Unmarshal(jsn, &jsonData)
+       if err != nil {
+               log.Printf("Failed to parse json")
+               log.Fatalln(err)
+       }
+
+       h.compositeAppName = jsonData.Name
+       h.compositeAppDesc = jsonData.Description
+       h.projectName = jsonData.Spec.ProjectName
+       h.meta = jsonData.Spec.Apps
+
+       // Sanity check. For each metadata there should be a
+       // corresponding file in the multipart request. If it
+       // not found we fail this API call.
+       for i := range h.meta {
+               switch {
+               case h.file[h.meta[i].Metadata.FileName] == nil:
+                       t := fmt.Sprintf("File %s not in request", h.meta[i].Metadata.FileName)
+                       w.WriteHeader(400)
+                       w.Write([]byte(t))
+                       fmt.Printf("app file not found\n")
+                       return
+               case h.file[h.meta[i].ProfileMetadata.FileName] == nil:
+                       t := fmt.Sprintf("File %s not in request", h.meta[i].ProfileMetadata.FileName)
+                       w.WriteHeader(400)
+                       w.Write([]byte(t))
+                       fmt.Printf("profile file not found\n")
+                       return
+               default:
+                       fmt.Println("Good request")
+               }
+       }
+
+       if len(h.meta) == 0 {
+               w.WriteHeader(400)
+               w.Write([]byte("Bad request, no app metadata\n"))
+               return
+       }
+
+       h.client = http.Client{}
+
+       // These maps will get populated by the return status and respones of each V2 API
+       // that is called during the execution of the workflow.
+       h.response.payload = make(map[string][]byte)
+       h.response.status = make(map[string]int)
+
+       // 1. create the composite application. the compAppHandler implements the
+       // orchWorkflow interface.
+       appHandler := &compAppHandler{}
+       appHandler.orchInstance = h
+       appStatus := createCompositeapp(appHandler)
+       if appStatus != nil {
+               if intval, ok := appStatus.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               w.Write(h.response.payload[h.compositeAppName])
+               return
+       }
+
+       // 2. create the composite application profiles
+       profileHandler := &ProfileHandler{}
+       profileHandler.orchInstance = h
+       profileStatus := createProfile(profileHandler)
+       if profileStatus != nil {
+               if intval, ok := profileStatus.(int); ok {
+                       w.WriteHeader(intval)
+               } else {
+                       w.WriteHeader(500)
+               }
+               w.Write(h.response.payload[h.compositeAppName+"_profile"])
+               return
+       }
+
+       w.WriteHeader(201)
+       w.Write(h.response.payload[h.compositeAppName])
+}
+
+func (h *OrchestrationHandler) createCluster(filename string, fh *multipart.FileHeader, clusterName string,
+       jsonData ClusterMetadata) interface{} {
+       url := "http://" + h.MiddleendConf.Clm + "/v2/cluster-providers/" + clusterName + "/clusters"
+
+       jsonLoad, _ := json.Marshal(jsonData)
+
+       status, err := h.apiPostMultipart(jsonLoad, fh, url, clusterName, filename)
+       if err != nil {
+               return err
+       }
+       if status != 201 {
+               return status
+       }
+       fmt.Printf("cluster creation %s status %s\n", clusterName, status)
+       return nil
+}
+
+func (h *OrchestrationHandler) CheckConnection(w http.ResponseWriter, r *http.Request) {
+       vars := mux.Vars(r)
+
+       parse_err := r.ParseMultipartForm(16777216)
+       if parse_err != nil {
+               fmt.Printf("multipart error: %s", parse_err.Error())
+               w.WriteHeader(500)
+               return
+       }
+
+       var fh *multipart.FileHeader
+       for _, v := range r.MultipartForm.File {
+               fh = v[0]
+       }
+       file, err := fh.Open()
+       if err != nil {
+               fmt.Printf("Failed to open the file: %s", err.Error())
+               w.WriteHeader(500)
+               return
+       }
+       defer file.Close()
+
+       // Read the kconfig
+       kubeconfig, _ := ioutil.ReadAll(file)
+
+       jsonData := ClusterMetadata{}
+       jsn := ([]byte(r.FormValue("metadata")))
+       err = json.Unmarshal(jsn, &jsonData)
+       if err != nil {
+               fmt.Printf("Failed to parse json")
+               w.WriteHeader(500)
+               return
+       }
+       fmt.Printf("metadata %+v\n", jsonData)
+
+       // RESTConfigFromKubeConfig is a convenience method to give back
+       // a restconfig from your kubeconfig bytes.
+       config, err := clientcmd.RESTConfigFromKubeConfig(kubeconfig)
+       if err != nil {
+               fmt.Printf("Error while reading the kubeconfig: %s", err.Error())
+               w.WriteHeader(500)
+               return
+       }
+
+       // create the clientset
+       clientset, err := kubernetes.NewForConfig(config)
+       if err != nil {
+               fmt.Printf("Failed to create clientset: %s", err.Error())
+               w.WriteHeader(500)
+               return
+       }
+
+       _, err = clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
+       if err != nil {
+               fmt.Printf("Failed to establish the connection: %s", err.Error())
+               w.WriteHeader(403)
+               w.Write([]byte("Cluster connectivity failed\n"))
+               return
+       }
+
+       fmt.Printf("Successfully established the connection\n")
+       h.client = http.Client{}
+       h.response.status = make(map[string]int)
+       h.response.payload = make(map[string][]byte)
+
+       status := h.createCluster(fh.Filename, fh, vars["cluster-provider-name"], jsonData)
+       if status != nil {
+               w.WriteHeader(500)
+               return
+       }
+
+       w.WriteHeader(200)
+       w.Write(h.response.payload[vars["cluster-provider-name"]])
+       return
+}
diff --git a/src/tools/emcoui/middle_end/app/compositeapp.go b/src/tools/emcoui/middle_end/app/compositeapp.go
new file mode 100644 (file)
index 0000000..daae5b0
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package app
+
+import (
+       "encoding/json"
+       "fmt"
+)
+
+// CompositeApp application structure
+type CompositeApp struct {
+       Metadata apiMetaData      `json:"metadata"`
+       Spec     compositeAppSpec `json:"spec"`
+}
+
+type compositeAppSpec struct {
+       Version string `json:"version"`
+}
+
+// compAppHandler , This implements the orchworkflow interface
+type compAppHandler struct {
+       orchURL      string
+       orchInstance *OrchestrationHandler
+}
+
+// CompositeAppKey is the mongo key to fetch apps in a composite app
+type CompositeAppKey struct {
+       Cname    string      `json:"compositeapp"`
+       Project  string      `json:"project"`
+       Cversion string      `json:"compositeappversion"`
+       App      interface{} `json:"app"`
+}
+
+func (h *compAppHandler) getObject() (interface{}, interface{}) {
+       orch := h.orchInstance
+       respcode := 200
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version + "/apps"
+
+               respcode, respdata, err := orch.apiGet(h.orchURL, orch.compositeAppName+"_getapps")
+               if err != nil {
+                       return nil, 500
+               }
+               if respcode != 200 {
+                       return nil, respcode
+               }
+               fmt.Printf("Get app status %s\n", respcode)
+               compositeAppValue.AppsDataArray = make(map[string]*AppsData, len(respdata))
+               var appList []CompositeApp
+               json.Unmarshal(respdata, &appList)
+               for _, value := range appList {
+                       var appsDataInstance AppsData
+                       appName := value.Metadata.Name
+                       appsDataInstance.App = value
+                       compositeAppValue.AppsDataArray[appName] = &appsDataInstance
+               }
+       }
+       return nil, respcode
+}
+
+func (h *compAppHandler) getAnchor() (interface{}, interface{}) {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       respcode := 200
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version
+
+               respcode, _, err := orch.apiGet(h.orchURL, orch.compositeAppName+"_getcompositeapp")
+               if err != nil {
+                       return nil, 500
+               }
+               if respcode != 200 {
+                       return nil, respcode
+               }
+               fmt.Printf("Get composite App %s\n", respcode)
+               //json.Unmarshal(respdata, &dataRead.CompositeApp)
+       }
+       return nil, respcode
+}
+
+func (h *compAppHandler) deleteObject() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version
+               appList := compositeAppValue.AppsDataArray
+               for _, value := range appList {
+                       url := h.orchURL + "/apps/" + value.App.Metadata.Name
+                       fmt.Printf("Delete app %s\n", url)
+                       resp, err := orch.apiDel(url, compositeAppMetadata.Name+"_delapp")
+                       if err != nil {
+                               return err
+                       }
+                       if resp != 204 {
+                               return resp
+                       }
+                       fmt.Printf("Delete app status %s\n", resp)
+               }
+       }
+       return nil
+}
+
+func (h *compAppHandler) deleteAnchor() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version
+               fmt.Printf("Delete composite app %s\n", h.orchURL)
+               resp, err := orch.apiDel(h.orchURL, compositeAppMetadata.Name+"_delcompapp")
+               if err != nil {
+                       return err
+               }
+               if resp != 204 {
+                       return resp
+               }
+               fmt.Printf("Delete compapp status %s\n", resp)
+       }
+       return nil
+}
+
+// CreateAnchor creates the anchor point for composite applications,
+// profiles, intents etc. For example Anchor for the composite application
+// will create the composite application resource in the the DB, and all apps
+// will get created and uploaded under this anchor point.
+func (h *compAppHandler) createAnchor() interface{} {
+       orch := h.orchInstance
+
+       compAppCreate := CompositeApp{
+               Metadata: apiMetaData{
+                       Name:        orch.compositeAppName,
+                       Description: orch.compositeAppDesc,
+                       UserData1:   "data 1",
+                       UserData2:   "data 2"},
+               Spec: compositeAppSpec{
+                       Version: "v1"},
+       }
+
+       jsonLoad, _ := json.Marshal(compAppCreate)
+       tem := CompositeApp{}
+       json.Unmarshal(jsonLoad, &tem)
+       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+               orch.projectName + "/composite-apps"
+       resp, err := orch.apiPost(jsonLoad, h.orchURL, orch.compositeAppName)
+       if err != nil {
+               return err
+       }
+       if resp != 201 {
+               return resp
+       }
+       orch.version = "v1"
+       fmt.Printf("compAppHandler resp %s\n", resp)
+
+       return nil
+}
+
+func (h *compAppHandler) createObject() interface{} {
+       orch := h.orchInstance
+       for i := range orch.meta {
+               fileName := orch.meta[i].Metadata.FileName
+               appName := orch.meta[i].Metadata.Name
+               appDesc := orch.meta[i].Metadata.Description
+
+               // Upload the application helm chart
+               fh := orch.file[fileName]
+               compAppAdd := CompositeApp{
+                       Metadata: apiMetaData{
+                               Name:        appName,
+                               Description: appDesc,
+                               UserData1:   "data 1",
+                               UserData2:   "data2"},
+               }
+               url := h.orchURL + "/" + orch.compositeAppName + "/" + orch.version + "/apps"
+
+               jsonLoad, _ := json.Marshal(compAppAdd)
+
+               status, err := orch.apiPostMultipart(jsonLoad, fh, url, appName, fileName)
+               if err != nil {
+                       return err
+               }
+               if status != 201 {
+                       return status
+               }
+               fmt.Printf("Composite app %s createObject status %s\n", appName, status)
+       }
+
+       return nil
+}
+
+func createCompositeapp(I orchWorkflow) interface{} {
+       // 1. Create the Anchor point
+       err := I.createAnchor()
+       if err != nil {
+               return err
+       }
+       // 2. Create the Objects
+       err = I.createObject()
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
+func delCompositeapp(I orchWorkflow) interface{} {
+       // 1. Delete the object
+       err := I.deleteObject()
+       if err != nil {
+               return err
+       }
+       // 2. Delete the Anchor
+       err = I.deleteAnchor()
+       if err != nil {
+               return err
+       }
+       return nil
+}
diff --git a/src/tools/emcoui/middle_end/app/digp.go b/src/tools/emcoui/middle_end/app/digp.go
new file mode 100644 (file)
index 0000000..b4c83e1
--- /dev/null
@@ -0,0 +1,322 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package app
+
+import (
+       "encoding/json"
+       "fmt"
+       "log"
+)
+
+type DeploymentIGP struct {
+       Metadata apiMetaData `json:"metadata"`
+       Spec     DigpSpec    `json:"spec"`
+}
+
+type DigpSpec struct {
+       Profile           string           `json:"profile"`
+       Version           string           `json:"version"`
+       Lcloud            string           `json:"logical-cloud"`
+       OverrideValuesObj []OverrideValues `json:"override-values"`
+}
+
+// OverrideValues ...
+type OverrideValues struct {
+       AppName   string            `json:"app-name"`
+       ValuesObj map[string]string `json:"values"`
+}
+
+type IgpIntents struct {
+       Metadata apiMetaData `json:"metadata"`
+       Spec     AppIntents  `json:"spec"`
+}
+
+type AppIntents struct {
+       Intent map[string]string `json:"intent"`
+}
+
+type DigpIntents struct {
+       Intent []DigDeployedIntents `json:"intent"`
+}
+type DigDeployedIntents struct {
+       GenericPlacementIntent string `json:"genericPlacementIntent"`
+       Ovnaction              string `json:"ovnaction"`
+}
+
+// digpHandler implements the orchworkflow interface
+type digpHandler struct {
+       orchURL      string
+       orchInstance *OrchestrationHandler
+}
+
+func (h *digpHandler) getAnchor() (interface{}, interface{}) {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       retcode := 200
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               var digpList []DeploymentIGP
+               // This for the cases where the dig name is in the URL
+               if orch.treeFilter != nil && orch.treeFilter.digName != ""{
+                       temp:=DeploymentIGP{}
+                       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups/" + orch.treeFilter.digName
+                       retcode, retval, err := orch.apiGet(h.orchURL, orch.compositeAppName+"_digp")
+                       fmt.Printf("Get Digp in composite app %s status %d\n", compositeAppMetadata.Name, retcode)
+                       if err != nil {
+                               fmt.Printf("Failed to read digp")
+                               return nil, 500
+                       }
+                       if retcode != 200 {
+                               fmt.Printf("Failed to read digp")
+                       return nil, retcode
+                       }
+                       json.Unmarshal(retval, &temp)
+                       digpList = append(digpList, temp)
+               } else {
+                       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups"
+                       retcode, retval, err := orch.apiGet(h.orchURL, orch.compositeAppName+"_digp")
+                       fmt.Printf("Get Digp in composite app %s status %d\n", compositeAppMetadata.Name, retcode)
+                       if err != nil {
+                               fmt.Printf("Failed to read digp")
+                               return nil, 500
+                       }
+                       if retcode != 200 {
+                               fmt.Printf("Failed to read digp")
+                       return nil, retcode
+                       }
+                       json.Unmarshal(retval, &digpList)
+               }
+
+               compositeAppValue.DigMap = make(map[string]*DigReadData, len(digpList))
+               for _, digpValue := range digpList {
+                       var Dig DigReadData
+                       Dig.DigpData = digpValue
+                       compositeAppValue.DigMap[digpValue.Metadata.Name] = &Dig
+               }
+       }
+       return nil, retcode
+}
+
+func (h *digpHandler) getObject() (interface{}, interface{}) {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version +
+                       "/deployment-intent-groups/"
+               digpList := compositeAppValue.DigMap
+               for digName, digValue := range digpList {
+                       url := h.orchURL + digName + "/intents"
+                       retcode, retval, err := orch.apiGet(url, compositeAppMetadata.Name+"_digpIntents")
+                       fmt.Printf("Get Dig int composite app %s Dig %s status %d \n", orch.compositeAppName,
+                               digName, retcode)
+                       if err != nil {
+                               fmt.Printf("Failed to read digp intents")
+                               return nil, 500
+                       }
+                       if retcode != 200 {
+                               fmt.Printf("Failed to read digp intents")
+                               return nil, retcode
+
+                       }
+                       err = json.Unmarshal(retval, &digValue.DigIntentsData)
+                       if err != nil {
+                               fmt.Printf("Failed to read intents %s\n", err)
+                       }
+               }
+       }
+       return nil, 200
+}
+
+func (h *digpHandler) deleteObject() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               digpList := compositeAppValue.DigMap
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version +
+                       "/deployment-intent-groups/"
+
+               for digName, _ := range digpList {
+                       url := h.orchURL + digName + "/intents/PlacementIntent"
+                       fmt.Printf("dlete intents %s\n", url)
+                       resp, err := orch.apiDel(url, orch.compositeAppName+"_deldigintents")
+                       if err != nil {
+                               return err
+                       }
+                       if resp != 204 {
+                               return resp
+                       }
+                       fmt.Printf("Delete dig intents resp %s\n", resp)
+               }
+       }
+       return nil
+}
+
+func (h *digpHandler) deleteAnchor() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               digpList := compositeAppValue.DigMap
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version +
+                       "/deployment-intent-groups/"
+
+               // loop through all the intents in the dig
+               for digName, _ := range digpList {
+                       url := h.orchURL + digName
+                       turl := h.orchURL + digName + "/terminate"
+                       fmt.Printf("delete intents %s\n", url)
+                       jsonLoad, _ := json.Marshal("{}")
+                       resp, err := orch.apiPost(jsonLoad, turl, orch.compositeAppName+"_terminatedig")
+                       //Not checking the status of terminate FIXME
+                       resp, err = orch.apiDel(url, orch.compositeAppName+"_deldig")
+                       if err != nil {
+                               return err
+                       }
+                       if resp != 204 {
+                               return resp
+                       }
+                       fmt.Printf("Delete dig resp %s\n", resp)
+               }
+       }
+       return nil
+}
+
+func (h *digpHandler) createAnchor() interface{} {
+       digData := h.orchInstance.DigData
+       orch := h.orchInstance
+
+       digp := DeploymentIGP{
+               Metadata: apiMetaData{
+                       Name:        digData.Name,
+                       Description: digData.Description,
+                       UserData1:   "data 1",
+                       UserData2:   "data2"},
+               Spec: DigpSpec{
+                       Profile:           digData.CompositeProfile,
+                       Version:           digData.DigVersion,
+                       Lcloud:            "unused_logical_cloud",
+                       OverrideValuesObj: make([]OverrideValues, len(digData.Spec.Apps)),
+               },
+       }
+       overrideVals := digp.Spec.OverrideValuesObj
+       for i, value := range digData.Spec.Apps {
+               overrideVals[i].ValuesObj = make(map[string]string)
+               overrideVals[i].AppName = value.Metadata.Name
+               overrideVals[i].ValuesObj["Values.global.dcaeCollectorIp"] = "1.8.0"
+       }
+
+       jsonLoad, _ := json.Marshal(digp)
+
+       // POST the generic placement intent
+       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + digData.Spec.ProjectName +
+               "/composite-apps/" + digData.CompositeAppName + "/" + digData.CompositeAppVersion +
+               "/deployment-intent-groups"
+       resp, err := orch.apiPost(jsonLoad, h.orchURL, digData.Name)
+       if err != nil {
+               return err
+       }
+       if resp != 201 {
+               return resp
+       }
+       orch.digpIntents["generic-placement-intent"] = digData.CompositeAppName + "_gpint"
+       orch.nwCtlIntents["network-controller-intent"] = digData.CompositeAppName + "_nwctlint"
+       fmt.Printf("Deloyment intent group resp %s\n", resp)
+
+       return nil
+}
+
+func (h *digpHandler) createObject() interface{} {
+       digData := h.orchInstance.DigData
+       orch := h.orchInstance
+       intentName := "PlacementIntent"
+       igp := IgpIntents{
+               Metadata: apiMetaData{
+                       Name:        intentName,
+                       Description: "NA",
+                       UserData1:   "data 1",
+                       UserData2:   "data2"},
+       }
+       if len(digData.Spec.Apps[0].Clusters[0].SelectedClusters[0].Interfaces) != 0 {
+               igp.Spec.Intent = make(map[string]string)
+               igp.Spec.Intent["genericPlacementIntent"] = orch.digpIntents["generic-placement-intent"]
+               igp.Spec.Intent["ovnaction"] = orch.nwCtlIntents["network-controller-intent"]
+       } else {
+               igp.Spec.Intent = make(map[string]string)
+               igp.Spec.Intent["genericPlacementIntent"] = orch.digpIntents["generic-placement-intent"]
+       }
+
+       url := h.orchURL + "/" + digData.Name + "/intents"
+       jsonLoad, _ := json.Marshal(igp)
+       status, err := orch.apiPost(jsonLoad, url, intentName)
+       fmt.Printf("DIG name req %s", string(jsonLoad))
+       if err != nil {
+               log.Fatalln(err)
+       }
+       if status != 201 {
+               return status
+       }
+       fmt.Printf("Placement intent %s status %s %s\n", intentName, status, url)
+
+       return nil
+}
+
+func createDInents(I orchWorkflow) interface{} {
+       // 1. Create the Anchor point
+       err := I.createAnchor()
+       if err != nil {
+               return err
+       }
+       // 2. Create the Objects
+       err = I.createObject()
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
+func delDigp(I orchWorkflow) interface{} {
+       // 1. Delete the object
+       err := I.deleteObject()
+       if err != nil {
+               return err
+       }
+       // 2. Delete the Anchor
+       err = I.deleteAnchor()
+       if err != nil {
+               return err
+       }
+       return nil
+}
diff --git a/src/tools/emcoui/middle_end/app/intents.go b/src/tools/emcoui/middle_end/app/intents.go
new file mode 100644 (file)
index 0000000..992f7b6
--- /dev/null
@@ -0,0 +1,664 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package app
+
+import (
+       "encoding/json"
+       "fmt"
+       "log"
+       "strconv"
+)
+
+type GenericPlacementIntent struct {
+       Metadata apiMetaData `json:"metadata"`
+}
+
+type PlacementIntent struct {
+       Metadata apiMetaData            `json:"metadata"`
+       Spec     AppPlacementIntentSpec `json:"spec"`
+}
+type PlacementIntentExport struct {
+       Metadata apiMetaData            `json:"metadata"`
+       Spec     AppPlacementIntentSpecExport `json:"spec"`
+}
+
+// appPlacementIntentSpec is the spec for per app intent
+type AppPlacementIntentSpec struct {
+       AppName string      `json:"app-name"`
+       Intent  arrayIntent `json:"intent"`
+}
+type arrayIntent struct {
+       AllofCluster []Allof `json:"allof"`
+}
+type Allof struct {
+       ProviderName string `json:"provider-name"`
+       ClusterName  string `json:"cluster-name"`
+}
+type AppPlacementIntentSpecExport struct {
+       AppName string      `json:"appName"`
+       Intent  arrayIntentExport `json:"intent"`
+}
+type arrayIntentExport struct {
+       AllofCluster []AllofExport `json:"allof"`
+}
+type AllofExport struct {
+       ProviderName string `json:"providerName"`
+       ClusterName  string `json:"clusterName"`
+}
+
+// plamcentIntentHandler implements the orchworkflow interface
+type placementIntentHandler struct {
+       orchURL      string
+       orchInstance *OrchestrationHandler
+}
+
+type NetworkCtlIntent struct {
+       Metadata apiMetaData `json:"metadata"`
+}
+
+type NetworkWlIntent struct {
+       Metadata apiMetaData        `json:"metadata"`
+       Spec     WorkloadIntentSpec `json:"spec"`
+}
+
+type WorkloadIntentSpec struct {
+       AppName  string `json:"application-name"`
+       Resource string `json:"workload-resource"`
+       Type     string `json:"type"`
+}
+type WorkloadIntentSpecExport struct {
+       AppName  string `json:"applicationName"`
+       Resource string `json:"workloadResource"`
+       Type     string `json:"type"`
+}
+
+type NwInterface struct {
+       Metadata apiMetaData   `json:"metadata"`
+       Spec     InterfaceSpec `json:"spec"`
+}
+
+type InterfaceSpec struct {
+       Interface      string `json:"interface"`
+       Name           string `json:"name"`
+       DefaultGateway string `json:"defaultGateway"`
+       IPAddress      string `json:"ipAddress"`
+       MacAddress     string `json:"macAddress"`
+}
+
+// networkIntentHandler implements the orchworkflow interface
+type networkIntentHandler struct {
+       ovnURL       string
+       orchInstance *OrchestrationHandler
+}
+
+func (h *placementIntentHandler) getObject() (interface{}, interface{}) {
+       orch := h.orchInstance
+       retcode := 200
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               Dig := compositeAppValue.DigMap
+               Apps := compositeAppValue.AppsDataArray
+               for digName, digValue := range Dig {
+                       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups/" + digName + "/generic-placement-intents"
+                       for gpintName, gpintValue := range digValue.GpintMap {
+                               for appName, _ := range Apps {
+                                       var appPint PlacementIntent
+                                       url := h.orchURL + "/" + gpintName + "/app-intents/" + appName + "_pint"
+                                       retcode, retval, err := orch.apiGet(url, compositeAppMetadata.Name+"_getappPint")
+                                       fmt.Printf("Get Gpint App intent in Composite app %s dig %s Gpint %s status %s\n",
+                                               orch.compositeAppName, digName, gpintName, retcode)
+                                       if err != nil {
+                                               fmt.Printf("Failed to read app pint\n")
+                                               return nil, 500
+                                       }
+                                       if retcode != 200 {
+                                               fmt.Printf("Failed to read app pint\n")
+                                               return nil, 200
+                                       }
+                                       err = json.Unmarshal(retval, &appPint)
+                                       if err != nil {
+                                               fmt.Printf("Failed to unmarshal json %s\n", err)
+                                               return nil, 500
+                                       }
+                                       gpintValue.AppIntentArray = append(gpintValue.AppIntentArray, appPint)
+                               }
+                       }
+               }
+       }
+       return nil, retcode
+}
+
+func (h *placementIntentHandler) getAnchor() (interface{}, interface{}) {
+       orch := h.orchInstance
+       retcode := 200
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               Dig := compositeAppValue.DigMap
+               for digName, digValue := range Dig {
+                       var gpintList []GenericPlacementIntent
+                       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups/" + digName + "/generic-placement-intents"
+                       retcode, retval, err := orch.apiGet(h.orchURL, compositeAppMetadata.Name+"_getgpint")
+                       fmt.Printf("Get Gpint in Composite app %s dig %s status %s\n", orch.compositeAppName,
+                               digName, retcode)
+                       if err != nil {
+                               fmt.Printf("Failed to read gpint\n")
+                               return nil, 500
+                       }
+                       if retcode != 200 {
+                               fmt.Printf("Failed to read gpint\n")
+                               return nil, retcode
+                       }
+                       json.Unmarshal(retval, &gpintList)
+                       digValue.GpintMap = make(map[string]*GpintData, len(gpintList))
+                       for _, value := range gpintList {
+                               var GpintDataInstance GpintData
+                               GpintDataInstance.Gpint = value
+                               digValue.GpintMap[value.Metadata.Name] = &GpintDataInstance
+                       }
+               }
+       }
+       return nil, retcode
+}
+
+func (h *placementIntentHandler) deleteObject() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               Dig := compositeAppValue.DigMap
+               Apps := compositeAppValue.AppsDataArray
+
+               // loop through all app intens in the gpint
+               for digName, digValue := range Dig {
+                       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups/" + digName + "/generic-placement-intents/"
+                       for gpintName, _ := range digValue.GpintMap {
+                               for appName, _ := range Apps {
+                                       url := h.orchURL + gpintName +
+                                               "/app-intents/" + appName + "_pint" // FIXME when query API works, change this API call to
+                                       // query based on app name.
+                                       fmt.Printf("Delete gping app intents %s\n", url)
+                                       resp, err := orch.apiDel(url, orch.compositeAppName+"_delgpintintents")
+                                       if err != nil {
+                                               return err
+                                       }
+                                       if resp != 204 {
+                                               return resp
+                                       }
+                                       fmt.Printf("Delete gpint intents resp %s\n", resp)
+                               }
+                       }
+               }
+       }
+       return nil
+}
+
+func (h placementIntentHandler) deleteAnchor() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               Dig := compositeAppValue.DigMap
+
+               // loop through all app intens in the gpint
+               for digName, digValue := range Dig {
+                       for gpintName, _ := range digValue.GpintMap {
+                               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                                       "/" + compositeAppSpec.Version +
+                                       "/deployment-intent-groups/" + digName + "/generic-placement-intents/" +
+                                       gpintName
+                               fmt.Printf("Delete gpint  %s\n", h.orchURL)
+                               resp, err := orch.apiDel(h.orchURL, compositeAppMetadata.Name+"_delgpints")
+                               if err != nil {
+                                       return err
+                               }
+                               if resp != 204 {
+                                       return resp
+                               }
+                               fmt.Printf("Delete gpint resp %s\n", resp)
+                       }
+               }
+       }
+       return nil
+}
+
+func (h *placementIntentHandler) createAnchor() interface{} {
+       orch := h.orchInstance
+       intentData := h.orchInstance.DigData
+
+       gpi := GenericPlacementIntent{
+               Metadata: apiMetaData{
+                       Name:        intentData.CompositeAppName + "_gpint",
+                       Description: "Generic placement intent created from middleend",
+                       UserData1:   "data 1",
+                       UserData2:   "data2"},
+       }
+
+       jsonLoad, _ := json.Marshal(gpi)
+       // POST the generic placement intent
+       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + intentData.Spec.ProjectName +
+               "/composite-apps/" + intentData.CompositeAppName + "/" + intentData.CompositeAppVersion +
+               "/deployment-intent-groups/" + intentData.Name
+       url := h.orchURL + "/generic-placement-intents"
+       resp, err := orch.apiPost(jsonLoad, url, orch.digpIntents["generic-placement-intent"])
+       if err != nil {
+               return err
+       }
+       if resp != 201 {
+               return resp
+       }
+       fmt.Printf("Generic placement intent resp %s\n", resp)
+
+       return nil
+}
+
+func (h *placementIntentHandler) createObject() interface{} {
+       orch := h.orchInstance
+       intentData := h.orchInstance.DigData
+
+       for _, value := range intentData.Spec.Apps {
+               appName := value.Metadata.Name
+               intentName := appName + "_pint"
+               genericAppIntentName := intentData.CompositeAppName + "_gpint"
+               providerName := value.Clusters[0].Provider
+               clusterName := value.Clusters[0].SelectedClusters[0].Name
+
+               pint := PlacementIntent{
+                       Metadata: apiMetaData{
+                               Name:        intentName,
+                               Description: "NA",
+                               UserData1:   "data 1",
+                               UserData2:   "data2"},
+                       Spec: AppPlacementIntentSpec{
+                               AppName: appName,
+                               Intent: arrayIntent{
+                                       AllofCluster: []Allof{ // FIXME: the logic requires to handle allof/anyof and multi cluster.
+                                               Allof{
+                                                       ProviderName: providerName,
+                                                       ClusterName:  clusterName},
+                                       },
+                               },
+                       },
+               }
+
+               url := h.orchURL + "/generic-placement-intents/" + genericAppIntentName + "/app-intents"
+               jsonLoad, _ := json.Marshal(pint)
+               status, err := orch.apiPost(jsonLoad, url, intentName)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               if status != 201 {
+                       return status
+               }
+               fmt.Printf("Placement intent %s status %s %s\n", intentName, status, url)
+       }
+
+       return nil
+}
+
+func addPlacementIntent(I orchWorkflow) interface{} {
+       // 1. Create the Anchor point
+       err := I.createAnchor()
+       if err != nil {
+               return err
+       }
+       // 2. Create the Objects
+       err = I.createObject()
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
+func delGpint(I orchWorkflow) interface{} {
+       // 1. Create the Anchor point
+       err := I.deleteObject()
+       if err != nil {
+               return err
+       }
+       // 2. Create the Objects
+       err = I.deleteAnchor()
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
+func (h *networkIntentHandler) createAnchor() interface{} {
+       orch := h.orchInstance
+       intentData := h.orchInstance.DigData
+
+       nwIntent := NetworkCtlIntent{
+               Metadata: apiMetaData{
+                       Name:        intentData.CompositeAppName + "_nwctlint",
+                       Description: "Network Controller created from middleend",
+                       UserData1:   "data 1",
+                       UserData2:   "data2"},
+       }
+       jsonLoad, _ := json.Marshal(nwIntent)
+       // POST the network controller intent
+       h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" + intentData.Spec.ProjectName +
+               "/composite-apps/" + intentData.CompositeAppName + "/" + intentData.CompositeAppVersion +
+               "/deployment-intent-groups/" + intentData.Name
+       url := h.ovnURL + "/network-controller-intent"
+       resp, err := orch.apiPost(jsonLoad, url, orch.nwCtlIntents["network-controller-intent"])
+       if err != nil {
+               return err
+       }
+       if resp != 201 {
+               return resp
+       }
+       fmt.Printf("Network contoller intent resp %s\n", resp)
+
+       return nil
+}
+
+func (h *networkIntentHandler) createObject() interface{} {
+       orch := h.orchInstance
+       intentData := h.orchInstance.DigData
+
+       for _, value := range intentData.Spec.Apps {
+
+               appName := value.Metadata.Name
+               intentName := value.Metadata.Name + "_wnwlint"
+               genericAppIntentName := intentData.CompositeAppName + "_nwctlint"
+
+               wlIntent := NetworkWlIntent{
+                       Metadata: apiMetaData{
+                               Name:        intentName,
+                               Description: "NA",
+                               UserData1:   "data 1",
+                               UserData2:   "data2"},
+                       Spec: WorkloadIntentSpec{
+                               AppName:  appName,
+                               Resource: appName,
+                               Type:     "deployment",
+                       },
+               }
+
+               url := h.ovnURL + "/network-controller-intent/" + genericAppIntentName + "/workload-intents"
+               jsonLoad, _ := json.Marshal(wlIntent)
+               status, err := orch.apiPost(jsonLoad, url, intentName)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               if status != 201 {
+                       return status
+               }
+               fmt.Printf("Workload intent %s status %s %s\n", intentName, status, url)
+       }
+
+       // Add interfaces for to each application
+       for _, value := range intentData.Spec.Apps {
+               interfaces := value.Clusters[0].SelectedClusters[0].Interfaces
+               for j := range interfaces {
+                       interfaceNum := strconv.Itoa(j)
+                       interfaceName := value.Metadata.Name + "_interface" + interfaceNum
+                       genericAppIntentName := intentData.CompositeAppName + "_nwctlint"
+                       workloadIntent := value.Metadata.Name + "_wnwlint"
+
+                       iface := NwInterface{
+                               Metadata: apiMetaData{
+                                       Name:        interfaceName,
+                                       Description: "NA",
+                                       UserData1:   "data 1",
+                                       UserData2:   "data2"},
+                               Spec: InterfaceSpec{
+                                       Interface:      "eth" + interfaceNum,
+                                       Name:           interfaces[j].NetworkName,
+                                       DefaultGateway: "false",
+                                       IPAddress:      interfaces[j].IP,
+                               },
+                       }
+
+                       url := h.ovnURL + "/network-controller-intent" + "/" + genericAppIntentName +
+                               "/workload-intents/" + workloadIntent + "/interfaces"
+                       jsonLoad, _ := json.Marshal(iface)
+                       status, err := orch.apiPost(jsonLoad, url, interfaceName)
+                       if err != nil {
+                               log.Fatalln(err)
+                       }
+                       if status != 201 {
+                               return status
+                       }
+                       fmt.Printf("interface %s status %s %s\n", interfaceName, status, url)
+               }
+       }
+
+       return nil
+}
+
+func (h *networkIntentHandler) getObject() (interface{}, interface{}) {
+       orch := h.orchInstance
+       retcode := 200
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               Dig := compositeAppValue.DigMap
+               for digName, digValue := range Dig {
+                       h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups/" + digName
+                       for nwintName, nwintValue := range digValue.NwintMap {
+                               var wrlintList []NetworkWlIntent
+                               wlurl := h.ovnURL + "/network-controller-intent/" + nwintName + "/workload-intents"
+                               retcode, retval, err := orch.apiGet(wlurl, orch.compositeAppName+"_getnwwlint")
+                               fmt.Printf("Get Wrkld intents in Composite app %s dig %s nw intent %s status %d\n",
+                                       orch.compositeAppName, digName, nwintName, retcode)
+                               if err != nil {
+                                       fmt.Printf("Failed to read nw  workload int")
+                                       return nil, 500
+                               }
+                               if retcode != 200 {
+                                       fmt.Printf("Failed to read nw  workload int")
+                                       return nil, retcode
+                               }
+                               json.Unmarshal(retval, &wrlintList)
+                               nwintValue.WrkintMap = make(map[string]*WrkintData, len(wrlintList))
+                               for _, wrlIntValue := range wrlintList {
+                                       var WrkintDataInstance WrkintData
+                                       WrkintDataInstance.Wrkint = wrlIntValue
+
+                                       var ifaceList []NwInterface
+                                       ifaceurl := h.ovnURL + "/network-controller-intent/" + nwintName +
+                                               "/workload-intents/" + wrlIntValue.Metadata.Name + "/interfaces"
+                                       retcode, retval, err := orch.apiGet(ifaceurl, orch.compositeAppName+"_getnwiface")
+                                       fmt.Printf("Get interface in Composite app %s dig %s nw intent %s wrkld intent %s status %d\n",
+                                               orch.compositeAppName, digName, nwintName, wrlIntValue.Metadata.Name, retcode)
+                                       if err != nil {
+                                               fmt.Printf("Failed to read nw interface")
+                                               return nil, 500
+                                       }
+                                       if retcode != 200 {
+                                               fmt.Printf("Failed to read nw interface")
+                                               return nil, retcode
+                                       }
+                                       json.Unmarshal(retval, &ifaceList)
+                                       WrkintDataInstance.Interfaces = ifaceList
+                                       nwintValue.WrkintMap[wrlIntValue.Metadata.Name] = &WrkintDataInstance
+                               }
+                       }
+               }
+       }
+       return nil, retcode
+}
+
+func (h *networkIntentHandler) getAnchor() (interface{}, interface{}) {
+       orch := h.orchInstance
+       retcode := 200
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               Dig := compositeAppValue.DigMap
+               for digName, digValue := range Dig {
+                       h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups/" + digName
+                       var nwintList []NetworkCtlIntent
+
+                       url := h.ovnURL + "/network-controller-intent"
+                       retcode, retval, err := orch.apiGet(url, orch.compositeAppName+"_getnwint")
+                       fmt.Printf("Get Network Ctl intent in Composite app %s dig %s status %d\n",
+                               orch.compositeAppName, digName, retcode)
+                       if err != nil {
+                               fmt.Printf("Failed to read nw int %s\n", err)
+                               return nil, 500
+                       }
+                       if retcode != 200 {
+                               fmt.Printf("Failed to read nw int")
+                               return nil, retcode
+                       }
+                       json.Unmarshal(retval, &nwintList)
+                       digValue.NwintMap = make(map[string]*NwintData, len(nwintList))
+                       for _, nwIntValue := range nwintList {
+                               var NwintDataInstance NwintData
+                               NwintDataInstance.Nwint = nwIntValue
+                               digValue.NwintMap[nwIntValue.Metadata.Name] = &NwintDataInstance
+                       }
+               }
+       }
+       return nil, retcode
+}
+
+func (h *networkIntentHandler) deleteObject() interface{} {
+       orch := h.orchInstance
+       retcode := 200
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               Dig := compositeAppValue.DigMap
+               for digName, digValue := range Dig {
+                       h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups/" + digName
+
+                       for nwintName, nwintValue := range digValue.NwintMap {
+                               for wrkintName, wrkintValue := range nwintValue.WrkintMap {
+                                       // Delete the interfaces per workload intent.
+                                       for _, value := range wrkintValue.Interfaces {
+                                               url := h.ovnURL + "network-controller-intent/" + nwintName + "/workload-intents/" +
+                                                       wrkintName + "/interfaces/" + value.Spec.Name
+                                               fmt.Printf("Delete app nw interface %s\n", url)
+                                               retcode, err := orch.apiDel(url, orch.compositeAppName+"_delnwinterface")
+                                               if err != nil {
+                                                       return err
+                                               }
+                                               if retcode != 204 {
+                                                       return retcode
+                                               }
+                                               fmt.Printf("Delete nw interface resp %s\n", retcode)
+                                       }
+                                       // Delete the workload intents.
+                                       url := h.ovnURL + "network-controller-intent/" + nwintName + "/workload-intents/" + wrkintName
+                                       fmt.Printf("Delete app nw wl intent %s\n", url)
+                                       retcode, err := orch.apiDel(url, orch.compositeAppName+"_delnwwrkintent")
+                                       if err != nil {
+                                               return err
+                                       }
+                                       if retcode != 204 {
+                                               return retcode
+                                       }
+                                       fmt.Printf("Delete nw wl intent resp %s\n", retcode)
+                               } // For workload intents in network controller intent.
+                       } // For network controller intents in Dig.
+               } // For Dig.
+       } // For composite app.
+       return retcode
+}
+
+func (h networkIntentHandler) deleteAnchor() interface{} {
+       orch := h.orchInstance
+       retcode := 200
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               Dig := compositeAppValue.DigMap
+               for digName, digValue := range Dig {
+                       h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" +
+                               orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                               "/" + compositeAppSpec.Version +
+                               "/deployment-intent-groups/" + digName
+                       for nwintName, _ := range digValue.NwintMap {
+                               // loop through all app intens in the gpint
+                               url := h.ovnURL + "/network-controller-intent/" + nwintName
+                               fmt.Printf("Delete app nw controller intent %s\n", url)
+                               retcode, err := orch.apiDel(url, compositeAppMetadata.Name+"_delnwctlintent")
+                               if err != nil {
+                                       return err
+                               }
+                               if retcode != 204 {
+                                       return retcode
+                               }
+                               fmt.Printf("Delete nw controller intent %s\n", retcode)
+                       }
+               }
+       }
+       return retcode
+}
+
+func addNetworkIntent(I orchWorkflow) interface{} {
+       //1. Add network controller Intent
+       err := I.createAnchor()
+       if err != nil {
+               return err
+       }
+
+       //2. Add network workload intent
+       err = I.createObject()
+       if err != nil {
+               return err
+       }
+
+       return nil
+}
+
+func delNwintData(I orchWorkflow) interface{} {
+       // 1. Create the Anchor point
+       err := I.deleteObject()
+       if err != nil {
+               return err
+       }
+       // 2. Create the Objects
+       err = I.deleteAnchor()
+       if err != nil {
+               return err
+       }
+       return nil
+}
diff --git a/src/tools/emcoui/middle_end/app/profile.go b/src/tools/emcoui/middle_end/app/profile.go
new file mode 100644 (file)
index 0000000..fff43cd
--- /dev/null
@@ -0,0 +1,261 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package app
+
+import (
+       "encoding/json"
+       "fmt"
+       "log"
+)
+
+// ProfileData captures per app profile
+type ProfileData struct {
+       Name        string            `json:"profileName"`
+       AppProfiles map[string]string `json:"appProfile"`
+}
+
+// ProfileMeta is metadta for the profile APIs
+type ProfileMeta struct {
+       Metadata apiMetaData `json:"metadata"`
+       Spec     ProfileSpec `json:"spec"`
+}
+
+// ProfileSpec is the spec for the profile APIs
+type ProfileSpec struct {
+       AppName string `json:"app-name"`
+}
+
+// ProfileHandler This implements the orchworkflow interface
+type ProfileHandler struct {
+       orchURL      string
+       orchInstance *OrchestrationHandler
+       response     struct {
+               payload map[string][]byte
+               status  map[string]string
+       }
+}
+
+func (h *ProfileHandler) getObject() (interface{}, interface{}) {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       retcode := 200
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               var profileList []ProfileMeta
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version + "/composite-profiles"
+               for profileName, profileValue := range compositeAppValue.ProfileDataArray {
+                       url := h.orchURL + "/" + profileName + "/profiles"
+                       retcode, respval, err := orch.apiGet(url, compositeAppMetadata.Name+"_getprofiles")
+                       fmt.Printf("Get app profiles status %d\n", retcode)
+                       if err != nil {
+                               fmt.Printf("Failed to read profile %s\n", profileName)
+                               return nil, 500
+                       }
+                       if retcode != 200 {
+                               fmt.Printf("Failed to read profile %s\n", profileName)
+                               return nil, retcode
+                       }
+                       json.Unmarshal(respval, &profileList)
+                       profileValue.AppProfiles = make([]ProfileMeta, len(profileList))
+                       for appProfileIndex, appProfile := range profileList {
+                               profileValue.AppProfiles[appProfileIndex] = appProfile
+                       }
+               }
+       }
+       return nil, retcode
+}
+
+func (h *ProfileHandler) getAnchor() (interface{}, interface{}) {
+       orch := h.orchInstance
+       respcode := 200
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               var profilemetaList []ProfileMeta
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version + "/composite-profiles"
+
+               respcode, respdata, err := orch.apiGet(h.orchURL, compositeAppMetadata.Name+"_getcprofile")
+               if err != nil {
+                       fmt.Printf("Failed to get composite profiles\n")
+                       return nil, 500
+               }
+               if respcode != 200 {
+                       fmt.Printf("composite profile GET status %d\n", respcode)
+                       return nil, respcode
+               }
+               json.Unmarshal(respdata, &profilemetaList)
+               compositeAppValue.ProfileDataArray = make(map[string]*ProfilesData, len(profilemetaList))
+               for _, value := range profilemetaList {
+                       ProfilesDataInstance := ProfilesData{}
+                       ProfilesDataInstance.Profile = value
+                       compositeAppValue.ProfileDataArray[value.Metadata.Name] = &ProfilesDataInstance
+               }
+       }
+       return nil, respcode
+}
+
+func (h *ProfileHandler) deleteObject() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version + "/composite-profiles/"
+               for profileName, profileValue := range compositeAppValue.ProfileDataArray {
+                       for _, appProfileValue := range profileValue.AppProfiles {
+                               url := h.orchURL + profileName + "/profiles/" + appProfileValue.Metadata.Name
+
+                               fmt.Printf("Delete app profiles %s\n", url)
+                               resp, err := orch.apiDel(url, compositeAppMetadata.Name+"_delappProfiles")
+                               if err != nil {
+                                       return err
+                               }
+                               if resp != 204 {
+                                       return resp
+                               }
+                               fmt.Printf("Delete profiles status %s\n", resp)
+                       }
+               }
+       }
+       return nil
+}
+
+func (h *ProfileHandler) deleteAnchor() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       for _, compositeAppValue := range dataRead.compositeAppMap {
+               compositeAppMetadata := compositeAppValue.Metadata.Metadata
+               compositeAppSpec := compositeAppValue.Metadata.Spec
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + compositeAppMetadata.Name +
+                       "/" + compositeAppSpec.Version + "/composite-profiles/"
+
+               for profileName, _ := range compositeAppValue.ProfileDataArray {
+                       url := h.orchURL + profileName
+                       fmt.Printf("Delete profile %s\n", url)
+                       resp, err := orch.apiDel(url, compositeAppMetadata.Name+"_delProfile")
+                       if err != nil {
+                               return err
+                       }
+                       if resp != 204 {
+                               return resp
+                       }
+                       fmt.Printf("Delete profile status %s\n", resp)
+               }
+       }
+       return nil
+}
+
+func (h *ProfileHandler) createAnchor() interface{} {
+       orch := h.orchInstance
+
+       profileCreate := ProfileMeta{
+               Metadata: apiMetaData{
+                       Name:        orch.compositeAppName + "_profile",
+                       Description: "Profile created from middleend",
+                       UserData1:   "data 1",
+                       UserData2:   "data2"},
+       }
+       jsonLoad, _ := json.Marshal(profileCreate)
+       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+               orch.projectName + "/composite-apps"
+       url := h.orchURL + "/" + orch.compositeAppName + "/" + "v1" + "/composite-profiles"
+       resp, err := orch.apiPost(jsonLoad, url, orch.compositeAppName+"_profile")
+       if err != nil {
+               return err
+       }
+       if resp != 201 {
+               return resp
+       }
+       fmt.Printf("ProfileHandler resp %s\n", resp)
+
+       return nil
+}
+
+func (h *ProfileHandler) createObject() interface{} {
+       orch := h.orchInstance
+
+       for i := range orch.meta {
+               fileName := orch.meta[i].ProfileMetadata.FileName
+               appName := orch.meta[i].Metadata.Name
+               profileName := orch.meta[i].Metadata.Name + "_profile"
+
+               // Upload the application helm chart
+               fh := orch.file[fileName]
+               profileAdd := ProfileMeta{
+                       Metadata: apiMetaData{
+                               Name:        profileName,
+                               Description: "NA",
+                               UserData1:   "data 1",
+                               UserData2:   "data2"},
+                       Spec: ProfileSpec{
+                               AppName: appName},
+               }
+               compositeProfilename := orch.compositeAppName + "_profile"
+
+               url := h.orchURL + "/" + orch.compositeAppName + "/" + "v1" + "/" +
+                       "composite-profiles" + "/" + compositeProfilename + "/profiles"
+               jsonLoad, _ := json.Marshal(profileAdd)
+               status, err := orch.apiPostMultipart(jsonLoad, fh, url, profileName, fileName)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               if status != 201 {
+                       return status
+               }
+               fmt.Printf("CompositeProfile Profile  %s status %s %s\n", profileName, status, url)
+       }
+
+       return nil
+}
+
+func createProfile(I orchWorkflow) interface{} {
+       // 1. Create the Anchor point
+       err := I.createAnchor()
+       if err != nil {
+               return err
+       }
+       // 2. Create the Objects
+       err = I.createObject()
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
+func delProfileData(I orchWorkflow) interface{} {
+       // 1. Delete the object
+       err := I.deleteObject()
+       if err != nil {
+               return err
+       }
+       // 2. Delete the Anchor
+       err = I.deleteAnchor()
+       if err != nil {
+               return err
+       }
+       return nil
+}
diff --git a/src/tools/emcoui/middle_end/app/projects.go b/src/tools/emcoui/middle_end/app/projects.go
new file mode 100644 (file)
index 0000000..39fe757
--- /dev/null
@@ -0,0 +1,187 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package app
+
+import (
+       "encoding/json"
+       "fmt"
+)
+
+// CompositeApp application structure
+type ProjectMetadata struct {
+       Metadata apiMetaData `json:"metadata"`
+}
+
+// CompAppHandler , This implements the orchworkflow interface
+type projectHandler struct {
+       orchURL      string
+       orchInstance *OrchestrationHandler
+}
+
+func (h *projectHandler) getObject() (interface{}, interface{}) {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       var cappList []CompositeApp
+       if orch.treeFilter != nil {
+               temp:=CompositeApp{}
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps/" + orch.treeFilter.compositeAppName+"/"+
+                       orch.treeFilter.compositeAppVersion
+               respcode, respdata, err := orch.apiGet(h.orchURL, orch.projectName+"_getcapps")
+               fmt.Printf("Get capp status %s\n", respcode)
+               if err != nil {
+                       return nil, 500
+               }
+               if respcode != 200 {
+                       return nil, respcode
+               }
+               fmt.Printf("Get capp status %s\n", respcode)
+               json.Unmarshal(respdata, &temp)
+               cappList = append(cappList, temp)
+       } else {
+               h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+                       orch.projectName + "/composite-apps"
+               respcode, respdata, err := orch.apiGet(h.orchURL, orch.projectName+"_getcapps")
+               fmt.Printf("Get capp status %s\n", respcode)
+               if err != nil {
+                       return nil, 500
+               }
+               if respcode != 200 {
+                       return nil, respcode
+               }
+               fmt.Printf("Get capp status %s\n", respcode)
+               json.Unmarshal(respdata, &cappList)
+       }
+
+       dataRead.compositeAppMap = make(map[string]*CompositeAppTree, len(cappList))
+       for k, value := range cappList {
+               fmt.Printf("%+v", cappList[k])
+               var cappsDataInstance CompositeAppTree
+               cappName := value.Metadata.Name
+               cappsDataInstance.Metadata = value
+               dataRead.compositeAppMap[cappName] = &cappsDataInstance
+       }
+       return nil, 200 
+}
+
+func (h *projectHandler) getAnchor() (interface{}, interface{}) {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+               orch.projectName
+
+       respcode, respdata, err := orch.apiGet(h.orchURL, orch.projectName+"_getProject")
+       if err != nil {
+               return nil, 500
+       }
+       if respcode != 200 {
+               return nil, respcode
+       }
+       fmt.Printf("Get project %s\n", respcode)
+       json.Unmarshal(respdata, &dataRead.Metadata)
+       return nil, respcode
+}
+
+func (h *projectHandler) deleteObject() interface{} {
+       orch := h.orchInstance
+       dataRead := h.orchInstance.dataRead
+       cappList := dataRead.compositeAppMap
+       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" +
+               orch.projectName + "/composite-apps"
+       for compositeAppName, compositeAppValue := range cappList {
+               url := h.orchURL + "/" + compositeAppName + "/" + compositeAppValue.Metadata.Spec.Version
+               fmt.Printf("Delete composite app %s\n", url)
+               resp, err := orch.apiDel(url, compositeAppName+"_delcapp")
+               if err != nil {
+                       return err
+               }
+               if resp != 204 {
+                       return resp
+               }
+               fmt.Printf("Delete composite app status %s\n", resp)
+       }
+       return nil
+}
+
+func (h *projectHandler) deleteAnchor() interface{} {
+       orch := h.orchInstance
+       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + orch.projectName
+       fmt.Printf("Delete Project %s \n", h.orchURL)
+       resp, err := orch.apiDel(h.orchURL, orch.projectName+"_delProject")
+       if err != nil {
+               return err
+       }
+       if resp != 204 {
+               return resp
+       }
+       fmt.Printf("Delete Project status %s\n", resp)
+       return nil
+}
+
+func (h *projectHandler) createAnchor() interface{} {
+       orch := h.orchInstance
+
+       projectCreate := ProjectMetadata{
+               Metadata: apiMetaData{
+                       Name:        orch.projectName,
+                       Description: orch.projectDesc,
+                       UserData1:   "data 1",
+                       UserData2:   "data 2"},
+       }
+
+       jsonLoad, _ := json.Marshal(projectCreate)
+       h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + orch.projectName
+       resp, err := orch.apiPost(jsonLoad, h.orchURL, orch.projectName)
+       if err != nil {
+               return err
+       }
+       if resp != 201 {
+               return resp
+       }
+       orch.version = "v1"
+       fmt.Printf("projectHandler resp %s\n", resp)
+
+       return nil
+}
+
+func (h *projectHandler) createObject() interface{} {
+       return nil
+}
+
+func createProject(I orchWorkflow) interface{} {
+       // 1. Create the Anchor point
+       err := I.createAnchor()
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
+func delProject(I orchWorkflow) interface{} {
+       // 1. Delete the object
+       err := I.deleteObject()
+       if err != nil {
+               return err
+       }
+       // 2. Delete the Anchor
+       err = I.deleteAnchor()
+       if err != nil {
+               return err
+       }
+       return nil
+}
diff --git a/src/tools/emcoui/middle_end/authproxy/README.md b/src/tools/emcoui/middle_end/authproxy/README.md
new file mode 100644 (file)
index 0000000..1d68a43
--- /dev/null
@@ -0,0 +1,16 @@
+
+Authproxy is part of middleend and it exposes following 3 apis
+1. **/v1/login**
+   - Redirects user to keycloak login page.
+   - Sets a cookie with original URL
+2. **/v1/callback**
+   - After successful login gets auth code and exchange it for token.
+   - Set id_token and access_token in cookie and redirects to original URL
+3. **/v1/auth**
+   - Retrieve idtoken from cookie and verifies the JWT.
+   - If id_token is valid then access to resources else redirects to login page.
+
+Required inputs of authproxy comes from authproxy section of helm config
+- Issuer
+- Redirect URI
+- Client id
diff --git a/src/tools/emcoui/middle_end/authproxy/authproxy.go b/src/tools/emcoui/middle_end/authproxy/authproxy.go
new file mode 100644 (file)
index 0000000..78819ef
--- /dev/null
@@ -0,0 +1,281 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package authproxy
+
+import (
+       "encoding/json"
+       "fmt"
+       "io/ioutil"
+       "log"
+       "net/http"
+       "net/url"
+       "strings"
+
+       "github.com/dgrijalva/jwt-go"
+)
+
+type AuthProxy struct {
+       AuthProxyConf AuthProxyConfig
+}
+
+// AuthProxyConfig holds inputs of authproxy
+type AuthProxyConfig struct {
+       Issuer      string `json:"issuer"`
+       RedirectURI string `json:"redirect_uri"`
+       ClientID    string `json:"client_id"`
+}
+
+// NewAppHandler interface implementing REST callhandler
+func NewAppHandler() *AuthProxy {
+       return &AuthProxy{}
+}
+
+// OpenIDConfiguration struct to map response from OIDC
+type OpenIDConfiguration struct {
+       Issuer                string `json:"issuer"`
+       AuthzEndpoint         string `json:"authorization_endpoint"`
+       TokenEndPoint         string `json:"token_endpoint"`
+       IntrospectionEndpoint string `json:"introspection_endpoint"`
+       JWKSURI               string `json:"jwks_uri"`
+}
+
+// TokenConfig struct holds tokens
+type TokenConfig struct {
+       ACCESSTOKEN string `json:"access_token"`
+       IDTOKEN     string `json:"id_token"`
+}
+
+// RealmConfig struct holds public_key of issuer
+type RealmConfig struct {
+       PublicKey string `json:"public_key"`
+}
+
+var openIDConfig *OpenIDConfiguration
+var realmConfig *RealmConfig
+
+// Loads the openIDconfig from the OIDC only once
+func getOpenIDConfig(issuer string) OpenIDConfiguration {
+       if openIDConfig != nil {
+               log.Println("openidconfig is not null and returning the cached value")
+               return *openIDConfig
+       }
+       log.Println("openidconfig is null and loading the values")
+       url := issuer + ".well-known/openid-configuration"
+       response, err := http.Get(url)
+       if err != nil {
+               log.Printf("The openidconfig HTTP request failed with error %s", err)
+               return *openIDConfig
+       }
+
+       defer response.Body.Close()
+       bodyBytes, _ := ioutil.ReadAll(response.Body)
+       json.Unmarshal(bodyBytes, &openIDConfig)
+       return *openIDConfig
+}
+
+// LoginHandler redirects to client login page and sets cookie with the original path
+func (h AuthProxy) LoginHandler(w http.ResponseWriter, r *http.Request) {
+       log.Println("LoginHandler start")
+
+       rd := r.FormValue("rd")
+       scope := r.FormValue("scope")
+
+       log.Printf("[LoginHandler] url Param 'rd' is: %s, 'scope' is: %s\n", string(rd), string(scope))
+       redirect := r.Header.Get("X-Auth-Request-Redirect")
+       log.Println("redirect url from HEADER is: " + redirect)
+       if len(redirect) == 0 {
+               redirect = rd
+       }
+
+       cookie := http.Cookie{
+               Name:     "org",
+               Value:    redirect,
+               Path:     "/",
+               Domain:   "",
+               Secure:   false,
+               HttpOnly: false,
+       }
+       // Set cookie with original URL
+       http.SetCookie(w, &cookie)
+       state := "1234" // Optional parameter included in all login redirects
+       if len(scope) == 0 {
+               // generate token with offline_access scope so that it can be stored in cookie and reused
+               scope = "openid offline_access"
+       }
+
+       // get authorization endpoint from function openidconfig
+       authzEndpoint := getOpenIDConfig(h.AuthProxyConf.Issuer).AuthzEndpoint
+
+       // Construct redirect URL with params
+       u, _ := url.Parse(authzEndpoint)
+       q := u.Query()
+       q.Add("client_id", h.AuthProxyConf.ClientID)
+       // h.AuthProxyConf.RedirectURI is the callback endpoint of middleend.
+       // after successful authentication, url will be redirected to this one
+       q.Add("redirect_uri", h.AuthProxyConf.RedirectURI)
+       q.Add("response_type", "code")
+       q.Add("scope", scope)
+       q.Add("state", state)
+       u.RawQuery = q.Encode()
+
+       log.Println("[LoginHandler] Redireced URL -> " + u.String())
+       http.Redirect(w, r, u.String(), http.StatusFound)
+}
+
+/*
+ * CallbackHandler reads the OIDC config
+ * Gets token with API and sets id and access tokens in cookies
+ * Redirects to original URL
+ */
+func (h AuthProxy) CallbackHandler(w http.ResponseWriter, r *http.Request) {
+       state := r.FormValue("state")
+       code := r.FormValue("code")
+       tokenEndpoint := getOpenIDConfig(h.AuthProxyConf.Issuer).TokenEndPoint
+       log.Printf("[CallbackHandler] state: %s , code: %s , tokenEndpoint: %s \n", state, code, tokenEndpoint)
+
+       client := http.Client{}
+       form := url.Values{}
+       form.Add("client_id", h.AuthProxyConf.ClientID)
+       form.Add("client_secret", "")
+       form.Add("grant_type", "authorization_code")
+       form.Add("code", code)
+       form.Add("redirect_uri", h.AuthProxyConf.RedirectURI)
+       request, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(form.Encode()))
+       request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+       resp, err := client.Do(request)
+       if err != nil {
+               log.Printf("[CallbackHandler] HTTP request to %s failed\n", tokenEndpoint)
+               log.Println(err)
+               w.WriteHeader(http.StatusInternalServerError)
+               return
+       }
+
+       defer resp.Body.Close()
+       body, err := ioutil.ReadAll(resp.Body)
+       if err != nil {
+               log.Println("[CallbackHandler] Error while reading response from tokenEndpoint")
+               log.Println(err)
+               w.WriteHeader(http.StatusInternalServerError)
+               return
+       }
+
+       var tokenConfig TokenConfig
+       json.Unmarshal(body, &tokenConfig)
+       log.Printf("[CallbackHandler] access_token: %s \n id_token: %s\n", tokenConfig.ACCESSTOKEN, tokenConfig.IDTOKEN)
+
+       // Construct the original URL with cookie org
+       var orginalURL string
+       cookie, err := r.Cookie("org")
+       if err == nil {
+               orginalURL = cookie.Value
+       }
+       fmt.Println("[CallbackHandler] orginalURL from cookie: " + orginalURL)
+
+       // Create cookies with id_token, access_token
+       idTokenCookie := http.Cookie{
+               Name:     "idtoken",
+               Value:    tokenConfig.IDTOKEN,
+               Path:     "/",
+               Domain:   "",
+               Secure:   false,
+               HttpOnly: false,
+       }
+       accessTokencookie := http.Cookie{
+               Name:     "accesstoken",
+               Value:    tokenConfig.ACCESSTOKEN,
+               Path:     "/",
+               Domain:   "",
+               Secure:   false,
+               HttpOnly: false,
+       }
+
+       http.SetCookie(w, &idTokenCookie)
+       http.SetCookie(w, &accessTokencookie)
+
+       // Finally return the original URL with the cookies
+       http.Redirect(w, r, orginalURL, http.StatusFound)
+}
+
+// AuthHandler verifies the token and returns response
+func (h AuthProxy) AuthHandler(w http.ResponseWriter, r *http.Request) {
+       log.Println("[AuthHandler] Authenticating the token")
+
+       var idToken string
+       cookie, err := r.Cookie("idtoken")
+       if err == nil {
+               cookieVal := cookie.Value
+               idToken = cookieVal
+       }
+
+       if idToken == "" {
+               log.Println("[AuthHandler] id token is nil ")
+               w.WriteHeader(http.StatusUnauthorized)
+               return
+       }
+       error := validateToken(h.AuthProxyConf.Issuer, idToken)
+       if error != nil {
+               log.Println("[AuthHandler] Issue with token and returning failed response")
+               w.WriteHeader(http.StatusUnauthorized)
+       }
+}
+
+/*
+* Validates JWT token
+* verifies signature, token expiry and invalid check... etc
+ */
+func validateToken(issuer string, reqToken string) error {
+       log.Printf("[AuthHandler] Validating JWT token: \n%s\n", reqToken)
+
+       //load realm public key only once
+       if realmConfig == nil {
+               log.Println("[AuthHandler] realmconfig is null and loading the value")
+               response, err := http.Get(issuer)
+               if err != nil {
+                       log.Printf("[AuthHandler] Error while retreiving issuer details : %s\n", err)
+                       return err
+               }
+               defer response.Body.Close()
+               bodyBytes, _ := ioutil.ReadAll(response.Body)
+               json.Unmarshal(bodyBytes, &realmConfig)
+       }
+       SecretKey := "-----BEGIN CERTIFICATE-----\n" + realmConfig.PublicKey + "\n-----END CERTIFICATE-----"
+       key, er := jwt.ParseRSAPublicKeyFromPEM([]byte(SecretKey))
+       if er != nil {
+               log.Println("[AuthHandler] Error occured while parsing public key")
+               log.Println(er)
+               return er
+       }
+
+       token, err := jwt.Parse(reqToken, func(token *jwt.Token) (interface{}, error) {
+               if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
+                       return nil, fmt.Errorf("[AuthHandler] Unexpected signing method: %v", token.Header["alg"])
+               }
+               return key, nil
+       })
+
+       if err != nil {
+               log.Println("[AuthHandler] Error while parsing token")
+               log.Println(err)
+               return err
+       }
+       if _, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
+               log.Println("[AuthHandler] Token is valid")
+       }
+       return nil
+}
diff --git a/src/tools/emcoui/middle_end/db/dbconnection.go b/src/tools/emcoui/middle_end/db/dbconnection.go
new file mode 100644 (file)
index 0000000..5496c39
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package db
+
+import (
+       "encoding/json"
+       "fmt"
+       "go.mongodb.org/mongo-driver/bson"
+       "go.mongodb.org/mongo-driver/mongo"
+       "go.mongodb.org/mongo-driver/mongo/options"
+       "golang.org/x/net/context"
+)
+
+// MongoStore is the interface which implements the db.Store interface
+type MongoStore struct {
+       db *mongo.Database
+}
+
+// Key interface
+type Key interface {
+}
+
+// DBconn variable of type Store
+var DBconn Store
+
+// Store Interface which implements the data store functions
+type Store interface {
+       HealthCheck() error
+       Find(coll string, key []byte, tag string) ([][]byte, error)
+       Unmarshal(inp []byte, out interface{}) error
+}
+
+// NewMongoStore Return mongo client
+func NewMongoStore(name string, store *mongo.Database, svcEp string) (Store, error) {
+       if store == nil {
+               ip := "mongodb://" + svcEp
+               clientOptions := options.Client()
+               clientOptions.ApplyURI(ip)
+               mongoClient, err := mongo.NewClient(clientOptions)
+               if err != nil {
+                       return nil, err
+               }
+
+               err = mongoClient.Connect(context.Background())
+               if err != nil {
+                       return nil, err
+               }
+               store = mongoClient.Database(name)
+       }
+       return &MongoStore{
+               db: store,
+       }, nil
+}
+
+// CreateDBClient creates the DB client. currently only mongo
+func CreateDBClient(dbType string, dbName string, svcEp string) error {
+       var err error
+       switch dbType {
+       case "mongo":
+               DBconn, err = NewMongoStore(dbName, nil, svcEp)
+       default:
+               fmt.Println(dbType + "DB not supported")
+       }
+       return err
+}
+
+// HealthCheck verifies the database connection
+func (m *MongoStore) HealthCheck() error {
+       _, err := (*mongo.SingleResult).DecodeBytes(m.db.RunCommand(context.Background(), bson.D{{"serverStatus", 1}}))
+       if err != nil {
+               fmt.Println("Error getting DB server status: err %s", err)
+       }
+       return nil
+}
+
+func (m *MongoStore) Unmarshal(inp []byte, out interface{}) error {
+       err := bson.Unmarshal(inp, out)
+       if err != nil {
+               fmt.Printf("Failed to unmarshall bson")
+               return err
+       }
+       return nil
+}
+
+// Find a document
+func (m *MongoStore) Find(coll string, key []byte, tag string) ([][]byte, error) {
+       var bsonMap bson.M
+       err := json.Unmarshal([]byte(key), &bsonMap)
+       if err != nil {
+               fmt.Println("Failed to unmarshall %s\n", key)
+               return nil, err
+       }
+
+       filter := bson.M{
+               "$and": []bson.M{bsonMap},
+       }
+
+       fmt.Printf("%+v %s\n", filter, tag)
+       projection := bson.D{
+               {tag, 1},
+               {"_id", 0},
+       }
+
+       c := m.db.Collection(coll)
+
+       cursor, err := c.Find(context.Background(), filter, options.Find().SetProjection(projection))
+       if err != nil {
+               fmt.Println("Failed to find the document %s\n", err)
+               return nil, err
+       }
+
+       defer cursor.Close(context.Background())
+       var data []byte
+       var result [][]byte
+       for cursor.Next(context.Background()) {
+               d := cursor.Current
+               switch d.Lookup(tag).Type {
+               case bson.TypeString:
+                       data = []byte(d.Lookup(tag).StringValue())
+               default:
+                       r, err := d.LookupErr(tag)
+                       if err != nil {
+                               fmt.Println("Unable to read data %s %s\n", string(r.Value), err)
+                       }
+                       data = r.Value
+               }
+               result = append(result, data)
+       }
+       return result, nil
+}
diff --git a/src/tools/emcoui/middle_end/go.mod b/src/tools/emcoui/middle_end/go.mod
new file mode 100644 (file)
index 0000000..3d19597
--- /dev/null
@@ -0,0 +1,14 @@
+module example.com/middleend
+
+go 1.14
+
+require (
+       github.com/dgrijalva/jwt-go v3.2.0+incompatible
+       github.com/gorilla/handlers v1.5.0
+       github.com/gorilla/mux v1.8.0
+       github.com/lestrrat-go/jwx v1.0.5
+       go.mongodb.org/mongo-driver v1.4.1
+       golang.org/x/net v0.0.0-20200707034311-ab3426394381
+       k8s.io/apimachinery v0.19.3
+       k8s.io/client-go v0.19.3
+)
diff --git a/src/tools/emcoui/middle_end/go.sum b/src/tools/emcoui/middle_end/go.sum
new file mode 100644 (file)
index 0000000..d8e5a43
--- /dev/null
@@ -0,0 +1,428 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
+github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
+github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/aws/aws-sdk-go v1.29.15 h1:0ms/213murpsujhsnxnNKNeVouW60aJqSd992Ks3mxs=
+github.com/aws/aws-sdk-go v1.29.15/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
+github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
+github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
+github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
+github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
+github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
+github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
+github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
+github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
+github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
+github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
+github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
+github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
+github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
+github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
+github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
+github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
+github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
+github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
+github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
+github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
+github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
+github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/gorilla/handlers v1.5.0 h1:4wjo3sf9azi99c8hTmyaxp9y5S+pFszsy3pP0rAw/lw=
+github.com/gorilla/handlers v1.5.0/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
+github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M=
+github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY=
+github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
+github.com/lestrrat-go/jwx v1.0.5 h1:8bVUGXXkR3+YQNwuFof3lLxSJMLtrscHJfGI6ZIBRD0=
+github.com/lestrrat-go/jwx v1.0.5/go.mod h1:TPF17WiSFegZo+c20fdpw49QD+/7n4/IsGvEmCSWwT0=
+github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8=
+github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
+github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
+github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc h1:n+nNi93yXLkJvKwXNP9d55HC7lGK4H/SRcwB5IaUZLo=
+github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.mongodb.org/mongo-driver v1.4.1 h1:38NSAyDPagwnFpUA/D5SFgbugUYR3NzYRNa4Qk9UxKs=
+go.mongodb.org/mongo-driver v1.4.1/go.mod h1:llVBH2pkj9HywK0Dtdt6lDikOjFLbceHVu/Rc0iMKLs=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
+golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
+golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+k8s.io/api v0.19.3 h1:GN6ntFnv44Vptj/b+OnMW7FmzkpDoIDLZRvKX3XH9aU=
+k8s.io/api v0.19.3/go.mod h1:VF+5FT1B74Pw3KxMdKyinLo+zynBaMBiAfGMuldcNDs=
+k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc=
+k8s.io/apimachinery v0.19.3/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
+k8s.io/client-go v0.19.3 h1:ctqR1nQ52NUs6LpI0w+a5U+xjYwflFwA13OJKcicMxg=
+k8s.io/client-go v0.19.3/go.mod h1:+eEMktZM+MG0KO+PTkci8xnbCZHvj9TqR6Q1XDUIJOM=
+k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A=
+k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
+k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg=
+k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/src/tools/emcoui/middle_end/main/main.go b/src/tools/emcoui/middle_end/main/main.go
new file mode 100644 (file)
index 0000000..97a3101
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+=======================================================================
+Copyright (c) 2017-2020 Aarna Networks, Inc.
+All rights reserved.
+======================================================================
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+========================================================================
+*/
+
+package main
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "io/ioutil"
+       "log"
+       "net/http"
+       "os"
+       "os/signal"
+       "time"
+
+       "example.com/middleend/app"
+       "example.com/middleend/authproxy"
+       "example.com/middleend/db"
+       "github.com/gorilla/handlers"
+       "github.com/gorilla/mux"
+)
+
+/* This is the main package of the middleend. This package
+ * implements the http server which exposes service ar 9891.
+ * It also intialises an API router which handles the APIs with
+ * subpath /v1.
+ */
+func main() {
+       depHandler := app.NewAppHandler()
+       authProxyHandler := authproxy.NewAppHandler()
+       configFile, err := os.Open("/opt/emco/config/middleend.conf")
+       if err != nil {
+               fmt.Printf("Failed to read middleend configuration")
+               return
+       }
+       defer configFile.Close()
+
+       // Read the configuration json
+       byteValue, _ := ioutil.ReadAll(configFile)
+       json.Unmarshal(byteValue, &depHandler.MiddleendConf)
+       json.Unmarshal(byteValue, &authProxyHandler.AuthProxyConf)
+
+       // Connect to the DB
+       err = db.CreateDBClient("mongo", "mco", depHandler.MiddleendConf.Mongo)
+       if err != nil {
+               fmt.Println("Failed to connect to DB")
+               return
+       }
+       // Get an instance of the OrchestrationHandler, this type implements
+       // the APIs i.e CreateApp, ShowApp, DeleteApp.
+       httpRouter := mux.NewRouter().PathPrefix("/middleend").Subrouter()
+       loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter)
+       log.Println("Starting middle end service")
+
+       httpServer := &http.Server{
+               Handler:      loggedRouter,
+               Addr:         ":" + depHandler.MiddleendConf.OwnPort,
+               WriteTimeout: 15 * time.Second,
+               ReadTimeout:  15 * time.Second,
+       }
+       httpRouter.HandleFunc("/healthcheck", depHandler.GetHealth).Methods("GET")
+
+       // POST, GET, DELETE composite apps
+       httpRouter.HandleFunc("/projects/{project-name}/composite-apps", depHandler.CreateApp).Methods("POST")
+       //httpRouter.HandleFunc("/projects/{project-name}/composite-apps", depHandler.GetAllCaps).Methods("GET")
+       httpRouter.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}",
+               depHandler.GetSvc).Methods("GET")
+       httpRouter.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}",
+               depHandler.DelSvc).Methods("DELETE")
+       // POST, GET, DELETE deployment intent groups
+       httpRouter.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/deployment-intent-groups",
+               depHandler.CreateDig).Methods("POST")
+       httpRouter.HandleFunc("/projects/{project-name}/deployment-intent-groups", depHandler.GetAllDigs).Methods("GET")
+       httpRouter.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/deployment-intent-groups/{deployment-intent-group-name}",
+               depHandler.DelDig).Methods("DELETE")
+
+       // Authproxy relates APIs
+       httpRouter.HandleFunc("/login", authProxyHandler.LoginHandler).Methods("GET")
+       httpRouter.HandleFunc("/callback", authProxyHandler.CallbackHandler).Methods("GET")
+       httpRouter.HandleFunc("/auth", authProxyHandler.AuthHandler).Methods("GET")
+       // Cluster createion API
+       httpRouter.HandleFunc("/clusterproviders/{cluster-provider-name}/clusters", depHandler.CheckConnection).Methods("POST")
+
+       // Start server in a go routine.
+       go func() {
+               log.Fatal(httpServer.ListenAndServe())
+       }()
+
+       // Gracefull shutdown of the server,
+       // create a channel and wait for SIGINT
+       c := make(chan os.Signal, 1)
+       signal.Notify(c, os.Interrupt)
+       log.Println("wait for signal")
+       <-c
+       log.Println("Bye Bye")
+       httpServer.Shutdown(context.Background())
+}
index e9e57dc..f2ce78e 100644 (file)
@@ -1,3 +1,17 @@
+#=======================================================================
+# Copyright (c) 2017-2020 Aarna Networks, Inc.
+# All rights reserved.
+# ======================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#           http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================================================================
 # https://www.robotstxt.org/robotstxt.html
 User-agent: *
 Disallow:
index 2613ecf..3a2c5ff 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React from "react";
 import {
   BrowserRouter as Router,
@@ -53,7 +53,7 @@ function App() {
                 <Redirect
                   exact
                   from={`${match.path}`}
-                  to={`${match.path}/composite-apps`}
+                  to={`${match.path}/services`}
                 />
                 <Route
                   path={`${match.path}`}
index be07cba..9cee73b 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import PropTypes from "prop-types";
 import clsx from "clsx";
@@ -36,16 +36,16 @@ const categories = [
         icon: <AppsIcon />,
         url: "/projects",
       },
-      {
-        id: "Clusters",
-        icon: <DnsRoundedIcon />,
-        url: "/clusters",
-      },
       {
         id: "Controllers",
         icon: <SettingsIcon />,
         url: "/controllers",
       },
+      {
+        id: "Clusters",
+        icon: <DnsRoundedIcon />,
+        url: "/clusters",
+      },
     ],
   },
 ];
index 150a191..57ee755 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
+// ========================================================================
+import React from "react";
+import PropTypes from "prop-types";
+import { withStyles } from "@material-ui/core/styles";
+import Button from "@material-ui/core/Button";
 
-import Dialog from '@material-ui/core/Dialog';
-import MuiDialogTitle from '@material-ui/core/DialogTitle';
-import MuiDialogContent from '@material-ui/core/DialogContent';
-import MuiDialogActions from '@material-ui/core/DialogActions';
-import IconButton from '@material-ui/core/IconButton';
-import CloseIcon from '@material-ui/icons/Close';
-import Typography from '@material-ui/core/Typography';
-import { TextField } from '@material-ui/core';
+import Dialog from "@material-ui/core/Dialog";
+import MuiDialogTitle from "@material-ui/core/DialogTitle";
+import MuiDialogContent from "@material-ui/core/DialogContent";
+import MuiDialogActions from "@material-ui/core/DialogActions";
+import IconButton from "@material-ui/core/IconButton";
+import CloseIcon from "@material-ui/icons/Close";
+import Typography from "@material-ui/core/Typography";
+import { TextField } from "@material-ui/core";
 import * as Yup from "yup";
-import { Formik } from 'formik';
+import { Formik } from "formik";
 
 const styles = (theme) => ({
-    root: {
-        margin: 0,
-        padding: theme.spacing(2),
-    },
-    closeButton: {
-        position: 'absolute',
-        right: theme.spacing(1),
-        top: theme.spacing(1),
-        color: theme.palette.grey[500],
-    },
+  root: {
+    margin: 0,
+    padding: theme.spacing(2),
+  },
+  closeButton: {
+    position: "absolute",
+    right: theme.spacing(1),
+    top: theme.spacing(1),
+    color: theme.palette.grey[500],
+  },
 });
 
 const DialogTitle = withStyles(styles)((props) => {
-    const { children, classes, onClose, ...other } = props;
-    return (
-        <MuiDialogTitle disableTypography className={classes.root} {...other}>
-            <Typography variant="h6">{children}</Typography>
-            {onClose ? (
-                <IconButton className={classes.closeButton} onClick={onClose}>
-                    <CloseIcon />
-                </IconButton>
-            ) : null}
-        </MuiDialogTitle>
-    );
+  const { children, classes, onClose, ...other } = props;
+  return (
+    <MuiDialogTitle disableTypography className={classes.root} {...other}>
+      <Typography variant="h6">{children}</Typography>
+      {onClose ? (
+        <IconButton className={classes.closeButton} onClick={onClose}>
+          <CloseIcon />
+        </IconButton>
+      ) : null}
+    </MuiDialogTitle>
+  );
 });
 
 const DialogActions = withStyles((theme) => ({
-    root: {
-        margin: 0,
-        padding: theme.spacing(1),
-    },
+  root: {
+    margin: 0,
+    padding: theme.spacing(1),
+  },
 }))(MuiDialogActions);
 
 const DialogContent = withStyles((theme) => ({
-    root: {
-        padding: theme.spacing(2),
-    }
+  root: {
+    padding: theme.spacing(2),
+  },
 }))(MuiDialogContent);
 
-const schema = Yup.object(
-    {
-        name: Yup.string().required(),
-        description: Yup.string(),
-    })
+const schema = Yup.object({
+  name: Yup.string().required(),
+  description: Yup.string(),
+});
 
 const ClusterProviderForm = (props) => {
-    const { onClose, item, open, onSubmit } = props;
-    const buttonLabel = item ? "OK" : "Create"
-    const title = item ? "Edit Cluster Provider" : "Register Cluster Provider"
-    const handleClose = () => {
-        onClose();
-    };
-    let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" }
+  const { onClose, item, open, onSubmit } = props;
+  const buttonLabel = item ? "OK" : "Create";
+  const title = item ? "Edit Cluster Provider" : "Register Cluster Provider";
+  const handleClose = () => {
+    onClose();
+  };
+  let initialValues = item
+    ? { name: item.metadata.name, description: item.metadata.description }
+    : { name: "", description: "" };
 
-    return (
-        <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick>
-            <DialogTitle id="simple-dialog-title">{title}</DialogTitle>
-            <Formik
-                initialValues={initialValues}
-                onSubmit={async values => {
-                    onSubmit(values);
-                }}
-                validationSchema={schema}
-            >
-                {props => {
-                    const {
-                        values,
-                        touched,
-                        errors,
-                        isSubmitting,
-                        handleChange,
-                        handleBlur,
-                        handleSubmit
-                    } = props;
-                    return (
-                        <form noValidate onSubmit={handleSubmit}>
-                            <DialogContent dividers>
-                                <TextField
-                                    style={{ width: "100%", marginBottom: "10px" }}
-                                    id="name"
-                                    label="Provider name"
-                                    type="text"
-                                    value={values.name}
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    helperText={(errors.name && touched.name && (
-                                        "Name is required"
-                                    ))}
-                                    required
-                                    error={errors.name && touched.name}
-                                />
-                                <TextField
-                                    style={{ width: "100%", marginBottom: "25px" }}
-                                    name="description"
-                                    value={values.description}
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    id="description"
-                                    label="Description"
-                                    multiline
-                                    rowsMax={4}
-                                />
-                            </DialogContent>
-                            <DialogActions>
-                                <Button autoFocus onClick={handleClose} color="secondary">
-                                    Cancel
-                                </Button>
-                                <Button autoFocus type="submit" color="primary" disabled={isSubmitting}>
-                                    {buttonLabel}
-                                </Button>
-                            </DialogActions>
-                        </form>
-                    );
-                }}
-            </Formik>
-        </Dialog>
-    );
+  return (
+    <Dialog
+      maxWidth={"xs"}
+      onClose={handleClose}
+      aria-labelledby="customized-dialog-title"
+      open={open}
+      disableBackdropClick
+    >
+      <DialogTitle id="simple-dialog-title">{title}</DialogTitle>
+      <Formik
+        initialValues={initialValues}
+        onSubmit={async (values) => {
+          onSubmit(values);
+        }}
+        validationSchema={schema}
+      >
+        {(props) => {
+          const {
+            values,
+            touched,
+            errors,
+            isSubmitting,
+            handleChange,
+            handleBlur,
+            handleSubmit,
+          } = props;
+          return (
+            <form noValidate onSubmit={handleSubmit}>
+              <DialogContent dividers>
+                <TextField
+                  style={{ width: "100%", marginBottom: "10px" }}
+                  id="name"
+                  label="Provider name"
+                  type="text"
+                  value={values.name}
+                  onChange={handleChange}
+                  onBlur={handleBlur}
+                  helperText={errors.name && touched.name && "Name is required"}
+                  required
+                  error={errors.name && touched.name}
+                />
+                <TextField
+                  style={{ width: "100%", marginBottom: "25px" }}
+                  name="description"
+                  value={values.description}
+                  onChange={handleChange}
+                  onBlur={handleBlur}
+                  id="description"
+                  label="Description"
+                  multiline
+                  rowsMax={4}
+                />
+              </DialogContent>
+              <DialogActions>
+                <Button autoFocus onClick={handleClose} color="secondary">
+                  Cancel
+                </Button>
+                <Button
+                  autoFocus
+                  type="submit"
+                  color="primary"
+                  disabled={isSubmitting}
+                >
+                  {buttonLabel}
+                </Button>
+              </DialogActions>
+            </form>
+          );
+        }}
+      </Formik>
+    </Dialog>
+  );
 };
 
 ClusterProviderForm.propTypes = {
-    onClose: PropTypes.func.isRequired,
-    open: PropTypes.bool.isRequired,
-    item: PropTypes.object
+  onClose: PropTypes.func.isRequired,
+  open: PropTypes.bool.isRequired,
+  item: PropTypes.object,
 };
 
 export default ClusterProviderForm;
index 2031769..192992b 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import { makeStyles } from "@material-ui/core/styles";
 import Accordion from "@material-ui/core/Accordion";
@@ -22,11 +22,13 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
 import apiService from "../../services/apiService";
 import { Button } from "@material-ui/core";
 import DeleteIcon from "@material-ui/icons/Delete";
-import EditIcon from "@material-ui/icons/Edit";
+// import EditIcon from "@material-ui/icons/Edit";
 import ClusterForm from "./clusters/ClusterForm";
 import ClustersTable from "./clusters/ClusterTable";
 import DeleteDialog from "../../common/Dialogue";
-import ClusterProviderForm from "../clusterProvider/ClusterProviderForm";
+import Notification from "../../common/Notification";
+
+//import ClusterProviderForm from "../clusterProvider/ClusterProviderForm";
 
 const useStyles = makeStyles((theme) => ({
   root: {
@@ -47,8 +49,9 @@ export default function ControlledAccordions({ data, setData, ...props }) {
   const [expanded, setExpanded] = useState(false);
   const [open, setOpen] = React.useState(false);
   const [formOpen, setFormOpen] = useState(false);
-  const [openProviderForm, setOpenProviderForm] = useState(false);
+  // const [openProviderForm, setOpenProviderForm] = useState(false);
   const [selectedRowIndex, setSelectedRowIndex] = useState(0);
+  const [notificationDetails, setNotificationDetails] = useState({});
   const handleAccordianOpen = (providerRow) => (event, isExpanded) => {
     if (!isExpanded) {
       setExpanded(isExpanded ? providerRow : false);
@@ -141,7 +144,7 @@ export default function ControlledAccordions({ data, setData, ...props }) {
     setSelectedRowIndex(index);
     setOpen(true);
   };
-  const handleSubmit = (values) => {
+  const handleSubmit = (values, setSubmitting) => {
     let metadata = {};
     if (values.userData) {
       metadata = JSON.parse(values.userData);
@@ -150,7 +153,6 @@ export default function ControlledAccordions({ data, setData, ...props }) {
     metadata.description = values.description;
     const formData = new FormData();
     formData.append("file", values.file);
-    // `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}`
     formData.append("metadata", `{"metadata":${JSON.stringify(metadata)}}`);
     formData.append("providerName", data[selectedRowIndex].metadata.name);
     apiService
@@ -161,12 +163,24 @@ export default function ControlledAccordions({ data, setData, ...props }) {
           ? (data[selectedRowIndex].clusters = [res])
           : data[selectedRowIndex].clusters.push(res);
         setData([...data]);
+        setFormOpen(false);
+        setNotificationDetails({
+          show: true,
+          message: `${values.name} cluster added`,
+          severity: "success",
+        });
       })
       .catch((err) => {
-        console.log("error adding cluster : ", err);
-      })
-      .finally(() => {
-        setFormOpen(false);
+        debugger;
+        if (err.response.status === 403) {
+          setNotificationDetails({
+            show: true,
+            message: `${err.response.data}`,
+            severity: "error",
+          });
+          setSubmitting(false);
+        }
+        console.log("error adding cluster : " + err);
       });
   };
   const handleFormClose = () => {
@@ -198,35 +212,36 @@ export default function ControlledAccordions({ data, setData, ...props }) {
     setOpen(false);
     setSelectedRowIndex(0);
   };
-  const handleEdit = (index) => {
-    setSelectedRowIndex(index);
-    setOpenProviderForm(true);
-  };
-  const handleCloseProviderForm = () => {
-    setOpenProviderForm(false);
-  };
-  const handleSubmitProviderForm = (values) => {
-    let request = {
-      payload: { metatada: values },
-      providerName: data[selectedRowIndex].metadata.name,
-    };
-    apiService
-      .updateClusterProvider(request)
-      .then((res) => {
-        setData((data) => {
-          data[selectedRowIndex].metadata = res.metadata;
-          return data;
-        });
-      })
-      .catch((err) => {
-        console.log("error updating cluster provider. " + err);
-      })
-      .finally(() => {
-        setOpenProviderForm(false);
-      });
-  };
+  // const handleEdit = (index) => {
+  //   setSelectedRowIndex(index);
+  //   setOpenProviderForm(true);
+  // };
+  // const handleCloseProviderForm = () => {
+  //   setOpenProviderForm(false);
+  // };
+  // const handleSubmitProviderForm = (values) => {
+  //   let request = {
+  //     payload: { metatada: values },
+  //     providerName: data[selectedRowIndex].metadata.name,
+  //   };
+  //   apiService
+  //     .updateClusterProvider(request)
+  //     .then((res) => {
+  //       setData((data) => {
+  //         data[selectedRowIndex].metadata = res.metadata;
+  //         return data;
+  //       });
+  //     })
+  //     .catch((err) => {
+  //       console.log("error updating cluster provider. " + err);
+  //     })
+  //     .finally(() => {
+  //       setOpenProviderForm(false);
+  //     });
+  // };
   return (
     <>
+      <Notification notificationDetails={notificationDetails} />
       {data && data.length > 0 && (
         <div className={classes.root}>
           <ClusterForm
@@ -234,12 +249,12 @@ export default function ControlledAccordions({ data, setData, ...props }) {
             onClose={handleFormClose}
             onSubmit={handleSubmit}
           />
-          <ClusterProviderForm
+          {/* <ClusterProviderForm
             open={openProviderForm}
             onClose={handleCloseProviderForm}
             onSubmit={handleSubmitProviderForm}
             item={data[selectedRowIndex]}
-          />
+          /> */}
           <DeleteDialog
             open={open}
             onClose={handleClose}
@@ -288,6 +303,8 @@ export default function ControlledAccordions({ data, setData, ...props }) {
                 >
                   Delete Provider
                 </Button>
+                {/* 
+                //edit cluster provider is not supported by the api yet
                 <Button
                   variant="outlined"
                   size="small"
@@ -299,7 +316,7 @@ export default function ControlledAccordions({ data, setData, ...props }) {
                   }}
                 >
                   Edit Provider
-                </Button>
+                </Button> */}
               </div>
               <AccordionDetails>
                 {item.clusters && (
index 6d9fc83..6c49cb8 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React from "react";
 import PropTypes from "prop-types";
 import { withStyles } from "@material-ui/core/styles";
@@ -113,8 +113,8 @@ const ClusterForm = (props) => {
       <DialogTitle id="simple-dialog-title">{title}</DialogTitle>
       <Formik
         initialValues={initialValues}
-        onSubmit={async (values) => {
-          onSubmit(values);
+        onSubmit={(values, actions) => {
+          onSubmit(values, actions.setSubmitting);
         }}
         validationSchema={schema}
       >
index 1066d47..26bc1ca 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-import AddIconOutline from '@material-ui/icons/AddCircleOutline';
-import AddIcon from '@material-ui/icons/Add';
-import Table from '@material-ui/core/Table';
-import TableBody from '@material-ui/core/TableBody';
-import TableCell from '@material-ui/core/TableCell';
-import TableContainer from '@material-ui/core/TableContainer';
-import TableHead from '@material-ui/core/TableHead';
-import TableRow from '@material-ui/core/TableRow';
-import IconButton from '@material-ui/core/IconButton';
-import EditIcon from '@material-ui/icons/Edit';
-import Chip from '@material-ui/core/Chip';
-import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet';
-import DeleteIcon from '@material-ui/icons/Delete';
-import { makeStyles, TextField, Button } from '@material-ui/core';
+// ========================================================================
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import AddIconOutline from "@material-ui/icons/AddCircleOutline";
+import AddIcon from "@material-ui/icons/Add";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableContainer from "@material-ui/core/TableContainer";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import IconButton from "@material-ui/core/IconButton";
+// import EditIcon from "@material-ui/icons/Edit";
+import Chip from "@material-ui/core/Chip";
+import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
+import DeleteIcon from "@material-ui/icons/Delete";
+import { makeStyles, TextField, Button } from "@material-ui/core";
 import NetworkForm from "../networks/NetworkForm";
 import apiService from "../../../services/apiService";
 import DeleteDialog from "../../../common/Dialogue";
-import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined';
-import CheckIcon from '@material-ui/icons/CheckCircleOutlineOutlined';
-import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';
+import CancelOutlinedIcon from "@material-ui/icons/CancelOutlined";
+import CheckIcon from "@material-ui/icons/CheckCircleOutlineOutlined";
+import InfoOutlinedIcon from "@material-ui/icons/InfoOutlined";
 import NetworkDetailsDialog from "../../../common/DetailsDialog";
-import DoneOutlineIcon from '@material-ui/icons/DoneOutline';
+import DoneOutlineIcon from "@material-ui/icons/DoneOutline";
 import ClusterForm from "../clusters/ClusterForm";
+import Notification from "../../../common/Notification";
 
 const useStyles = makeStyles((theme) => ({
-    root: {
-        width: '100%',
-    },
-    heading: {
-        fontSize: theme.typography.pxToRem(15),
-        flexBasis: '33.33%',
-        flexShrink: 0,
-    },
-    secondaryHeading: {
-        fontSize: theme.typography.pxToRem(15),
-        color: theme.palette.text.secondary,
-    },
+  root: {
+    width: "100%",
+  },
+  heading: {
+    fontSize: theme.typography.pxToRem(15),
+    flexBasis: "33.33%",
+    flexShrink: 0,
+  },
+  secondaryHeading: {
+    fontSize: theme.typography.pxToRem(15),
+    color: theme.palette.text.secondary,
+  },
 }));
 
 const ClusterTable = ({ clustersData, ...props }) => {
-    const classes = useStyles();
-    const [formOpen, setformOpen] = useState(false);
-    const [networkDetailsOpen, setNetworkDetailsOpen] = useState(false);
-    const [network, setNetwork] = useState({});
-    const [activeRowIndex, setActiveRowIndex] = useState(0);
-    const [activeNetwork, setActiveNetwork] = useState({});
-    const [open, setOpen] = useState(false);
-    const [openDeleteNetwork, setOpenDeleteNetwork] = useState(false);
-    const [showAddLabel, setShowAddLabel] = useState(false);
-    const [labelInput, setLabelInput] = React.useState("");
-    const [clusterFormOpen, setClusterFormOpen] = useState(false);
-    const handleFormClose = () => {
+  const classes = useStyles();
+  const [formOpen, setformOpen] = useState(false);
+  const [networkDetailsOpen, setNetworkDetailsOpen] = useState(false);
+  const [network, setNetwork] = useState({});
+  const [activeRowIndex, setActiveRowIndex] = useState(0);
+  const [activeNetwork, setActiveNetwork] = useState({});
+  const [open, setOpen] = useState(false);
+  const [openDeleteNetwork, setOpenDeleteNetwork] = useState(false);
+  const [showAddLabel, setShowAddLabel] = useState(false);
+  const [labelInput, setLabelInput] = useState("");
+  //   const [clusterFormOpen, setClusterFormOpen] = useState(false);
+  const [notificationDetails, setNotificationDetails] = useState({});
+  const handleFormClose = () => {
+    setformOpen(false);
+  };
+  const handleSubmit = (data) => {
+    let networkSpec = JSON.parse(data.spec);
+    let payload = {
+      metadata: { name: data.name, description: data.description },
+      spec: networkSpec,
+    };
+    let request = {
+      providerName: props.providerName,
+      clusterName: clustersData[activeRowIndex].metadata.name,
+      networkType: data.type,
+      payload: payload,
+    };
+    apiService
+      .addNetwork(request)
+      .then((res) => {
+        let networkType =
+          data.type === "networks" ? "networks" : "providerNetworks";
+        !clustersData[activeRowIndex][networkType] ||
+        clustersData[activeRowIndex][networkType] === null
+          ? (clustersData[activeRowIndex][networkType] = [res])
+          : clustersData[activeRowIndex][networkType].push(res);
+      })
+      .catch((err) => {
+        console.log("error adding cluster network : ", err);
+      })
+      .finally(() => {
+        setActiveRowIndex(0);
         setformOpen(false);
-    }
-    const handleSubmit = (data) => {
-        let networkSpec = JSON.parse(data.spec);
-        let payload = { metadata: { name: data.name, description: data.description }, spec: networkSpec };
-        let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, networkType: data.type, payload: payload };
-        apiService.addNetwork(request).then(res => {
-            let networkType = (data.type === "networks" ? "networks" : "providerNetworks");
-            (!clustersData[activeRowIndex][networkType] || clustersData[activeRowIndex][networkType] === null) ? (clustersData[activeRowIndex][networkType] = [res]) : clustersData[activeRowIndex][networkType].push(res);
-        }).catch(err => {
-            console.log("error adding cluster network : ", err)
-        }).finally(() => {
-            setActiveRowIndex(0);
-            setformOpen(false);
+      });
+  };
+  const handleAddNetwork = (index) => {
+    setActiveRowIndex(index);
+    setformOpen(true);
+  };
+  const handleDeleteLabel = (index, label, labelIndex) => {
+    let request = {
+      providerName: props.providerName,
+      clusterName: clustersData[index].metadata.name,
+      labelName: label,
+    };
+    apiService
+      .deleteClusterLabel(request)
+      .then((res) => {
+        console.log("label deleted");
+        clustersData[index].labels.splice(labelIndex, 1);
+        props.onUpdateCluster(props.parentIndex, clustersData);
+      })
+      .catch((err) => {
+        console.log("error deleting label : ", err);
+      });
+  };
+  const handleClose = (el) => {
+    if (el.target.innerText === "Delete") {
+      let request = {
+        providerName: props.providerName,
+        clusterName: clustersData[activeRowIndex].metadata.name,
+      };
+      apiService
+        .deleteCluster(request)
+        .then(() => {
+          console.log("cluster deleted");
+          props.onDeleteCluster(props.parentIndex, activeRowIndex);
+        })
+        .catch((err) => {
+          console.log("Error deleting cluster : ", +err);
+          setNotificationDetails({
+            show: true,
+            message: "Unable to remove cluster",
+            severity: "error",
+          });
         });
     }
-    const handleAddNetwork = (index) => {
-        setActiveRowIndex(index);
-        setformOpen(true);
-    }
-    const handleDeleteLabel = (index, label, labelIndex) => {
-        let request = { providerName: props.providerName, clusterName: clustersData[index].metadata.name, labelName: label }
-        apiService.deleteClusterLabel(request).then(res => {
-            console.log("label deleted");
-            clustersData[index].labels.splice(labelIndex, 1);
-            props.onUpdateCluster(props.parentIndex, clustersData);
-        }).catch(err => { console.log("error deleting label : ", err) })
-    }
-    const handleClose = el => {
-        if (el.target.innerText === "Delete") {
-            let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name };
-            apiService.deleteCluster(request).then(() => {
-                console.log("cluster deleted");
-                props.onDeleteCluster(props.parentIndex, activeRowIndex);
-            }).catch(err => {
-                console.log("Error deleting cluster : ", err)
-            })
-        }
-        setOpen(false);
-        setActiveRowIndex(0);
-    };
+    setOpen(false);
+    setActiveRowIndex(0);
+  };
 
-    const handleCloseDeleteNetwork = (el) => {
-        if (el.target.innerText === "Delete") {
-            let networkName = clustersData[activeRowIndex][activeNetwork.networkType][activeNetwork.networkIndex].metadata.name;
-            let networkType = (activeNetwork.networkType === "providerNetworks" ? "provider-networks" : "networks");
-            let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, networkType: networkType, networkName: networkName };
-            apiService.deleteClusterNetwork(request).then(() => {
-                console.log("cluster network deleted");
-                clustersData[activeRowIndex][activeNetwork.networkType].splice(activeNetwork.networkIndex, 1);
-            }).catch(err => {
-                console.log("Error deleting cluster network : ", err)
-            }).finally(() => { setActiveRowIndex(0); setActiveNetwork({}); })
-        }
-        setOpenDeleteNetwork(false);
-    }
-    const handleDeleteCluster = (index) => {
-        setActiveRowIndex(index);
-        setOpen(true);
-    }
-    const handleAddLabel = (index) => {
-        if (labelInput !== "") {
-            let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, payload: { "label-name": labelInput } };
-            apiService.addClusterLabel(request)
-                .then(res => {
-                    (!clustersData[index].labels || clustersData[index].labels === null) ? (clustersData[index].labels = [res]) : clustersData[index].labels.push(res);
-                })
-                .catch(err => { console.log("error adding label", err) })
-                .finally(() => {
-                    setShowAddLabel(!showAddLabel);
-                })
-        }
+  const handleCloseDeleteNetwork = (el) => {
+    if (el.target.innerText === "Delete") {
+      let networkName =
+        clustersData[activeRowIndex][activeNetwork.networkType][
+          activeNetwork.networkIndex
+        ].metadata.name;
+      let networkType =
+        activeNetwork.networkType === "providerNetworks"
+          ? "provider-networks"
+          : "networks";
+      let request = {
+        providerName: props.providerName,
+        clusterName: clustersData[activeRowIndex].metadata.name,
+        networkType: networkType,
+        networkName: networkName,
+      };
+      apiService
+        .deleteClusterNetwork(request)
+        .then(() => {
+          console.log("cluster network deleted");
+          clustersData[activeRowIndex][activeNetwork.networkType].splice(
+            activeNetwork.networkIndex,
+            1
+          );
+        })
+        .catch((err) => {
+          console.log("Error deleting cluster network : ", err);
+        })
+        .finally(() => {
+          setActiveRowIndex(0);
+          setActiveNetwork({});
+        });
     }
-
-    const handleToggleAddLabel = (index) => {
-        setShowAddLabel(showAddLabel === index ? false : index);
-        setActiveRowIndex(index);
-        setLabelInput('');
+    setOpenDeleteNetwork(false);
+  };
+  const handleDeleteCluster = (index) => {
+    setActiveRowIndex(index);
+    setOpen(true);
+  };
+  const handleAddLabel = (index) => {
+    if (labelInput !== "") {
+      let request = {
+        providerName: props.providerName,
+        clusterName: clustersData[activeRowIndex].metadata.name,
+        payload: { "label-name": labelInput },
+      };
+      apiService
+        .addClusterLabel(request)
+        .then((res) => {
+          !clustersData[index].labels || clustersData[index].labels === null
+            ? (clustersData[index].labels = [res])
+            : clustersData[index].labels.push(res);
+        })
+        .catch((err) => {
+          console.log("error adding label", err);
+        })
+        .finally(() => {
+          setShowAddLabel(!showAddLabel);
+        });
     }
-    const handleLabelInputChange = (event) => {
-        setLabelInput(event.target.value);
-    };
+  };
 
-    const handleNetworkDetailOpen = (network) => {
-        setNetwork(network);
-        setNetworkDetailsOpen(true);
-    }
-    const handleDeleteNetwork = (index, networkIndex, networkType, networkName) => {
-        setActiveNetwork({ networkIndex: networkIndex, networkType: networkType, name: networkName });
-        setActiveRowIndex(index);
-        setOpenDeleteNetwork(true);
-    }
-    const applyNetworkConfig = (clusterName) => {
-        let request = { providerName: props.providerName, clusterName: clusterName }
-        apiService.applyNetworkConfig(request)
-            .then(res => {
-                console.log("Network config applied");
-            })
-            .catch(err => {
-                console.log("Error applying network config : ", err);
-                if (err.response)
-                    console.log("Network config applied" + err.response.data);
-                else
-                    console.log("Network config applied" + err);
-            });
-    }
-    const handleClusterFormClose = () => {
-        setClusterFormOpen(false);
-    }
-    const handleClusterSubmit = (values) => {
-        const formData = new FormData();
-        if (values.file)
-            formData.append('file', values.file);
-        formData.append("metadata", `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}`);
-        formData.append("providerName", props.providerName);
-        apiService.updateCluster(formData)
-            .then(res => {
-                clustersData[activeRowIndex].metadata = res.metadata;
-                props.onUpdateCluster(props.parentIndex, clustersData);
-            })
-            .catch(err => { console.log("error updating cluster : ", err) })
-            .finally(() => { handleClusterFormClose() });
+  const handleToggleAddLabel = (index) => {
+    setShowAddLabel(showAddLabel === index ? false : index);
+    setActiveRowIndex(index);
+    setLabelInput("");
+  };
+  const handleLabelInputChange = (event) => {
+    setLabelInput(event.target.value);
+  };
 
-    }
-    const handleEditCluster = (index) => {
-        setActiveRowIndex(index);
-        setClusterFormOpen(true);
-    }
-    return (
+  const handleNetworkDetailOpen = (network) => {
+    setNetwork(network);
+    setNetworkDetailsOpen(true);
+  };
+  const handleDeleteNetwork = (
+    index,
+    networkIndex,
+    networkType,
+    networkName
+  ) => {
+    setActiveNetwork({
+      networkIndex: networkIndex,
+      networkType: networkType,
+      name: networkName,
+    });
+    setActiveRowIndex(index);
+    setOpenDeleteNetwork(true);
+  };
+  const applyNetworkConfig = (clusterName) => {
+    let request = {
+      providerName: props.providerName,
+      clusterName: clusterName,
+    };
+    apiService
+      .applyNetworkConfig(request)
+      .then((res) => {
+        setNotificationDetails({
+          show: true,
+          message: "Network configuration applied",
+          severity: "success",
+        });
+        console.log("Network config applied");
+      })
+      .catch((err) => {
+        setNotificationDetails({
+          show: true,
+          message: "Error applying network configuration",
+          severity: "error",
+        });
+        console.log("Error applying network config : ", err);
+        if (err.response)
+          console.log("Network config applied" + err.response.data);
+        else console.log("Network config applied" + err);
+      });
+  };
+  //   const handleClusterFormClose = () => {
+  //     setClusterFormOpen(false);
+  //   };
+  //   const handleClusterSubmit = (values) => {
+  //     const formData = new FormData();
+  //     if (values.file) formData.append("file", values.file);
+  //     formData.append(
+  //       "metadata",
+  //       `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}`
+  //     );
+  //     formData.append("providerName", props.providerName);
+  //     apiService
+  //       .updateCluster(formData)
+  //       .then((res) => {
+  //         clustersData[activeRowIndex].metadata = res.metadata;
+  //         props.onUpdateCluster(props.parentIndex, clustersData);
+  //       })
+  //       .catch((err) => {
+  //         console.log("error updating cluster : ", err);
+  //       })
+  //       .finally(() => {
+  //         handleClusterFormClose();
+  //       });
+  //   };
+  //disabling as edit is not supported yet by the api yet
+  //   const handleEditCluster = (index) => {
+  //     setActiveRowIndex(index);
+  //     setClusterFormOpen(true);
+  //   };
+  return (
+    <>
+      <Notification notificationDetails={notificationDetails} />
+      {clustersData && clustersData.length > 0 && (
         <>
-            {clustersData && (clustersData.length > 0) &&
-                (<>
-                    <ClusterForm item={clustersData[activeRowIndex]} open={clusterFormOpen} onClose={handleClusterFormClose} onSubmit={handleClusterSubmit} />
-                    <NetworkDetailsDialog onClose={setNetworkDetailsOpen} open={networkDetailsOpen} item={network} type="Network" />
-                    <NetworkForm onClose={handleFormClose} onSubmit={handleSubmit} open={formOpen} />
-                    <DeleteDialog open={open} onClose={handleClose} title={"Delete Cluster"}
-                        content={`Are you sure you want to delete "${clustersData[activeRowIndex] ? clustersData[activeRowIndex].metadata.name : ""}" ?`} />
-                    <DeleteDialog open={openDeleteNetwork} onClose={handleCloseDeleteNetwork} title={"Delete Network"} content={`Are you sure you want to delete "${activeNetwork.name}" ?`} />
-                    <TableContainer >
-                        <Table className={classes.table}>
-                            <TableHead>
-                                <TableRow>
-                                    <TableCell style={{ width: "10%" }}>Name</TableCell>
-                                    <TableCell style={{ width: "15%" }}>Description</TableCell>
-                                    <TableCell style={{ width: "20%" }}>Networks </TableCell>
-                                    <TableCell style={{ width: "35%" }}>Labels </TableCell>
-                                    <TableCell style={{ width: "20%" }}>Actions</TableCell>
-                                </TableRow>
-                            </TableHead>
-                            <TableBody>
-                                {clustersData.map((row, index) => (
-                                    <TableRow key={row.metadata.name + "" + index}>
-                                        <TableCell >{row.metadata.name}</TableCell>
-                                        <TableCell >{row.metadata.description}</TableCell>
-                                        <TableCell>
-                                            <div>
-                                                {row.providerNetworks && (row.providerNetworks.length > 0) && row.providerNetworks.map((providerNetwork, providerNetworkIndex) =>
-                                                    (<Chip
-                                                        key={providerNetwork.metadata.name + "" + providerNetworkIndex}
-                                                        size="small"
-                                                        icon={<InfoOutlinedIcon onClick={() => { handleNetworkDetailOpen(providerNetwork) }} style={{ cursor: "pointer" }} />}
-                                                        onDelete={(e) => { handleDeleteNetwork(index, providerNetworkIndex, "providerNetworks", providerNetwork.metadata.name) }}
-                                                        label={providerNetwork.metadata.name}
-                                                        style={{ marginRight: "10px", marginBottom: "5px" }}
-                                                    />)
-                                                )}
+          {/* <ClusterForm
+            item={clustersData[activeRowIndex]}
+            open={clusterFormOpen}
+            onClose={handleClusterFormClose}
+            onSubmit={handleClusterSubmit}
+          /> */}
+          <NetworkDetailsDialog
+            onClose={setNetworkDetailsOpen}
+            open={networkDetailsOpen}
+            item={network}
+            type="Network"
+          />
+          <NetworkForm
+            onClose={handleFormClose}
+            onSubmit={handleSubmit}
+            open={formOpen}
+          />
+          <DeleteDialog
+            open={open}
+            onClose={handleClose}
+            title={"Delete Cluster"}
+            content={`Are you sure you want to delete "${
+              clustersData[activeRowIndex]
+                ? clustersData[activeRowIndex].metadata.name
+                : ""
+            }" ?`}
+          />
+          <DeleteDialog
+            open={openDeleteNetwork}
+            onClose={handleCloseDeleteNetwork}
+            title={"Delete Network"}
+            content={`Are you sure you want to delete "${activeNetwork.name}" ?`}
+          />
+          <TableContainer>
+            <Table className={classes.table}>
+              <TableHead>
+                <TableRow>
+                  <TableCell style={{ width: "10%" }}>Name</TableCell>
+                  <TableCell style={{ width: "15%" }}>Description</TableCell>
+                  <TableCell style={{ width: "20%" }}>Networks </TableCell>
+                  <TableCell style={{ width: "35%" }}>Labels </TableCell>
+                  <TableCell style={{ width: "20%" }}>Actions</TableCell>
+                </TableRow>
+              </TableHead>
+              <TableBody>
+                {clustersData.map((row, index) => (
+                  <TableRow key={row.metadata.name + "" + index}>
+                    <TableCell>{row.metadata.name}</TableCell>
+                    <TableCell>{row.metadata.description}</TableCell>
+                    <TableCell>
+                      <div>
+                        {row.providerNetworks &&
+                          row.providerNetworks.length > 0 &&
+                          row.providerNetworks.map(
+                            (providerNetwork, providerNetworkIndex) => (
+                              <Chip
+                                key={
+                                  providerNetwork.metadata.name +
+                                  "" +
+                                  providerNetworkIndex
+                                }
+                                size="small"
+                                icon={
+                                  <InfoOutlinedIcon
+                                    onClick={() => {
+                                      handleNetworkDetailOpen(providerNetwork);
+                                    }}
+                                    style={{ cursor: "pointer" }}
+                                  />
+                                }
+                                onDelete={(e) => {
+                                  handleDeleteNetwork(
+                                    index,
+                                    providerNetworkIndex,
+                                    "providerNetworks",
+                                    providerNetwork.metadata.name
+                                  );
+                                }}
+                                label={providerNetwork.metadata.name}
+                                style={{
+                                  marginRight: "10px",
+                                  marginBottom: "5px",
+                                }}
+                              />
+                            )
+                          )}
 
-                                                {row.networks && (row.networks.length > 0) && row.networks.map((network, networkIndex) =>
-                                                    (<Chip
-                                                        key={network.metadata.name + "" + networkIndex}
-                                                        size="small"
-                                                        icon={<InfoOutlinedIcon onClick={() => { handleNetworkDetailOpen(network) }} style={{ cursor: "pointer" }} />}
-                                                        onDelete={(e) => { handleDeleteNetwork(index, networkIndex, "networks", network.metadata.name) }}
-                                                        label={network.metadata.name}
-                                                        style={{ marginRight: "10px", marginBottom: "5px" }}
-                                                        color="secondary"
-                                                    />)
-                                                )}
-                                            </div>
-                                        </TableCell>
-                                        <TableCell>
-                                            {row.labels && (row.labels.length > 0) && row.labels.map((label, labelIndex) =>
-                                                (<Chip
-                                                    key={label["label-name"] + "" + labelIndex}
-                                                    size="small"
-                                                    icon={<SettingsEthernetIcon />}
-                                                    label={label["label-name"]}
-                                                    onDelete={(e) => { handleDeleteLabel(index, label["label-name"], labelIndex) }}
-                                                    color="primary"
-                                                    style={{ marginRight: "10px" }}
-                                                />)
-                                            )}
-                                            {(showAddLabel === index) &&
-                                                <TextField
-                                                    style={{ height: "24px" }}
-                                                    size="small"
-                                                    value={labelInput}
-                                                    onChange={handleLabelInputChange}
-                                                    id="outlined-basic" label="Add label" variant="outlined" />
-                                            }
-                                            {(showAddLabel === index) &&
-                                                <IconButton color="primary" onClick={() => { handleAddLabel(index) }}>
-                                                    <CheckIcon />
-                                                </IconButton>
-                                            }
-                                            <IconButton color="primary" onClick={() => { handleToggleAddLabel(index) }}>
-                                                {!(showAddLabel === index) && <AddIconOutline />}
-                                                {(showAddLabel === index) && <CancelOutlinedIcon color="secondary" />}
-                                            </IconButton>
-                                        </TableCell>
-                                        <TableCell>
-                                            <Button
-                                                variant="outlined"
-                                                startIcon={<AddIcon />}
-                                                size="small"
-                                                color="primary"
-                                                title="Add Network"
-                                                onClick={() => { handleAddNetwork(index) }}>
-                                                Network
-                                            </Button>
-                                            <IconButton
-                                                style={{ color: "green" }}
-                                                onClick={() => { applyNetworkConfig(row.metadata.name) }}
-                                                title="Apply Network Configuration">
-                                                <DoneOutlineIcon />
-                                            </IconButton>
-                                            <IconButton
-                                                title="Edit"
-                                                onClick={() => { handleEditCluster(index) }}
-                                                color="primary">
-                                                <EditIcon />
-                                            </IconButton>
-                                            <IconButton
-                                                title="Delete"
-                                                color="secondary"
-                                                onClick={() => { handleDeleteCluster(index) }}>
-                                                <DeleteIcon />
-                                            </IconButton>
-                                        </TableCell>
-                                    </TableRow>))}
-                            </TableBody>
-                        </Table>
-                    </TableContainer>
-                </>)}
-            {(!clustersData || (clustersData.length === 0)) && (<span>No Clusters</span>)}
-        </>)
-}
+                        {row.networks &&
+                          row.networks.length > 0 &&
+                          row.networks.map((network, networkIndex) => (
+                            <Chip
+                              key={network.metadata.name + "" + networkIndex}
+                              size="small"
+                              icon={
+                                <InfoOutlinedIcon
+                                  onClick={() => {
+                                    handleNetworkDetailOpen(network);
+                                  }}
+                                  style={{ cursor: "pointer" }}
+                                />
+                              }
+                              onDelete={(e) => {
+                                handleDeleteNetwork(
+                                  index,
+                                  networkIndex,
+                                  "networks",
+                                  network.metadata.name
+                                );
+                              }}
+                              label={network.metadata.name}
+                              style={{
+                                marginRight: "10px",
+                                marginBottom: "5px",
+                              }}
+                              color="secondary"
+                            />
+                          ))}
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      {row.labels &&
+                        row.labels.length > 0 &&
+                        row.labels.map((label, labelIndex) => (
+                          <Chip
+                            key={label["label-name"] + "" + labelIndex}
+                            size="small"
+                            icon={<SettingsEthernetIcon />}
+                            label={label["label-name"]}
+                            onDelete={(e) => {
+                              handleDeleteLabel(
+                                index,
+                                label["label-name"],
+                                labelIndex
+                              );
+                            }}
+                            color="primary"
+                            style={{ marginRight: "10px" }}
+                          />
+                        ))}
+                      {showAddLabel === index && (
+                        <TextField
+                          style={{ height: "24px" }}
+                          size="small"
+                          value={labelInput}
+                          onChange={handleLabelInputChange}
+                          id="outlined-basic"
+                          label="Add label"
+                          variant="outlined"
+                        />
+                      )}
+                      {showAddLabel === index && (
+                        <IconButton
+                          color="primary"
+                          onClick={() => {
+                            handleAddLabel(index);
+                          }}
+                        >
+                          <CheckIcon />
+                        </IconButton>
+                      )}
+                      <IconButton
+                        color="primary"
+                        onClick={() => {
+                          handleToggleAddLabel(index);
+                        }}
+                      >
+                        {!(showAddLabel === index) && <AddIconOutline />}
+                        {showAddLabel === index && (
+                          <CancelOutlinedIcon color="secondary" />
+                        )}
+                      </IconButton>
+                    </TableCell>
+                    <TableCell>
+                      <Button
+                        variant="outlined"
+                        startIcon={<AddIcon />}
+                        size="small"
+                        color="primary"
+                        title="Add Network"
+                        onClick={() => {
+                          handleAddNetwork(index);
+                        }}
+                      >
+                        Network
+                      </Button>
+                      <IconButton
+                        color="primary"
+                        disabled={
+                          !(
+                            (row.networks && row.networks.length > 0) ||
+                            (row.providerNetworks &&
+                              row.providerNetworks.length > 0)
+                          )
+                        }
+                        onClick={() => {
+                          applyNetworkConfig(row.metadata.name);
+                        }}
+                        title="Apply Network Configuration"
+                      >
+                        <DoneOutlineIcon />
+                      </IconButton>
+                      {/* 
+                      //disabling as edit is not supported yet by the api yet
+                        <IconButton
+                            title="Edit"
+                            onClick={() => { handleEditCluster(index) }}
+                            color="primary">
+                            <EditIcon />
+                        </IconButton> */}
+                      <IconButton
+                        title="Delete"
+                        color="secondary"
+                        disabled={
+                          (row.networks && row.networks.length > 0) ||
+                          (row.providerNetworks &&
+                            row.providerNetworks.length > 0) ||
+                          (row.labels && row.labels.length > 0)
+                        }
+                        onClick={() => {
+                          handleDeleteCluster(index);
+                        }}
+                      >
+                        <DeleteIcon />
+                      </IconButton>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </TableContainer>
+        </>
+      )}
+      {(!clustersData || clustersData.length === 0) && <span>No Clusters</span>}
+    </>
+  );
+};
 ClusterTable.propTypes = {
-    clusters: PropTypes.arrayOf(PropTypes.object)
+  clusters: PropTypes.arrayOf(PropTypes.object),
 };
 export default ClusterTable;
index 4a8a502..4316f6e 100644 (file)
@@ -29,7 +29,8 @@ function Controllers() {
     apiService
       .getControllers()
       .then((res) => {
-        setControllersData(res);
+        if (res && res.length > 0) setControllersData(res);
+        else setControllersData([]);
       })
       .catch((err) => {
         console.log("error getting controllers : " + err);
@@ -53,9 +54,7 @@ function Controllers() {
         .addController(request)
         .then((res) => {
           setControllersData((controllersData) => {
-            if (controllersData && controllersData.length > 0)
-              return [...controllersData, res];
-            else return [res];
+            return [...controllersData, res];
           });
         })
         .catch((err) => {
index 751de5d..4ea87b2 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
+// ========================================================================
+import React from "react";
+import PropTypes from "prop-types";
+import { withStyles } from "@material-ui/core/styles";
+import Button from "@material-ui/core/Button";
 
-import Dialog from '@material-ui/core/Dialog';
-import MuiDialogTitle from '@material-ui/core/DialogTitle';
-import MuiDialogContent from '@material-ui/core/DialogContent';
-import MuiDialogActions from '@material-ui/core/DialogActions';
-import IconButton from '@material-ui/core/IconButton';
-import CloseIcon from '@material-ui/icons/Close';
-import Typography from '@material-ui/core/Typography';
-import { TextField } from '@material-ui/core';
+import Dialog from "@material-ui/core/Dialog";
+import MuiDialogTitle from "@material-ui/core/DialogTitle";
+import MuiDialogContent from "@material-ui/core/DialogContent";
+import MuiDialogActions from "@material-ui/core/DialogActions";
+import IconButton from "@material-ui/core/IconButton";
+import CloseIcon from "@material-ui/icons/Close";
+import Typography from "@material-ui/core/Typography";
+import { TextField } from "@material-ui/core";
 import * as Yup from "yup";
-import { Formik } from 'formik';
+import { Formik } from "formik";
 
 const styles = (theme) => ({
-    root: {
-        margin: 0,
-        padding: theme.spacing(2),
-    },
-    closeButton: {
-        position: 'absolute',
-        right: theme.spacing(1),
-        top: theme.spacing(1),
-        color: theme.palette.grey[500],
-    },
+  root: {
+    margin: 0,
+    padding: theme.spacing(2),
+  },
+  closeButton: {
+    position: "absolute",
+    right: theme.spacing(1),
+    top: theme.spacing(1),
+    color: theme.palette.grey[500],
+  },
 });
 
 const DialogTitle = withStyles(styles)((props) => {
-    const { children, classes, onClose, ...other } = props;
-    return (
-        <MuiDialogTitle disableTypography className={classes.root} {...other}>
-            <Typography variant="h6">{children}</Typography>
-            {onClose ? (
-                <IconButton className={classes.closeButton} onClick={onClose}>
-                    <CloseIcon />
-                </IconButton>
-            ) : null}
-        </MuiDialogTitle>
-    );
+  const { children, classes, onClose, ...other } = props;
+  return (
+    <MuiDialogTitle disableTypography className={classes.root} {...other}>
+      <Typography variant="h6">{children}</Typography>
+      {onClose ? (
+        <IconButton className={classes.closeButton} onClick={onClose}>
+          <CloseIcon />
+        </IconButton>
+      ) : null}
+    </MuiDialogTitle>
+  );
 });
 
 const DialogActions = withStyles((theme) => ({
-    root: {
-        margin: 0,
-        padding: theme.spacing(1),
-    },
+  root: {
+    margin: 0,
+    padding: theme.spacing(1),
+  },
 }))(MuiDialogActions);
 
 const DialogContent = withStyles((theme) => ({
-    root: {
-        padding: theme.spacing(2),
-    }
+  root: {
+    padding: theme.spacing(2),
+  },
 }))(MuiDialogContent);
 
-const schema = Yup.object(
-    {
-        name: Yup.string().required(),
-        description: Yup.string(),
-    })
+const schema = Yup.object({
+  name: Yup.string().required(),
+  description: Yup.string(),
+});
 
 const ProjectFormFunc = (props) => {
-    const { onClose, item, open, onSubmit } = props;
-    const buttonLabel = item ? "OK" : "Create"
-    const title = item ? "Edit Project" : "Create Project"
-    const handleClose = () => {
-        onClose();
-    };
-    let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" }
+  const { onClose, item, open, onSubmit } = props;
+  const buttonLabel = item ? "OK" : "Create";
+  const title = item ? "Edit Project" : "Create Project";
+  const handleClose = () => {
+    onClose();
+  };
+  let initialValues = item
+    ? { name: item.metadata.name, description: item.metadata.description }
+    : { name: "", description: "" };
 
-    return (
-        <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick>
-            <DialogTitle id="simple-dialog-title">{title}</DialogTitle>
-            <Formik
-                initialValues={initialValues}
-                onSubmit={async values => {
-                    onSubmit(values);
-                }}
-                validationSchema={schema}
-            >
-                {props => {
-                    const {
-                        values,
-                        touched,
-                        errors,
-                        isSubmitting,
-                        handleChange,
-                        handleBlur,
-                        handleSubmit
-                    } = props;
-                    return (
-                        <form noValidate onSubmit={handleSubmit}>
-                            <DialogContent dividers>
-                                <TextField
-                                    style={{ width: "100%", marginBottom: "10px" }}
-                                    id="name"
-                                    label="Project name"
-                                    type="text"
-                                    value={values.name}
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    helperText={(errors.name && touched.name && (
-                                        "Name is required"
-                                    ))}
-                                    required
-                                    error={errors.name && touched.name}
-                                />
-                                <TextField
-                                    style={{ width: "100%", marginBottom: "25px" }}
-                                    name="description"
-                                    value={values.description}
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    id="description"
-                                    label="Description"
-                                    multiline
-                                    rowsMax={4}
-                                />
-                            </DialogContent>
-                            <DialogActions>
-                                <Button autoFocus onClick={handleClose} color="secondary">
-                                    Cancel
-                                </Button>
-                                <Button autoFocus type="submit" color="primary" disabled={isSubmitting}>
-                                    {buttonLabel}
-                                </Button>
-                            </DialogActions>
-                        </form>
-                    );
-                }}
-            </Formik>
-        </Dialog>
-    );
+  return (
+    <Dialog
+      maxWidth={"xs"}
+      onClose={handleClose}
+      aria-labelledby="customized-dialog-title"
+      open={open}
+      disableBackdropClick
+    >
+      <DialogTitle id="simple-dialog-title">{title}</DialogTitle>
+      <Formik
+        initialValues={initialValues}
+        onSubmit={async (values) => {
+          onSubmit(values);
+        }}
+        validationSchema={schema}
+      >
+        {(props) => {
+          const {
+            values,
+            touched,
+            errors,
+            isSubmitting,
+            handleChange,
+            handleBlur,
+            handleSubmit,
+          } = props;
+          return (
+            <form noValidate onSubmit={handleSubmit}>
+              <DialogContent dividers>
+                <TextField
+                  style={{ width: "100%", marginBottom: "10px" }}
+                  id="name"
+                  label="Project name"
+                  type="text"
+                  value={values.name}
+                  onChange={handleChange}
+                  onBlur={handleBlur}
+                  helperText={errors.name && touched.name && "Name is required"}
+                  required
+                  disabled={item}
+                  error={errors.name && touched.name}
+                />
+                <TextField
+                  style={{ width: "100%", marginBottom: "25px" }}
+                  name="description"
+                  value={values.description}
+                  onChange={handleChange}
+                  onBlur={handleBlur}
+                  id="description"
+                  label="Description"
+                  multiline
+                  rowsMax={4}
+                />
+              </DialogContent>
+              <DialogActions>
+                <Button autoFocus onClick={handleClose} color="secondary">
+                  Cancel
+                </Button>
+                <Button
+                  autoFocus
+                  type="submit"
+                  color="primary"
+                  disabled={isSubmitting}
+                >
+                  {buttonLabel}
+                </Button>
+              </DialogActions>
+            </form>
+          );
+        }}
+      </Formik>
+    </Dialog>
+  );
 };
 
 ProjectFormFunc.propTypes = {
-    onClose: PropTypes.func.isRequired,
-    open: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  open: PropTypes.bool.isRequired,
 };
 
 export default ProjectFormFunc;
index d96d44f..fb03155 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React from "react";
 import { withStyles, makeStyles } from "@material-ui/core/styles";
 import Table from "@material-ui/core/Table";
@@ -24,118 +24,142 @@ import Paper from "@material-ui/core/Paper";
 import { Link } from "react-router-dom";
 import IconButton from "@material-ui/core/IconButton";
 import EditIcon from "@material-ui/icons/Edit";
-import DeleteDialog from "../../common/Dialogue"
+import DeleteDialog from "../../common/Dialogue";
 import DeleteIcon from "@material-ui/icons/Delete";
 import ProjectForm from "./ProjectForm";
 import apiService from "../../services/apiService";
 
 const StyledTableCell = withStyles((theme) => ({
-    body: {
-        fontSize: 14,
-    },
+  body: {
+    fontSize: 14,
+  },
 }))(TableCell);
 
 const StyledTableRow = withStyles((theme) => ({
-    root: {
-        "&:nth-of-type(odd)": {
-            backgroundColor: theme.palette.action.hover,
-        },
+  root: {
+    "&:nth-of-type(odd)": {
+      backgroundColor: theme.palette.action.hover,
     },
+  },
 }))(TableRow);
 
 const useStyles = makeStyles({
-    table: {
-        minWidth: 350,
-    },
-    cell: {
-        color: "grey",
-    },
+  table: {
+    minWidth: 350,
+  },
+  cell: {
+    color: "grey",
+  },
 });
 
 export default function ProjectsTable(props) {
-    const classes = useStyles();
-    const [open, setOpen] = React.useState(false);
-    const [openForm, setOpenForm] = React.useState(false);
-    const [index, setIndex] = React.useState(0);
+  const classes = useStyles();
+  const [open, setOpen] = React.useState(false);
+  const [openForm, setOpenForm] = React.useState(false);
+  const [index, setIndex] = React.useState(0);
 
-    let handleEdit = index => {
-        setIndex(index);
-        setOpenForm(true);
-    }
-    const handleClose = el => {
-        if (el.target.innerText === "Delete") {
-            apiService.deleteProject(props.data[index].metadata.name).then(() => {
-                console.log("project deleted");
-                props.data.splice(index, 1);
-                props.setProjectsData([...props.data]);
-            }).catch(err => {
-                console.log("Error deleting project : ", err)
-            })
-        }
-        setOpen(false);
-        setIndex(0);
-    };
-    const handleFormClose = () => {
-        setIndex(0);
-        setOpenForm(false);
-    };
-    const handleDelete = (index) => {
-        setIndex(index);
-        setOpen(true);
-    }
-    const handleSubmit = (data) => {
-        let payload = { "metadata": data }
-        apiService.updateProject(payload).then(res => {
-            props.data[index] = res;
-            props.setProjectsData([...props.data]);
-        }).catch(err => {
-            console.log("Error updating project : ", err);
+  let handleEdit = (index) => {
+    setIndex(index);
+    setOpenForm(true);
+  };
+  const handleClose = (el) => {
+    if (el.target.innerText === "Delete") {
+      apiService
+        .deleteProject(props.data[index].metadata.name)
+        .then(() => {
+          console.log("project deleted");
+          props.data.splice(index, 1);
+          props.setProjectsData([...props.data]);
         })
-        setOpenForm(false);
-    };
-
-    return (
-        <React.Fragment>
-            {(props.data && props.data.length > 0) &&
-                <>
-                    <ProjectForm open={openForm} onClose={handleFormClose} item={props.data[index]} onSubmit={handleSubmit} />
-                    <DeleteDialog open={open} onClose={handleClose} title={"Delete Project"}
-                        content={`Are you sure you want to delete "${props.data[index] ? props.data[index].metadata.name : ""}" ?`} />
-                    <TableContainer component={Paper}>
-                        <Table className={classes.table} size="small">
-                            <TableHead>
-                                <TableRow>
-                                    <StyledTableCell>Name</StyledTableCell>
-                                    <StyledTableCell>Description</StyledTableCell>
-                                    <StyledTableCell>Actions</StyledTableCell>
-                                </TableRow>
-                            </TableHead>
-                            <TableBody>
-                                {props.data.map((row, index) => (
-                                    <StyledTableRow key={row.metadata.name + "" + index}>
-                                        <StyledTableCell>
-                                            {" "}
-                                            <Link to={`/app/projects/${row.metadata.name}`}>{row.metadata.name}</Link>
-                                        </StyledTableCell>
-                                        <StyledTableCell className={classes.cell}>
-                                            {row.metadata.description}
-                                        </StyledTableCell>
-                                        <StyledTableCell className={classes.cell}>
-                                            <IconButton onClick={(e) => handleEdit(index)} title="Edit" >
-                                                <EditIcon color="primary" />
-                                            </IconButton>
-                                            <IconButton onClick={(e) => handleDelete(index)} title="Delete" >
-                                                <DeleteIcon color="secondary" />
-                                            </IconButton>
-                                        </StyledTableCell>
-                                    </StyledTableRow>
-                                ))}
-                            </TableBody>
-                        </Table>
-                    </TableContainer>
-                </>
-            }
+        .catch((err) => {
+          console.log("Error deleting project : ", err);
+        });
+    }
+    setOpen(false);
+    setIndex(0);
+  };
+  const handleFormClose = () => {
+    setIndex(0);
+    setOpenForm(false);
+  };
+  const handleDelete = (index) => {
+    setIndex(index);
+    setOpen(true);
+  };
+  const handleSubmit = (data) => {
+    let payload = { metadata: data };
+    apiService
+      .updateProject(payload)
+      .then((res) => {
+        props.data[index] = res;
+        props.setProjectsData([...props.data]);
+      })
+      .catch((err) => {
+        console.log("Error updating project : ", err);
+      });
+    setOpenForm(false);
+  };
 
-        </React.Fragment>
-    );
+  return (
+    <React.Fragment>
+      {props.data && props.data.length > 0 && (
+        <>
+          <ProjectForm
+            open={openForm}
+            onClose={handleFormClose}
+            item={props.data[index]}
+            onSubmit={handleSubmit}
+          />
+          <DeleteDialog
+            open={open}
+            onClose={handleClose}
+            title={"Delete Project"}
+            content={`Are you sure you want to delete "${
+              props.data[index] ? props.data[index].metadata.name : ""
+            }" ?`}
+          />
+          <TableContainer component={Paper}>
+            <Table className={classes.table} size="small">
+              <TableHead>
+                <TableRow>
+                  <StyledTableCell>Name</StyledTableCell>
+                  <StyledTableCell>Description</StyledTableCell>
+                  <StyledTableCell>Actions</StyledTableCell>
+                </TableRow>
+              </TableHead>
+              <TableBody>
+                {props.data.map((row, index) => (
+                  <StyledTableRow key={row.metadata.name + "" + index}>
+                    <StyledTableCell>
+                      {" "}
+                      <Link to={`/app/projects/${row.metadata.name}`}>
+                        {row.metadata.name}
+                      </Link>
+                    </StyledTableCell>
+                    <StyledTableCell className={classes.cell}>
+                      {row.metadata.description}
+                    </StyledTableCell>
+                    <StyledTableCell className={classes.cell}>
+                      <IconButton
+                        onClick={(e) => handleEdit(index)}
+                        title="Edit"
+                      >
+                        <EditIcon color="primary" />
+                      </IconButton>
+                      <IconButton
+                        onClick={(e) => handleDelete(index)}
+                        title="Delete"
+                      >
+                        <DeleteIcon color="secondary" />
+                      </IconButton>
+                    </StyledTableCell>
+                  </StyledTableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </TableContainer>
+        </>
+      )}
+    </React.Fragment>
+  );
 }
index 5dd3b53..76dc4d8 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React from "react";
 import PropTypes from "prop-types";
 import { ThemeProvider, withStyles } from "@material-ui/core/styles";
@@ -25,6 +25,7 @@ import theme from "../theme/Theme";
 import apiService from "../services/apiService";
 import DeploymentIntentGroups from "../deploymentIntentGroups/DeploymentIntentGroups";
 import { Switch, Route, Link } from "react-router-dom";
+// import Dashboard from "../dashboard/DashboardView";
 
 const drawerWidth = 256;
 const styles = {
@@ -45,11 +46,10 @@ const styles = {
   },
   main: {
     flex: 1,
-    padding: theme.spacing(6, 4),
+    padding: theme.spacing(3, 4, 6, 4),
     background: "#eaeff1",
   },
   footer: {
-    // padding: theme.spacing(2),
     background: "#eaeff1",
   },
 };
@@ -63,17 +63,6 @@ class AppBase extends React.Component {
     };
   }
 
-  componentDidMount() {
-    apiService
-      .getCompositeApps({ projectName: this.state.projectName })
-      .then((response) => {
-        this.setState({ data: response });
-      })
-      .catch((err) => {
-        console.log("Unable to get composite apps");
-      })
-      .finally();
-  }
   setMobileOpen = (mobileOpen) => {
     this.setState({ mobileOpen });
   };
@@ -114,15 +103,15 @@ class AppBase extends React.Component {
                       path={`${this.props.match.url}/404`}
                       component={() => <div>Page Not found</div>}
                     />
-                    <Route
-                      exact
-                      path={`${this.props.match.url}/composite-apps`}
-                    >
+                    {/* <Route exact path={`${this.props.match.url}/dashboard`}>
+                      <Dashboard projectName={this.state.projectName} />
+                    </Route> */}
+                    <Route exact path={`${this.props.match.url}/services`}>
                       <CompositeApps projectName={this.state.projectName} />
                     </Route>
                     <Route
                       exact
-                      path={`${this.props.match.url}/composite-apps/:appname/:version`}
+                      path={`${this.props.match.url}/services/:appname/:version`}
                     >
                       <CompositeApp projectName={this.state.projectName} />
                     </Route>
index 2c907ac..eff9a56 100644 (file)
@@ -95,7 +95,7 @@ function Content(props) {
       </AppBar>
       <div className={classes.contentWrapper}>
         <Typography color="textSecondary" align="center">
-          No composite apps for this project yet
+          No services for this project yet
         </Typography>
       </div>
     </Paper>
index 0222151..19f148a 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React from "react";
 import PropTypes from "prop-types";
 import AppBar from "@material-ui/core/AppBar";
@@ -57,18 +57,18 @@ function Header(props) {
 
   let headerName = "";
   let getHeaderName = () => {
-    if (location.pathname === `${props.match.url}/composite-apps`) {
-      headerName = "Composite Apps";
+    if (location.pathname === `${props.match.url}/dashboard`) {
+      headerName = "Dashboard";
+    } else if (location.pathname === `${props.match.url}/services`) {
+      headerName = "Services";
     } else if (
       location.pathname === `${props.match.url}/deployment-intent-group`
     ) {
       headerName = "Deployment Intent Groups";
-    } else if (location.pathname.includes("composite-apps")) {
+    } else if (location.pathname.includes("services")) {
       headerName =
-        "Composite Apps / " +
-        location.pathname
-          .slice(location.pathname.indexOf("composite-apps"))
-          .slice(15);
+        "services / " +
+        location.pathname.slice(location.pathname.indexOf("services")).slice(9);
     } else if (location.pathname === `${props.match.url}/projects`) {
       headerName = "Projects";
     } else if (location.pathname === `${props.match.url}/clusters`) {
index 2df2c00..e8f1636 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import PropTypes from "prop-types";
 import clsx from "clsx";
@@ -23,7 +23,7 @@ import ListItem from "@material-ui/core/ListItem";
 import ListItemIcon from "@material-ui/core/ListItemIcon";
 import ListItemText from "@material-ui/core/ListItemText";
 import HomeIcon from "@material-ui/icons/Home";
-import AppsIcon from "@material-ui/icons/Apps";
+import DeviceHubIcon from "@material-ui/icons/DeviceHub";
 import DnsRoundedIcon from "@material-ui/icons/DnsRounded";
 import { withRouter, Link } from "react-router-dom";
 
@@ -32,9 +32,9 @@ const categories = [
     id: "1",
     children: [
       {
-        id: "Composite Apps",
-        icon: <AppsIcon />,
-        url: "/composite-apps",
+        id: "Services",
+        icon: <DeviceHubIcon />,
+        url: "/services",
       },
       {
         id: "Deployment Intent Groups",
@@ -85,8 +85,8 @@ const styles = (theme) => ({
     marginTop: theme.spacing(2),
   },
   version: {
-    fontSize: "15px"
-  }
+    fontSize: "15px",
+  },
 });
 
 function Navigator(props) {
@@ -99,11 +99,20 @@ function Navigator(props) {
     setActiveTab(location.pathname);
   }
   return (
-    <Drawer PaperProps={props.PaperProps} variant={props.variant} open={props.open} onClose={props.onClose}>
+    <Drawer
+      PaperProps={props.PaperProps}
+      variant={props.variant}
+      open={props.open}
+      onClose={props.onClose}
+    >
       <List disablePadding>
-        <Link style={{ textDecoration: "none" }} to='/'>
+        <Link style={{ textDecoration: "none" }} to="/">
           <ListItem
-            className={clsx(classes.firebase, classes.item, classes.itemCategory)}
+            className={clsx(
+              classes.firebase,
+              classes.item,
+              classes.itemCategory
+            )}
           >
             <ListItemText
               classes={{
@@ -111,14 +120,29 @@ function Navigator(props) {
               }}
             >
               ONAP4K8s
-              </ListItemText>
-            <span
-              className={clsx(classes.version)}
-            >{process.env.REACT_APP_VERSION}</span>
+            </ListItemText>
+            <span className={clsx(classes.version)}>
+              {process.env.REACT_APP_VERSION}
+            </span>
           </ListItem>
         </Link>
 
-        <ListItem className={clsx(classes.item, classes.itemCategory)}>
+        {/* <Link
+          style={{ textDecoration: "none" }}
+          to={{
+            pathname: `${props.match.url}/dashboard`,
+            activeItem: "childId",
+          }}
+          key={"childId"}
+        > */}
+        <ListItem
+          button
+          className={clsx(
+            classes.item,
+            classes.itemCategory,
+            activeItem.includes("dashboard") && classes.itemActiveItem
+          )}
+        >
           <ListItemIcon className={classes.itemIcon}>
             <HomeIcon />
           </ListItemIcon>
@@ -130,10 +154,18 @@ function Navigator(props) {
             Dashboard
           </ListItemText>
         </ListItem>
+        {/* </Link> */}
         {categories.map(({ id, children }) => (
           <React.Fragment key={id}>
             {children.map(({ id: childId, icon, url }) => (
-              <Link style={{ textDecoration: "none" }} to={{ pathname: `${props.match.url}${url}`, activeItem: childId }} key={childId}>
+              <Link
+                style={{ textDecoration: "none" }}
+                to={{
+                  pathname: `${props.match.url}${url}`,
+                  activeItem: childId,
+                }}
+                key={childId}
+              >
                 <ListItem
                   button
                   className={clsx(
diff --git a/src/tools/emcoui/src/assets/icons/empty.svg b/src/tools/emcoui/src/assets/icons/empty.svg
new file mode 100644 (file)
index 0000000..f4e020e
--- /dev/null
@@ -0,0 +1 @@
+<svg enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/><path d="m16.807 17h-9.614c-.622 0-1.186-.391-1.404-.973l-1.014-2.703c-.072-.194-.26-.324-.468-.324h-3.557c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h3.557c.622 0 1.186.391 1.405.973l1.013 2.703c.073.194.261.324.468.324h9.613c.208 0 .396-.13.468-.324l1.013-2.703c.22-.582.784-.973 1.406-.973h3.807c.276 0 .5.224.5.5s-.224.5-.5.5h-3.807c-.208 0-.396.13-.468.324l-1.013 2.703c-.219.582-.784.973-1.405.973z"/></svg>
\ No newline at end of file
diff --git a/src/tools/emcoui/src/common/ExpandableCard.jsx b/src/tools/emcoui/src/common/ExpandableCard.jsx
new file mode 100644 (file)
index 0000000..1d2ea9e
--- /dev/null
@@ -0,0 +1,94 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+
+import React, { useState } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import clsx from "clsx";
+import Card from "@material-ui/core/Card";
+import CardHeader from "@material-ui/core/CardHeader";
+import CardContent from "@material-ui/core/CardContent";
+import Collapse from "@material-ui/core/Collapse";
+import IconButton from "@material-ui/core/IconButton";
+import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
+import StorageIcon from "@material-ui/icons/Storage";
+import ErrorIcon from "@material-ui/icons/Error";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    width: "100%",
+  },
+  expand: {
+    transform: "rotate(0deg)",
+    marginLeft: "auto",
+    transition: theme.transitions.create("transform", {
+      duration: theme.transitions.duration.shortest,
+    }),
+  },
+  expandOpen: {
+    transform: "rotate(180deg)",
+  },
+}));
+const ExpandableCard = (props) => {
+  const classes = useStyles();
+  const [expanded, setExpanded] = useState(false);
+
+  const handleExpandClick = () => {
+    if (!expanded) {
+      setExpanded(!expanded);
+    } else {
+      setExpanded(!expanded);
+    }
+  };
+
+  return (
+    <>
+      <Card className={classes.root}>
+        <CardHeader
+          onClick={handleExpandClick}
+          avatar={
+            <>
+              <StorageIcon fontSize="large" />
+            </>
+          }
+          action={
+            <>
+              {props.error && (
+                <ErrorIcon color="error" style={{ verticalAlign: "middle" }} />
+              )}
+              <IconButton
+                className={clsx(classes.expand, {
+                  [classes.expandOpen]: expanded,
+                })}
+                onClick={handleExpandClick}
+                aria-expanded={expanded}
+              >
+                <ExpandMoreIcon />
+              </IconButton>
+            </>
+          }
+          title={props.title}
+          subheader={props.description}
+        />
+        <Collapse in={expanded} timeout="auto" unmountOnExit>
+          <CardContent>{props.content}</CardContent>
+        </Collapse>
+      </Card>
+    </>
+  );
+};
+
+ExpandableCard.propTypes = {};
+
+export default ExpandableCard;
index 847951e..97d34bc 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
-import React from 'react';
-import PropTypes from 'prop-types';
+// ========================================================================
+import React from "react";
+import PropTypes from "prop-types";
 import FileCopyIcon from "@material-ui/icons/FileCopy";
 import CloudUploadIcon from "@material-ui/icons/CloudUpload";
-import './fileUpload.css'
+import "./fileUpload.css";
 
 const FileUpload = (props) => {
-    return (
-        <>
-            <div className="file-upload">
-                <div
-                    className="file-upload-wrap"
-                    style={{
-                        border: props.file && props.file.name && "2px dashed rgba(0, 131, 143, 1)"
-                    }}
-                >
-                    <input
-                        required
-                        className="file-upload-input"
-                        type="file"
-                        accept={props.accept ? props.accept : "*"}
-                        name="file"
-                        onBlur={props.handleBlur ? props.handleBlur : null}
-                        onChange={(event) => {
-                            props.setFieldValue("file", event.currentTarget.files[0]);
-                        }}
-                    />
+  return (
+    <>
+      <div className="file-upload">
+        <div
+          className="file-upload-wrap"
+          style={{
+            border:
+              props.file &&
+              props.file.name &&
+              "2px dashed rgba(0, 131, 143, 1)",
+          }}
+        >
+          <input
+            required
+            className="file-upload-input"
+            type="file"
+            accept={props.accept ? props.accept : "*"}
+            name="file"
+            onBlur={props.handleBlur ? props.handleBlur : null}
+            onChange={(event) => {
+              props.setFieldValue(props.name, event.currentTarget.files[0]);
+            }}
+          />
 
-                    <div className="file-upload-text">
-                        {(props.file && props.file.name) ? (<>
-                            <span>
-                                <FileCopyIcon color="primary" />
-                            </span>
-                            <span style={{ fontWeight: 600 }}>{props.file.name}</span>
-                        </>) : (<>
-                            <span>
-                                <CloudUploadIcon />
-                            </span>
-                            <span>
-                                Drag And Drop or Click To Upload
-                             </span>
-                        </>)}
-                    </div>
-                </div>
-            </div>
-        </>);
+          <div className="file-upload-text">
+            {props.file && props.file.name ? (
+              <>
+                <span>
+                  <FileCopyIcon color="primary" />
+                </span>
+                <span style={{ fontWeight: 600 }}>{props.file.name}</span>
+              </>
+            ) : (
+              <>
+                <span>
+                  <CloudUploadIcon />
+                </span>
+                <span>Drag And Drop or Click To Upload</span>
+              </>
+            )}
+          </div>
+        </div>
+      </div>
+    </>
+  );
 };
 
 FileUpload.propTypes = {
-    handleBlur: PropTypes.func,
-    setFieldValue: PropTypes.func.isRequired,
+  handleBlur: PropTypes.func,
+  setFieldValue: PropTypes.func.isRequired,
 };
 
 export default FileUpload;
index e9fe3a2..6e8eee2 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
+// ========================================================================
+import React from "react";
+import PropTypes from "prop-types";
+import { withStyles } from "@material-ui/core/styles";
+import Button from "@material-ui/core/Button";
 
-import Dialog from '@material-ui/core/Dialog';
-import MuiDialogTitle from '@material-ui/core/DialogTitle';
-import MuiDialogContent from '@material-ui/core/DialogContent';
-import MuiDialogActions from '@material-ui/core/DialogActions';
-import IconButton from '@material-ui/core/IconButton';
-import CloseIcon from '@material-ui/icons/Close';
-import Typography from '@material-ui/core/Typography';
-import { TextField } from '@material-ui/core';
+import Dialog from "@material-ui/core/Dialog";
+import MuiDialogTitle from "@material-ui/core/DialogTitle";
+import MuiDialogContent from "@material-ui/core/DialogContent";
+import MuiDialogActions from "@material-ui/core/DialogActions";
+import IconButton from "@material-ui/core/IconButton";
+import CloseIcon from "@material-ui/icons/Close";
+import Typography from "@material-ui/core/Typography";
+import { TextField } from "@material-ui/core";
 import * as Yup from "yup";
-import { Formik } from 'formik';
+import { Formik } from "formik";
 
 const styles = (theme) => ({
-    root: {
-        margin: 0,
-        padding: theme.spacing(2),
-    },
-    closeButton: {
-        position: 'absolute',
-        right: theme.spacing(1),
-        top: theme.spacing(1),
-        color: theme.palette.grey[500],
-    },
+  root: {
+    margin: 0,
+    padding: theme.spacing(2),
+  },
+  closeButton: {
+    position: "absolute",
+    right: theme.spacing(1),
+    top: theme.spacing(1),
+    color: theme.palette.grey[500],
+  },
 });
 
 const DialogTitle = withStyles(styles)((props) => {
-    const { children, classes, onClose, ...other } = props;
-    return (
-        <MuiDialogTitle disableTypography className={classes.root} {...other}>
-            <Typography variant="h6">{children}</Typography>
-            {onClose ? (
-                <IconButton className={classes.closeButton} onClick={onClose}>
-                    <CloseIcon />
-                </IconButton>
-            ) : null}
-        </MuiDialogTitle>
-    );
+  const { children, classes, onClose, ...other } = props;
+  return (
+    <MuiDialogTitle disableTypography className={classes.root} {...other}>
+      <Typography variant="h6">{children}</Typography>
+      {onClose ? (
+        <IconButton className={classes.closeButton} onClick={onClose}>
+          <CloseIcon />
+        </IconButton>
+      ) : null}
+    </MuiDialogTitle>
+  );
 });
 
 const DialogActions = withStyles((theme) => ({
-    root: {
-        margin: 0,
-        padding: theme.spacing(1),
-    },
+  root: {
+    margin: 0,
+    padding: theme.spacing(1),
+  },
 }))(MuiDialogActions);
 
 const DialogContent = withStyles((theme) => ({
-    root: {
-        padding: theme.spacing(2),
-    }
+  root: {
+    padding: theme.spacing(2),
+  },
 }))(MuiDialogContent);
 
-const schema = Yup.object(
-    {
-        name: Yup.string().required(),
-        description: Yup.string(),
-    })
+const schema = Yup.object({
+  name: Yup.string().required(),
+  description: Yup.string(),
+});
 
 const CreateForm = (props) => {
-    const { onClose, item, open, onSubmit } = props;
-    const buttonLabel = item ? "OK" : "Create"
-    const title = item ? "Edit" : "Create"
-    const handleClose = () => {
-        onClose();
-    };
-    let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" }
+  const { onClose, item, open, onSubmit } = props;
+  const buttonLabel = item ? "OK" : "Create";
+  const title = item ? "Edit" : "Create";
+  const handleClose = () => {
+    onClose();
+  };
+  let initialValues = item
+    ? { name: item.metadata.name, description: item.metadata.description }
+    : { name: "", description: "" };
 
-    return (
-        <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick>
-            <DialogTitle id="simple-dialog-title">{title}</DialogTitle>
-            <Formik
-                initialValues={initialValues}
-                onSubmit={async values => {
-                    onSubmit(values);
-                }}
-                validationSchema={schema}
-            >
-                {props => {
-                    const {
-                        touched,
-                        errors,
-                        isSubmitting,
-                        handleChange,
-                        handleBlur,
-                        handleSubmit
-                    } = props;
-                    return (
-                        <form noValidate onSubmit={handleSubmit}>
-                            <DialogContent dividers>
-                                <TextField
-                                    style={{ width: "100%", marginBottom: "10px" }}
-                                    id="name"
-                                    label="Name"
-                                    name="name"
-                                    type="text"
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    helperText={(errors.name && touched.name && (
-                                        "Name is required"
-                                    ))}
-                                    required
-                                    error={errors.name && touched.name}
-                                />
-                                <TextField
-                                    style={{ width: "100%", marginBottom: "25px" }}
-                                    name="description"
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    id="description"
-                                    label="Description"
-                                    multiline
-                                    rowsMax={4}
-                                />
-                            </DialogContent>
-                            <DialogActions>
-                                <Button autoFocus onClick={handleClose} color="secondary">
-                                    Cancel
-                                </Button>
-                                <Button autoFocus type="submit" color="primary" disabled={isSubmitting}>
-                                    {buttonLabel}
-                                </Button>
-                            </DialogActions>
-                        </form>
-                    );
-                }}
-            </Formik>
-        </Dialog>
-    );
+  return (
+    <Dialog
+      maxWidth={"xs"}
+      onClose={handleClose}
+      aria-labelledby="customized-dialog-title"
+      open={open}
+      disableBackdropClick
+    >
+      <DialogTitle id="simple-dialog-title">{title}</DialogTitle>
+      <Formik
+        initialValues={initialValues}
+        onSubmit={async (values) => {
+          onSubmit(values);
+        }}
+        validationSchema={schema}
+      >
+        {(props) => {
+          const {
+            touched,
+            errors,
+            isSubmitting,
+            handleChange,
+            handleBlur,
+            handleSubmit,
+            submitCount,
+          } = props;
+          return (
+            <form noValidate onSubmit={handleSubmit}>
+              <DialogContent dividers>
+                <TextField
+                  style={{ width: "100%", marginBottom: "10px" }}
+                  id="name"
+                  label="Name"
+                  name="name"
+                  type="text"
+                  onChange={handleChange}
+                  onBlur={handleBlur}
+                  helperText={errors.name && touched.name && "Name is required"}
+                  required
+                  error={errors.name && touched.name}
+                />
+                <TextField
+                  style={{ width: "100%", marginBottom: "25px" }}
+                  name="description"
+                  onChange={handleChange}
+                  onBlur={handleBlur}
+                  id="description"
+                  label="Description"
+                  multiline
+                  rowsMax={4}
+                />
+              </DialogContent>
+              <DialogActions>
+                <Button autoFocus onClick={handleClose} color="secondary">
+                  Cancel
+                </Button>
+                <Button
+                  autoFocus
+                  type="submit"
+                  color="primary"
+                  disabled={isSubmitting || submitCount > 0}
+                >
+                  {buttonLabel}
+                </Button>
+              </DialogActions>
+            </form>
+          );
+        }}
+      </Formik>
+    </Dialog>
+  );
 };
 
 CreateForm.propTypes = {
-    onClose: PropTypes.func.isRequired,
-    open: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  open: PropTypes.bool.isRequired,
 };
 
 export default CreateForm;
index 34d07fb..8b2c2b1 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React from "react";
 import Tab from "@material-ui/core/Tab";
 import Tabs from "@material-ui/core/Tabs";
@@ -19,15 +19,15 @@ import Paper from "@material-ui/core/Paper";
 import { withStyles } from "@material-ui/core/styles";
 import Box from "@material-ui/core/Box";
 import PropTypes from "prop-types";
-import Apps from "../compositeApps/apps/Apps";
-import CompositeProfiles from "../compositeApps/compositeProfiles/CompositeProfiles";
-import Intents from "../compositeApps/intents/GenericPlacementIntents";
 import BackIcon from "@material-ui/icons/ArrowBack";
 import { withRouter } from "react-router-dom";
 import { IconButton } from "@material-ui/core";
 import apiService from "../services/apiService";
 import Spinner from "../common/Spinner";
-import NetworkIntent from "../networkIntents/NetworkIntents";
+import Apps from "../compositeApps/apps/Apps";
+import CompositeProfiles from "../compositeApps/compositeProfiles/CompositeProfiles";
+// import Intents from "../compositeApps/intents/GenericPlacementIntents";
+// import NetworkIntent from "../networkIntents/NetworkIntents";
 
 const lightColor = "rgba(255, 255, 255, 0.7)";
 
@@ -134,8 +134,6 @@ class CompositeApp extends React.Component {
           >
             <Tab label="Apps" />
             <Tab label="Composite Profiles" />
-            <Tab label="Generic Placement Intents" />
-            <Tab label="Network Controller Intents" />
           </Tabs>
           {this.state.isLoading && <Spinner />}
 
@@ -158,22 +156,6 @@ class CompositeApp extends React.Component {
                   appsData={this.state.appsData}
                 />
               </TabPanel>
-              <TabPanel value={this.state.activeTab} index={2}>
-                <Intents
-                  projectName={this.props.projectName}
-                  compositeAppName={this.state.compositeAppName}
-                  compositeAppVersion={this.state.compositeAppVersion}
-                  appsData={this.state.appsData}
-                />
-              </TabPanel>
-              <TabPanel value={this.state.activeTab} index={3}>
-                <NetworkIntent
-                  projectName={this.props.projectName}
-                  compositeAppName={this.state.compositeAppName}
-                  compositeAppVersion={this.state.compositeAppVersion}
-                  appsData={this.state.appsData}
-                />
-              </TabPanel>
             </>
           )}
         </Paper>
@@ -181,7 +163,5 @@ class CompositeApp extends React.Component {
     );
   }
 }
-
 CompositeApp.propTypes = {};
-
 export default withStyles(styles)(withRouter(CompositeApp));
index 220d7df..926ab54 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import { withStyles, makeStyles } from "@material-ui/core/styles";
 import Table from "@material-ui/core/Table";
@@ -21,13 +21,15 @@ import TableContainer from "@material-ui/core/TableContainer";
 import TableHead from "@material-ui/core/TableHead";
 import TableRow from "@material-ui/core/TableRow";
 import Paper from "@material-ui/core/Paper";
-import { Link } from "react-router-dom";
+// import { Link } from "react-router-dom";
 import IconButton from "@material-ui/core/IconButton";
 import EditIcon from "@material-ui/icons/Edit";
-import DeleteIcon from '@material-ui/icons/Delete';
+import DeleteIcon from "@material-ui/icons/Delete";
 import CreateCompositeAppForm from "./dialogs/CompositeAppForm";
 import apiService from "../services/apiService";
 import DeleteDialog from "../common/Dialogue";
+import { Link, withRouter } from "react-router-dom";
+import Notification from "../common/Notification";
 
 const StyledTableCell = withStyles((theme) => ({
   body: {
@@ -52,50 +54,80 @@ const useStyles = makeStyles({
   },
 });
 
-export default function CustomizedTables({ data, ...props }) {
+function CustomizedTables({ data, ...props }) {
   const classes = useStyles();
   const [openForm, setOpenForm] = useState(false);
   const [activeRowIndex, setActiveRowIndex] = useState(0);
   const [row, setRow] = useState({});
   const [open, setOpen] = useState(false);
+  const [notificationDetails, setNotificationDetails] = useState({});
   let onEditCompositeApp = (row, index) => {
     setActiveRowIndex(index);
     setRow(row);
     setOpenForm(true);
-  }
+    // props.history.push(`services/${row.metadata.name}/${row.spec.version}`);
+  };
   const handleCloseForm = (fields) => {
     if (fields) {
-      let request = { payload: { name: fields.name, description: fields.description, spec: { version: fields.version } }, projectName: props.projectName, compositeAppVersion: row.spec.version };
-      apiService.updateCompositeApp(request).then(res => {
-        let updatedData = data.slice();
-        updatedData.splice(activeRowIndex, 1);
-        updatedData.push(res);
-        props.handleUpdateState(updatedData);
-      }).catch(err => {
-        console.log("error creating composite app : ", err)
-      }).finally(() => {
-        setOpenForm(false);
-      });
-    }
-    else {
+      let request = {
+        payload: {
+          name: fields.name,
+          description: fields.description,
+          spec: { version: fields.version },
+        },
+        projectName: props.projectName,
+        compositeAppVersion: row.spec.version,
+      };
+      apiService
+        .updateCompositeApp(request)
+        .then((res) => {
+          let updatedData = data.slice();
+          updatedData.splice(activeRowIndex, 1);
+          updatedData.push(res);
+          props.handleUpdateState(updatedData);
+        })
+        .catch((err) => {
+          console.log("error creating composite app : ", err);
+        })
+        .finally(() => {
+          setOpenForm(false);
+        });
+    } else {
       setOpenForm(false);
     }
   };
   const handleDeleteCompositeApp = (index) => {
     setActiveRowIndex(index);
     setOpen(true);
-  }
-  const handleClose = el => {
+  };
+  const handleClose = (el) => {
     if (el.target.innerText === "Delete") {
-      let request = { projectName: props.projectName, compositeAppName: data[activeRowIndex].metadata.name, compositeAppVersion: data[activeRowIndex].spec.version };
-      apiService.deleteCompositeApp(request).then(() => {
-        console.log("cluster deleted");
-        data.splice(activeRowIndex, 1);
-        let updatedData = data.slice();
-        props.handleUpdateState(updatedData);
-      }).catch(err => {
-        console.log("Error deleting cluster : ", err)
-      })
+      let request = {
+        projectName: props.projectName,
+        compositeAppName: data[activeRowIndex].metadata.name,
+        compositeAppVersion: data[activeRowIndex].spec.version,
+      };
+      apiService
+        .deleteCompositeApp(request)
+        .then(() => {
+          console.log("cluster deleted");
+          data.splice(activeRowIndex, 1);
+          let updatedData = data.slice();
+          props.handleUpdateState(updatedData);
+        })
+        .catch((err) => {
+          console.log("Error deleting cluster : ", err);
+          let message = "Error deleting service";
+          if (err.response.data.includes("Non emtpy DIG in service")) {
+            message =
+              "Error deleting service : please delete deployment intent group first";
+          }
+          setNotificationDetails({
+            show: true,
+            message: message,
+            severity: "error",
+          });
+        });
     }
     setOpen(false);
     setActiveRowIndex(0);
@@ -103,11 +135,22 @@ export default function CustomizedTables({ data, ...props }) {
 
   return (
     <>
-      {data && (data.length > 0) &&
-        (<>
-          <CreateCompositeAppForm open={openForm} handleClose={handleCloseForm} item={row} />
-          <DeleteDialog open={open} onClose={handleClose} title={"Delete Cluster"}
-            content={`Are you sure you want to delete "${data[activeRowIndex] ? data[activeRowIndex].metadata.name : ""}" ?`} />
+      <Notification notificationDetails={notificationDetails} />
+      {data && data.length > 0 && (
+        <>
+          <CreateCompositeAppForm
+            open={openForm}
+            handleClose={handleCloseForm}
+            item={row}
+          />
+          <DeleteDialog
+            open={open}
+            onClose={handleClose}
+            title={"Delete Service"}
+            content={`Are you sure you want to delete "${
+              data[activeRowIndex] ? data[activeRowIndex].metadata.name : ""
+            }" ?`}
+          />
           <TableContainer component={Paper}>
             <Table className={classes.table} size="small">
               <TableHead>
@@ -122,8 +165,12 @@ export default function CustomizedTables({ data, ...props }) {
                 {data.map((row, index) => (
                   <StyledTableRow key={row.metadata.name}>
                     <StyledTableCell>
-                      {" "}
-                      <Link to={`composite-apps/${row.metadata.name}/${row.spec.version}`}>{row.metadata.name}</Link>
+                      <Link
+                        to={`services/${row.metadata.name}/${row.spec.version}`}
+                      >
+                        {row.metadata.name}
+                      </Link>
+                      {/* {row.metadata.name} */}
                     </StyledTableCell>
                     <StyledTableCell className={classes.cell}>
                       {row.metadata.description}
@@ -132,10 +179,18 @@ export default function CustomizedTables({ data, ...props }) {
                       {row.spec.version}
                     </StyledTableCell>
                     <StyledTableCell className={classes.cell}>
-                      <IconButton onClick={(e) => onEditCompositeApp(row, index)} title="Edit">
+                      {/* <IconButton
+                        onClick={(e) => onEditCompositeApp(row, index)}
+                        title="Edit"
+                      >
                         <EditIcon color="primary" />
-                      </IconButton>
-                      <IconButton color="secondary" onClick={() => { handleDeleteCompositeApp(index) }}>
+                      </IconButton> */}
+                      <IconButton
+                        color="secondary"
+                        onClick={() => {
+                          handleDeleteCompositeApp(index);
+                        }}
+                      >
                         <DeleteIcon />
                       </IconButton>
                     </StyledTableCell>
@@ -144,7 +199,11 @@ export default function CustomizedTables({ data, ...props }) {
               </TableBody>
             </Table>
           </TableContainer>
-        </>)}
-      {(!data || (data.length === 0)) && (<span>No Clusters</span>)}
-    </>)
+        </>
+      )}
+      {(!data || data.length === 0) && <span>No Composite Apps</span>}
+    </>
+  );
 }
+
+export default withRouter(CustomizedTables);
index e7901ff..5c54003 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React from "react";
 import CompositeAppTable from "./CompositeAppTable";
-import { withStyles, Button, Grid } from "@material-ui/core";
+import { withStyles, Button, Grid, Typography } from "@material-ui/core";
 import CreateCompositeAppForm from "./dialogs/CompositeAppForm";
 import AddIcon from "@material-ui/icons/Add";
 import apiService from "../services/apiService";
 import Spinner from "../common/Spinner";
+import { ReactComponent as EmptyIcon } from "../assets/icons/empty.svg";
 const styles = {
   root: {
     display: "flex",
@@ -57,22 +58,43 @@ class CompositeApps extends React.Component {
 
   handleClose = (fields) => {
     if (fields) {
-      let request = {
-        payload: {
-          metadata: { name: fields.name, description: fields.description },
-          spec: { version: fields.version },
-        },
-        projectName: this.props.projectName,
+      const formData = new FormData();
+      let appsData = [];
+      fields.apps.forEach((app) => {
+        //add files for each app
+        formData.append(`${app.appName}_file`, app.file);
+        formData.append(`${app.appName}_profile`, app.profilePackageFile);
+        appsData.push({
+          metadata: {
+            name: app.appName,
+            description: app.description ? app.description : "na",
+            filename: app.file.name,
+          },
+          profileMetadata: {
+            name: `${app.appName}_profile`,
+            filename: app.profilePackageFile.name,
+          },
+          clusters: app.clusters,
+        });
+      });
+
+      let servicePayload = {
+        name: fields.name,
+        description: fields.description,
+        spec: { projectName: this.props.projectName, appsData },
       };
+      formData.append("servicePayload", JSON.stringify(servicePayload));
+      let request = { projectName: this.props.projectName, payload: formData };
       apiService
-        .createCompositeApp(request)
+        .addService(request)
         .then((res) => {
+          console.log("create service response : " + res);
           if (this.state.data && this.state.data.length > 0)
             this.setState({ data: [...this.state.data, res] });
           else this.setState({ data: [res] });
         })
         .catch((err) => {
-          console.log("error creating composite app : ", err);
+          console.log("error adding app : ", err);
         });
     }
     this.setState({ open: false });
@@ -88,32 +110,48 @@ class CompositeApps extends React.Component {
         {this.state.isLoading && <Spinner />}
         {!this.state.isLoading && (
           <>
-            <Button
-              variant="outlined"
-              color="primary"
-              startIcon={<AddIcon />}
-              onClick={this.handleCreateCompositeApp}
-            >
-              Create Composite App
-            </Button>
             <CreateCompositeAppForm
               open={this.state.open}
               handleClose={this.handleClose}
             />
             <Grid container spacing={2} alignItems="center">
-              <Grid item xs style={{ marginTop: "20px" }}>
-                {this.state.data && this.state.data.length > 0 && (
+              <Grid item xs={12}>
+                <Button
+                  variant="outlined"
+                  color="primary"
+                  startIcon={<AddIcon />}
+                  onClick={this.handleCreateCompositeApp}
+                >
+                  Add service
+                </Button>
+              </Grid>
+              {this.state.data && this.state.data.length > 0 && (
+                <Grid item xs={12}>
                   <CompositeAppTable
                     data={this.state.data}
                     projectName={this.props.projectName}
                     handleUpdateState={this.handleUpdateState}
                   />
-                )}
-                {(!this.state.data || this.state.data.length === 0) && (
-                  <span>No Composite Apps</span>
-                )}
-              </Grid>
+                </Grid>
+              )}
             </Grid>
+            {(!this.state.data || this.state.data.length === 0) && (
+              <Grid
+                container
+                spacing={2}
+                direction="column"
+                alignItems="center"
+              >
+                <Grid style={{ marginTop: "60px" }} item xs={6}>
+                  <EmptyIcon style={{ height: "100px", width: "100px" }} />
+                </Grid>
+                <Grid item xs={12}>
+                  <Typography variant="h6">
+                    No service found, start by adding a service
+                  </Typography>
+                </Grid>
+              </Grid>
+            )}
           </>
         )}
       </>
index 14be60c..0e3638b 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import { makeStyles } from "@material-ui/core/styles";
 import Card from "@material-ui/core/Card";
@@ -19,7 +19,7 @@ import CardContent from "@material-ui/core/CardContent";
 import IconButton from "@material-ui/core/IconButton";
 import Typography from "@material-ui/core/Typography";
 import DeleteIcon from "@material-ui/icons/Delete";
-import EditIcon from "@material-ui/icons/Edit";
+// import EditIcon from "@material-ui/icons/Edit";
 import { Grid, Button, Tooltip } from "@material-ui/core";
 import AddIcon from "@material-ui/icons/Add";
 import apiService from "../../services/apiService";
@@ -144,7 +144,7 @@ const Apps = ({ data, onStateChange, ...props }) => {
   };
   return (
     <>
-      <Button
+      {/* <Button
         variant="outlined"
         color="primary"
         startIcon={<AddIcon />}
@@ -152,7 +152,7 @@ const Apps = ({ data, onStateChange, ...props }) => {
         size="small"
       >
         Add App
-      </Button>
+      </Button> */}
       <AppForm
         open={formOpen}
         onClose={handleFormClose}
@@ -200,6 +200,7 @@ const Apps = ({ data, onStateChange, ...props }) => {
                     </Typography>
                   </CardContent>
                   <div className={classes.controls}>
+                    {/* //edit app api is not implemented yet
                     <IconButton
                       onClick={handleEditApp.bind(this, value)}
                       color="primary"
@@ -212,7 +213,7 @@ const Apps = ({ data, onStateChange, ...props }) => {
                       onClick={() => handleDeleteApp(index)}
                     >
                       <DeleteIcon />
-                    </IconButton>
+                    </IconButton> */}
                   </div>
                 </div>
               </Card>
index 58161e8..d565eba 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import { makeStyles } from "@material-ui/core/styles";
 import clsx from "clsx";
@@ -35,7 +35,7 @@ import TableCell from "@material-ui/core/TableCell";
 import Paper from "@material-ui/core/Paper";
 import TableBody from "@material-ui/core/TableBody";
 import apiService from "../../services/apiService";
-import EditIcon from "@material-ui/icons/Edit";
+// import EditIcon from "@material-ui/icons/Edit";
 import DeleteIcon from "@material-ui/icons/Delete";
 import ProfileForm from "./ProfileForm";
 import DeleteDialog from "../../common/Dialogue";
@@ -196,7 +196,7 @@ export default function RecipeReviewCard(props) {
         />
         <Collapse in={expanded} timeout="auto" unmountOnExit>
           <CardContent>
-            <Button
+            {/* <Button
               disabled={!(props.appsData && props.appsData.length > 0)}
               variant="outlined"
               size="small"
@@ -208,11 +208,12 @@ export default function RecipeReviewCard(props) {
               }}
             >
               Add Profile
-            </Button>
-            <Button
+            </Button> */}
+            {/* <Button
               variant="outlined"
               size="small"
               color="secondary"
+              disabled={data && data.length > 0}
               style={{ float: "right" }}
               startIcon={<DeleteIcon />}
               onClick={() => {
@@ -220,7 +221,7 @@ export default function RecipeReviewCard(props) {
               }}
             >
               Delete Composite Profile
-            </Button>
+            </Button> */}
             {data && data.length > 0 && (
               <>
                 <DeleteDialog
@@ -238,7 +239,7 @@ export default function RecipeReviewCard(props) {
                         <StyledTableCell>Name</StyledTableCell>
                         <StyledTableCell>Description</StyledTableCell>
                         <StyledTableCell>App</StyledTableCell>
-                        <StyledTableCell>Actions</StyledTableCell>
+                        {/* <StyledTableCell>Actions</StyledTableCell> */}
                       </TableRow>
                     </TableHead>
                     <TableBody>
@@ -253,7 +254,9 @@ export default function RecipeReviewCard(props) {
                           <StyledTableCell>
                             {profile.spec["app-name"]}
                           </StyledTableCell>
-                          <StyledTableCell>
+                          {/* <StyledTableCell>
+                            
+                            //edit profile api is not implemented yet
                             <IconButton
                               onClick={(e) => handleEdit(index)}
                               title="Edit"
@@ -266,7 +269,7 @@ export default function RecipeReviewCard(props) {
                             >
                               <DeleteIcon color="secondary" />
                             </IconButton>
-                          </StyledTableCell>
+                          </StyledTableCell> */}
                         </StyledTableRow>
                       ))}
                     </TableBody>
@@ -275,7 +278,7 @@ export default function RecipeReviewCard(props) {
               </>
             )}
             {!(props.appsData && props.appsData.length > 0) && (
-              <div>No apps found for adding profile</div>
+              <div>No app found for adding profile</div>
             )}
           </CardContent>
         </Collapse>
index 25ecaae..26629e0 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState, useEffect } from "react";
 import Card from "./CompositeProfileCard";
 import { Button, Grid } from "@material-ui/core";
@@ -82,7 +82,6 @@ const CompositeProfiles = (props) => {
         compositeAppVersion: props.compositeAppVersion,
         compositeProfileName: data[index].metadata.name,
       };
-      console.log(request);
       apiService
         .deleteCompositeProfile(request)
         .then(() => {
@@ -115,7 +114,7 @@ const CompositeProfiles = (props) => {
         }"`}
       />
 
-      <Button
+      {/* <Button
         disabled={isLoading}
         variant="outlined"
         color="primary"
@@ -123,7 +122,7 @@ const CompositeProfiles = (props) => {
         onClick={handleAddCompositeProfile}
       >
         Add Composite Profile
-      </Button>
+      </Button> */}
       <Form onClose={handleCloseForm} open={openForm} onSubmit={handleSubmit} />
       <Grid
         container
diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx
new file mode 100644 (file)
index 0000000..12dd7dd
--- /dev/null
@@ -0,0 +1,149 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import React from "react";
+import ExpandableCard from "../../common/ExpandableCard";
+import { Grid, Paper, TextField } from "@material-ui/core";
+import FileUpload from "../../common/FileUpload";
+
+function AppDetailsForm({ formikProps, ...props }) {
+  return (
+    <>
+      <Paper
+        style={{
+          width: "100%",
+          padding: "20px",
+          maxHeight: "395px",
+          overflowY: "auto",
+          scrollbarWidth: "thin",
+        }}
+      >
+        <Grid container spacing={3}>
+          <Grid item xs={6}>
+            <TextField
+              fullWidth
+              value={formikProps.values.apps[props.index].appName}
+              name={`apps[${props.index}].appName`}
+              id="app-name"
+              label="App name"
+              size="small"
+              onChange={formikProps.handleChange}
+              onBlur={formikProps.handleBlur}
+              required
+              helperText={
+                formikProps.errors.apps &&
+                formikProps.errors.apps[props.index] &&
+                formikProps.errors.apps[props.index].appName
+              }
+              error={
+                formikProps.errors.apps &&
+                formikProps.errors.apps[props.index] &&
+                formikProps.errors.apps[props.index].appName &&
+                true
+              }
+            />
+          </Grid>
+          <Grid item xs={6}>
+            <TextField
+              fullWidth
+              value={formikProps.values.apps[props.index].description}
+              name={`apps[${props.index}].description`}
+              id="app-description"
+              label="Description"
+              multiline
+              onChange={formikProps.handleChange}
+              onBlur={formikProps.handleBlur}
+              rowsMax={4}
+            />
+          </Grid>
+          <Grid item xs={6}>
+            <label
+              style={{ marginTop: "20px" }}
+              className="MuiFormLabel-root MuiInputLabel-root"
+              htmlFor="file"
+              id="file-label"
+            >
+              App tgz file
+              <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk">
+                â€‰*
+              </span>
+            </label>
+            <FileUpload
+              setFieldValue={formikProps.setFieldValue}
+              file={formikProps.values.apps[props.index].file}
+              onBlur={formikProps.handleBlur}
+              name={`apps[${props.index}].file`}
+              accept={".tgz"}
+            />
+            {formikProps.errors.apps &&
+              formikProps.errors.apps[props.index] &&
+              formikProps.errors.apps[props.index].file && (
+                <p style={{ color: "#f44336" }}>
+                  {formikProps.errors.apps[props.index].file}
+                </p>
+              )}
+          </Grid>
+          <Grid item xs={6}>
+            <label
+              style={{ marginTop: "20px" }}
+              className="MuiFormLabel-root MuiInputLabel-root"
+              htmlFor="file"
+              id="file-label"
+            >
+              Profile tar file
+              <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk">
+                â€‰*
+              </span>
+            </label>
+            <FileUpload
+              setFieldValue={formikProps.setFieldValue}
+              file={formikProps.values.apps[props.index].profilePackageFile}
+              onBlur={formikProps.handleBlur}
+              name={`apps[${props.index}].profilePackageFile`}
+              accept={".tar.gz, .tar"}
+            />
+            {formikProps.errors.apps &&
+              formikProps.errors.apps[props.index] &&
+              formikProps.errors.apps[props.index].profilePackageFile && (
+                <p style={{ color: "#f44336" }}>
+                  {formikProps.errors.apps[props.index].profilePackageFile}
+                </p>
+              )}
+          </Grid>
+        </Grid>
+      </Paper>
+    </>
+  );
+}
+
+const AppForm = (props) => {
+  return (
+    <ExpandableCard
+      error={
+        props.formikProps.errors.apps &&
+        props.formikProps.errors.apps[props.index]
+      }
+      title={props.name}
+      description={props.description}
+      content={
+        <AppDetailsForm
+          formikProps={props.formikProps}
+          name={props.name}
+          index={props.index}
+        />
+      }
+    />
+  );
+};
+export default AppForm;
diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx
new file mode 100644 (file)
index 0000000..e2272ae
--- /dev/null
@@ -0,0 +1,129 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import { Grid, Paper, TextField } from "@material-ui/core";
+import FileUpload from "../../common/FileUpload";
+import React from "react";
+
+function AppFormGeneral({ formikProps, ...props }) {
+  return (
+    <>
+      <Paper
+        style={{
+          width: "100%",
+          padding: "20px",
+          maxHeight: "395px",
+          overflowY: "auto",
+          scrollbarWidth: "thin",
+        }}
+      >
+        <Grid container spacing={3}>
+          <Grid item xs={6}>
+            <TextField
+              fullWidth
+              value={formikProps.values.apps[props.index].appName}
+              name={`apps[${props.index}].appName`}
+              id="app-name"
+              label="App name"
+              size="small"
+              onChange={formikProps.handleChange}
+              onBlur={formikProps.handleBlur}
+              required
+              helperText={
+                formikProps.errors.apps &&
+                formikProps.errors.apps[props.index] &&
+                formikProps.errors.apps[props.index].appName
+              }
+              error={
+                formikProps.errors.apps &&
+                formikProps.errors.apps[props.index] &&
+                formikProps.errors.apps[props.index].appName &&
+                true
+              }
+            />
+          </Grid>
+          <Grid item xs={6}>
+            <TextField
+              fullWidth
+              value={formikProps.values.apps[props.index].description}
+              name={`apps[${props.index}].description`}
+              id="app-description"
+              label="Description"
+              multiline
+              onChange={formikProps.handleChange}
+              onBlur={formikProps.handleBlur}
+              rowsMax={4}
+            />
+          </Grid>
+          <Grid item xs={6}>
+            <label
+              style={{ marginTop: "20px" }}
+              className="MuiFormLabel-root MuiInputLabel-root"
+              htmlFor="file"
+              id="file-label"
+            >
+              App tgz file
+              <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk">
+                â€‰*
+              </span>
+            </label>
+            <FileUpload
+              setFieldValue={formikProps.setFieldValue}
+              file={formikProps.values.apps[props.index].file}
+              onBlur={formikProps.handleBlur}
+              name={`apps[${props.index}].file`}
+              accept={".tgz"}
+            />
+            {formikProps.errors.apps &&
+              formikProps.errors.apps[props.index] &&
+              formikProps.errors.apps[props.index].file && (
+                <p style={{ color: "#f44336" }}>
+                  {formikProps.errors.apps[props.index].file}
+                </p>
+              )}
+          </Grid>
+          <Grid item xs={6}>
+            <label
+              style={{ marginTop: "20px" }}
+              className="MuiFormLabel-root MuiInputLabel-root"
+              htmlFor="file"
+              id="file-label"
+            >
+              Profile tar file
+              <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk">
+                â€‰*
+              </span>
+            </label>
+            <FileUpload
+              setFieldValue={formikProps.setFieldValue}
+              file={formikProps.values.apps[props.index].profilePackageFile}
+              onBlur={formikProps.handleBlur}
+              name={`apps[${props.index}].profilePackageFile`}
+              accept={".tar.gz, .tar"}
+            />
+            {formikProps.errors.apps &&
+              formikProps.errors.apps[props.index] &&
+              formikProps.errors.apps[props.index].profilePackageFile && (
+                <p style={{ color: "#f44336" }}>
+                  {formikProps.errors.apps[props.index].profilePackageFile}
+                </p>
+              )}
+          </Grid>
+        </Grid>
+      </Paper>
+    </>
+  );
+}
+
+export default AppFormGeneral;
diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx
new file mode 100644 (file)
index 0000000..c52c2b4
--- /dev/null
@@ -0,0 +1,83 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import React from "react";
+import PropTypes from "prop-types";
+import { Grid, Paper, Typography } from "@material-ui/core";
+import EnhancedTable from "./SortableTable";
+
+function AppFormPlacement({
+  formikProps,
+  index,
+  clusterProviders,
+  handleRowSelect,
+  ...props
+}) {
+  return (
+    <>
+      <Typography variant="subtitle1" style={{ float: "left" }}>
+        Select Clusters
+        <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> *</span>
+      </Typography>
+      {formikProps.errors.apps &&
+        formikProps.errors.apps[index] &&
+        formikProps.errors.apps[index].clusters && (
+          <span
+            style={{
+              color: "#f44336",
+              marginRight: "35px",
+              float: "right",
+            }}
+          >
+            {typeof formikProps.errors.apps[index].clusters === "string" &&
+              formikProps.errors.apps[index].clusters}
+          </span>
+        )}
+      <Grid
+        container
+        spacing={3}
+        style={{
+          height: "400px",
+          overflowY: "auto",
+          width: "100%",
+          scrollbarWidth: "thin",
+        }}
+      >
+        {clusterProviders &&
+          clusterProviders.length > 0 &&
+          clusterProviders.map((clusterProvider) => (
+            <Grid key={clusterProvider.name} item xs={12}>
+              <Paper>
+                <EnhancedTable
+                  key={clusterProvider.name}
+                  tableName={clusterProvider.name}
+                  clusters={clusterProvider.clusters}
+                  formikValues={formikProps.values.apps[index].clusters}
+                  onRowSelect={handleRowSelect}
+                />
+              </Paper>
+            </Grid>
+          ))}
+      </Grid>
+    </>
+  );
+}
+
+AppFormPlacement.propTypes = {
+  formikProps: PropTypes.object,
+  index: PropTypes.number,
+  handleRowSelect: PropTypes.func,
+};
+
+export default AppFormPlacement;
diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx
new file mode 100644 (file)
index 0000000..055dc3e
--- /dev/null
@@ -0,0 +1,524 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import React, { useState } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import Button from "@material-ui/core/Button";
+import Typography from "@material-ui/core/Typography";
+import { Grid, IconButton } from "@material-ui/core";
+import { TextField, Select, MenuItem, InputLabel } from "@material-ui/core";
+import AddIcon from "@material-ui/icons/Add";
+import CardContent from "@material-ui/core/CardContent";
+import Card from "@material-ui/core/Card";
+import apiService from "../../services/apiService";
+import DeleteIcon from "@material-ui/icons/Delete";
+import { Formik } from "formik";
+import Notification from "../../common/Notification";
+
+function NetworkForm({ formikProps, ...props }) {
+  const [clusters, setClusters] = useState(props.clusters);
+  const [notificationDetails, setNotificationDetails] = useState({});
+  const useStyles = makeStyles({
+    root: {
+      minWidth: 275,
+    },
+    title: {
+      fontSize: 14,
+    },
+    pos: {
+      marginBottom: 12,
+    },
+  });
+
+  const handleAddNetworkInterface = (providerIndex, clusterIndex, values) => {
+    let updatedFields = [];
+    if (
+      values.apps[props.index].clusters[providerIndex].selectedClusters[
+        clusterIndex
+      ].interfaces
+    ) {
+      updatedFields = [
+        ...values.apps[props.index].clusters[providerIndex].selectedClusters[
+          clusterIndex
+        ].interfaces,
+        {
+          networkName: "",
+          ip: "",
+          subnet: "",
+        },
+      ];
+    } else {
+      updatedFields = [
+        {
+          networkName: "",
+          ip: "",
+          subnet: "",
+        },
+      ];
+    }
+
+    let request = {
+      providerName: values.apps[props.index].clusters[providerIndex].provider,
+      clusterName:
+        values.apps[props.index].clusters[providerIndex].selectedClusters[
+          clusterIndex
+        ].name,
+    };
+    apiService
+      .getClusterProviderNetworks(request)
+      .then((networks) => {
+        let networkData = [];
+        if (networks && networks.length > 0) {
+          networks.forEach((network) => {
+            networkData.push({
+              name: network.metadata.name,
+              subnets: network.spec.ipv4Subnets,
+            });
+          });
+        }
+
+        apiService
+          .getClusterNetworks(request)
+          .then((clusterNetworks) => {
+            if (clusterNetworks && clusterNetworks.length > 0) {
+              clusterNetworks.forEach((clusterNetwork) => {
+                networkData.push({
+                  name: clusterNetwork.metadata.name,
+                  subnets: clusterNetwork.spec.ipv4Subnets,
+                });
+              });
+            }
+            //add interface entry onyl of there is atlease one available network
+            if (networkData.length > 0) {
+              setClusters((clusters) => {
+                clusters[providerIndex].selectedClusters[
+                  clusterIndex
+                ].interfaces = updatedFields;
+                clusters[providerIndex].selectedClusters[
+                  clusterIndex
+                ].networks = networkData;
+                clusters[providerIndex].selectedClusters[
+                  clusterIndex
+                ].availableNetworks = getAvailableNetworks(
+                  clusters[providerIndex].selectedClusters[clusterIndex]
+                );
+                return clusters;
+              });
+              formikProps.setFieldValue(
+                `apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces`,
+                updatedFields
+              );
+            } else {
+              setNotificationDetails({
+                show: true,
+                message: `No network available for this cluster`,
+                severity: "warning",
+              });
+            }
+          })
+          .catch((err) => {
+            console.log("error getting cluster networks : ", err);
+          });
+      })
+      .catch((err) => {
+        console.log("error getting cluster provider networks : ", err);
+      })
+      .finally(() => {
+        return updatedFields;
+      });
+  };
+
+  const handleSelectNetowrk = (
+    e,
+    providerIndex,
+    clusterIndex,
+    interfaceIndex
+  ) => {
+    setClusters((clusters) => {
+      clusters[providerIndex].selectedClusters[clusterIndex].interfaces[
+        interfaceIndex
+      ] = {
+        networkName: e.target.value,
+        ip: "",
+        subnet: "",
+      };
+      clusters[providerIndex].selectedClusters[
+        clusterIndex
+      ].availableNetworks = getAvailableNetworks(
+        clusters[providerIndex].selectedClusters[clusterIndex],
+        "handleAddNetworkInterface"
+      );
+      return clusters;
+    });
+    formikProps.handleChange(e);
+  };
+  const handleRemoveNetwork = (providerIndex, clusterIndex, interfaceIndex) => {
+    setClusters((clusters) => {
+      clusters[providerIndex].selectedClusters[clusterIndex].interfaces.splice(
+        interfaceIndex,
+        1
+      );
+      clusters[providerIndex].selectedClusters[
+        clusterIndex
+      ].availableNetworks = getAvailableNetworks(
+        clusters[providerIndex].selectedClusters[clusterIndex]
+      );
+      return clusters;
+    });
+    formikProps.setFieldValue(
+      `apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces`,
+      clusters[providerIndex].selectedClusters[clusterIndex].interfaces
+    );
+  };
+  const getAvailableNetworks = (cluster) => {
+    let availableNetworks = [];
+    cluster.networks.forEach((network) => {
+      let match = false;
+      cluster.interfaces.forEach((networkInterface) => {
+        if (network.name === networkInterface.networkName) {
+          match = true;
+          return;
+        }
+      });
+      if (!match) availableNetworks.push(network);
+    });
+    return availableNetworks;
+  };
+
+  const classes = useStyles();
+  return (
+    <>
+      <Notification notificationDetails={notificationDetails} />
+      <Grid
+        key="networkForm"
+        container
+        spacing={3}
+        style={{
+          height: "400px",
+          overflowY: "auto",
+          width: "100%",
+          scrollbarWidth: "thin",
+        }}
+      >
+        {(!clusters || clusters.length < 1) && (
+          <Grid item xs={12}>
+            <Typography variant="h6">No clusters selected</Typography>
+          </Grid>
+        )}
+        {clusters &&
+          clusters.map((cluster, providerIndex) => (
+            <Grid key={cluster.provider + providerIndex} item xs={12}>
+              <Card className={classes.root}>
+                <CardContent>
+                  <Grid container spacing={2}>
+                    <Grid item xs={12}>
+                      <Typography
+                        className={classes.title}
+                        color="textSecondary"
+                        gutterBottom
+                      >
+                        {cluster.provider}
+                      </Typography>
+                    </Grid>
+                    {cluster.selectedClusters.map(
+                      (selectedCluster, clusterIndex) => (
+                        <React.Fragment key={selectedCluster.name}>
+                          <Grid item xs={12}>
+                            <Typography>{selectedCluster.name}</Typography>
+                          </Grid>
+                          <Formik>
+                            {() => {
+                              const {
+                                values,
+                                errors,
+                                handleChange,
+                                handleBlur,
+                              } = formikProps;
+                              return (
+                                <>
+                                  {selectedCluster.interfaces &&
+                                  selectedCluster.interfaces.length > 0
+                                    ? selectedCluster.interfaces.map(
+                                        (networkInterface, interfaceIndex) => (
+                                          <Grid
+                                            spacing={1}
+                                            container
+                                            item
+                                            key={interfaceIndex}
+                                            xs={12}
+                                          >
+                                            <Grid item xs={4}>
+                                              <InputLabel id="network-select-label">
+                                                Network
+                                              </InputLabel>
+                                              <Select
+                                                fullWidth
+                                                labelId="network-select-label"
+                                                id="network-select"
+                                                name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].networkName`}
+                                                value={
+                                                  values.apps[props.index]
+                                                    .clusters[providerIndex]
+                                                    .selectedClusters[
+                                                    clusterIndex
+                                                  ].interfaces[interfaceIndex]
+                                                    .networkName
+                                                }
+                                                onChange={(e) => {
+                                                  handleSelectNetowrk(
+                                                    e,
+                                                    providerIndex,
+                                                    clusterIndex,
+                                                    interfaceIndex
+                                                  );
+                                                }}
+                                              >
+                                                {values.apps[props.index]
+                                                  .clusters[providerIndex]
+                                                  .selectedClusters[
+                                                  clusterIndex
+                                                ].interfaces[interfaceIndex]
+                                                  .networkName && (
+                                                  <MenuItem
+                                                    key={
+                                                      values.apps[props.index]
+                                                        .clusters[providerIndex]
+                                                        .selectedClusters[
+                                                        clusterIndex
+                                                      ].interfaces[
+                                                        interfaceIndex
+                                                      ].networkName
+                                                    }
+                                                    value={
+                                                      values.apps[props.index]
+                                                        .clusters[providerIndex]
+                                                        .selectedClusters[
+                                                        clusterIndex
+                                                      ].interfaces[
+                                                        interfaceIndex
+                                                      ].networkName
+                                                    }
+                                                  >
+                                                    {
+                                                      values.apps[props.index]
+                                                        .clusters[providerIndex]
+                                                        .selectedClusters[
+                                                        clusterIndex
+                                                      ].interfaces[
+                                                        interfaceIndex
+                                                      ].networkName
+                                                    }
+                                                  </MenuItem>
+                                                )}
+                                                {selectedCluster.availableNetworks &&
+                                                  selectedCluster.availableNetworks.map(
+                                                    (network) => (
+                                                      <MenuItem
+                                                        key={network.name}
+                                                        value={network.name}
+                                                      >
+                                                        {network.name}
+                                                      </MenuItem>
+                                                    )
+                                                  )}
+                                              </Select>
+                                            </Grid>
+
+                                            <Grid item xs={4}>
+                                              <InputLabel id="subnet-select-label">
+                                                Subnet
+                                              </InputLabel>
+                                              <Select
+                                                fullWidth
+                                                labelId="subnet-select-label"
+                                                id="subnet-select-label"
+                                                name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].subnet`}
+                                                value={
+                                                  values.apps[props.index]
+                                                    .clusters[providerIndex]
+                                                    .selectedClusters[
+                                                    clusterIndex
+                                                  ].interfaces[interfaceIndex]
+                                                    .subnet
+                                                }
+                                                onChange={handleChange}
+                                              >
+                                                {values.apps[props.index]
+                                                  .clusters[providerIndex]
+                                                  .selectedClusters[
+                                                  clusterIndex
+                                                ].interfaces[interfaceIndex]
+                                                  .networkName === ""
+                                                  ? null
+                                                  : selectedCluster.networks
+                                                      .filter(
+                                                        (network) =>
+                                                          network.name ===
+                                                          values.apps[
+                                                            props.index
+                                                          ].clusters[
+                                                            providerIndex
+                                                          ].selectedClusters[
+                                                            clusterIndex
+                                                          ].interfaces[
+                                                            interfaceIndex
+                                                          ].networkName
+                                                      )[0]
+                                                      .subnets.map((subnet) => (
+                                                        <MenuItem
+                                                          key={subnet.name}
+                                                          value={subnet.name}
+                                                        >
+                                                          {subnet.name}(
+                                                          {subnet.subnet})
+                                                        </MenuItem>
+                                                      ))}
+                                              </Select>
+                                            </Grid>
+                                            <Grid item xs={3}>
+                                              <TextField
+                                                width={"65%"}
+                                                name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].ip`}
+                                                onBlur={handleBlur}
+                                                id="ip"
+                                                label="IP Address"
+                                                value={
+                                                  values.apps[props.index]
+                                                    .clusters[providerIndex]
+                                                    .selectedClusters[
+                                                    clusterIndex
+                                                  ].interfaces[interfaceIndex]
+                                                    .ip
+                                                }
+                                                onChange={handleChange}
+                                                helperText={
+                                                  (errors.apps &&
+                                                    errors.apps[props.index] &&
+                                                    errors.apps[props.index]
+                                                      .clusters &&
+                                                    errors.apps[props.index]
+                                                      .clusters[clusterIndex] &&
+                                                    errors.apps[props.index]
+                                                      .clusters[clusterIndex]
+                                                      .selectedClusters[
+                                                      clusterIndex
+                                                    ] &&
+                                                    errors.apps[props.index]
+                                                      .clusters[clusterIndex]
+                                                      .selectedClusters[
+                                                      clusterIndex
+                                                    ].interfaces[
+                                                      interfaceIndex
+                                                    ] &&
+                                                    errors.apps[props.index]
+                                                      .clusters[clusterIndex]
+                                                      .selectedClusters[
+                                                      clusterIndex
+                                                    ].interfaces[interfaceIndex]
+                                                      .ip) ||
+                                                  "blank for auto assign"
+                                                }
+                                                error={
+                                                  errors.apps &&
+                                                  errors.apps[props.index] &&
+                                                  errors.apps[props.index]
+                                                    .clusters &&
+                                                  errors.apps[props.index]
+                                                    .clusters[clusterIndex] &&
+                                                  errors.apps[props.index]
+                                                    .clusters[clusterIndex]
+                                                    .selectedClusters[
+                                                    clusterIndex
+                                                  ] &&
+                                                  errors.apps[props.index]
+                                                    .clusters[clusterIndex]
+                                                    .selectedClusters[
+                                                    clusterIndex
+                                                  ].interfaces[
+                                                    interfaceIndex
+                                                  ] &&
+                                                  errors.apps[props.index]
+                                                    .clusters[clusterIndex]
+                                                    .selectedClusters[
+                                                    clusterIndex
+                                                  ].interfaces[interfaceIndex]
+                                                    .ip &&
+                                                  true
+                                                }
+                                              />
+                                            </Grid>
+                                            <Grid item xs={1}>
+                                              <IconButton
+                                                color="secondary"
+                                                onClick={() => {
+                                                  handleRemoveNetwork(
+                                                    providerIndex,
+                                                    clusterIndex,
+                                                    interfaceIndex
+                                                  );
+                                                }}
+                                              >
+                                                <DeleteIcon fontSize="small" />
+                                              </IconButton>
+                                            </Grid>
+                                          </Grid>
+                                        )
+                                      )
+                                    : null}
+                                  <Grid
+                                    key={selectedCluster.name + "addButton"}
+                                    item
+                                    xs={12}
+                                  >
+                                    <Button
+                                      variant="outlined"
+                                      size="small"
+                                      fullWidth
+                                      color="primary"
+                                      disabled={
+                                        selectedCluster.interfaces &&
+                                        selectedCluster.interfaces.length > 0 &&
+                                        selectedCluster.networks.length ===
+                                          selectedCluster.interfaces.length
+                                      }
+                                      onClick={() => {
+                                        handleAddNetworkInterface(
+                                          providerIndex,
+                                          clusterIndex,
+                                          values
+                                        );
+                                      }}
+                                      startIcon={<AddIcon />}
+                                    >
+                                      Add Network Interface
+                                    </Button>
+                                  </Grid>
+                                </>
+                              );
+                            }}
+                          </Formik>
+                        </React.Fragment>
+                      )
+                    )}
+                  </Grid>
+                </CardContent>
+              </Card>
+            </Grid>
+          ))}
+      </Grid>
+    </>
+  );
+}
+
+export default NetworkForm;
index 29e17cd..751ea8e 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
-import React from "react";
-import { withStyles } from "@material-ui/core/styles";
+// ========================================================================
+import React, { useState } from "react";
 import Button from "@material-ui/core/Button";
 import Dialog from "@material-ui/core/Dialog";
-import MuiDialogTitle from "@material-ui/core/DialogTitle";
-import MuiDialogContent from "@material-ui/core/DialogContent";
-import MuiDialogActions from "@material-ui/core/DialogActions";
+import AppBar from "@material-ui/core/AppBar";
+import Toolbar from "@material-ui/core/Toolbar";
+import IconButton from "@material-ui/core/IconButton";
 import Typography from "@material-ui/core/Typography";
-import { TextField } from '@material-ui/core';
-const styles = (theme) => ({
-  root: {
-    margin: 0,
-    padding: theme.spacing(2),
+import CloseIcon from "@material-ui/icons/Close";
+import Slide from "@material-ui/core/Slide";
+import { Grid } from "@material-ui/core";
+import { TextField } from "@material-ui/core";
+import { makeStyles } from "@material-ui/core/styles";
+import AddIcon from "@material-ui/icons/Add";
+import NewAppForm from "../../common/Form";
+import AppForm from "./AppForm";
+import { Formik, FieldArray } from "formik";
+import * as Yup from "yup";
+
+const Transition = React.forwardRef(function Transition(props, ref) {
+  return <Slide direction="up" ref={ref} {...props} />;
+});
+
+const useStyles = makeStyles((theme) => ({
+  tableRoot: {
+    width: "100%",
   },
-  closeButton: {
+  paper: {
+    width: "100%",
+    marginBottom: theme.spacing(2),
+  },
+  table: {
+    minWidth: 550,
+  },
+  visuallyHidden: {
+    border: 0,
+    clip: "rect(0 0 0 0)",
+    height: 1,
+    margin: -1,
+    overflow: "hidden",
+    padding: 0,
     position: "absolute",
-    right: theme.spacing(1),
-    top: theme.spacing(1),
-    color: theme.palette.grey[500],
+    top: 20,
+    width: 1,
   },
-});
-
-
-const DialogContent = withStyles((theme) => ({
-  root: {
-    padding: theme.spacing(2),
+  appBar: {
+    position: "relative",
+  },
+  title: {
+    marginLeft: theme.spacing(2),
+    flex: 1,
+  },
+  demo: {
+    backgroundColor: theme.palette.background.paper,
   },
-}))(MuiDialogContent);
-
-const DialogActions = withStyles((theme) => ({
   root: {
-    margin: 0,
-    padding: theme.spacing(1),
+    flexGrow: 1,
+    backgroundColor: theme.palette.background.paper,
+    display: "flex",
+    height: 424,
   },
-}))(MuiDialogActions);
-
+  tabs: {
+    borderRight: `1px solid ${theme.palette.divider}`,
+  },
+}));
 
-class CreateCompositeAppForm extends React.Component {
-  constructor(props) {
-    super(props)
-    this.state = {
-      fields: { name: "", version: "", description: "" },
-      errors: {}
-    }
-    this.handleChange = this.handleChange.bind(this);
-    this.submituserRegistrationForm = this.submituserRegistrationForm.bind(this);
-  }
+const PROFILE_SUPPORTED_FORMATS = [
+  ".tgz",
+  ".tar.gz",
+  ".tar",
+  "application/x-tar",
+  "application/x-tgz",
+  "application/x-compressed",
+  "application/x-gzip",
+  "application/x-compressed-tar",
+  "application/gzip",
+];
 
+const APP_PACKAGE_SUPPORTED_FORMATS = [
+  ".tgz",
+  ".tar.gz",
+  ".tar",
+  "application/x-tar",
+  "application/x-tgz",
+  "application/x-compressed",
+  "application/x-gzip",
+  "application/x-compressed-tar",
+];
+const serviceBasicValidationSchema = Yup.object({
+  name: Yup.string().required(),
+  description: Yup.string(),
+  apps: Yup.array()
+    .of(
+      Yup.object({
+        appName: Yup.string().required("App name is required"),
+        file: Yup.mixed()
+          .required("An app package file is required")
+          .test(
+            "fileFormat",
+            "Unsupported file format",
+            (value) =>
+              value && APP_PACKAGE_SUPPORTED_FORMATS.includes(value.type)
+          ),
+        profilePackageFile: Yup.mixed()
+          .required("A profile package file is required")
+          .test(
+            "fileFormat",
+            "Unsupported file format",
+            (value) => value && PROFILE_SUPPORTED_FORMATS.includes(value.type)
+          ),
+      })
+    )
+    .required("At least one app is required"),
+});
 
-  componentDidMount = () => {
-    if (this.props.item) {
-      this.title = "Edit Composite App";
-      this.buttonLabel = "Update";
-      this.isEdit = true;
-    }
-    else {
-      this.title = "New Composite App";
-      this.buttonLabel = "Create";
-      this.isEdit = false;
-    }
+const CreateCompositeAppForm = ({ open, handleClose }) => {
+  const classes = useStyles();
+  const [openForm, setOpenForm] = useState(false);
+  const handleCloseForm = () => {
+    setOpenForm(false);
   };
-
-  componentDidUpdate = (prevProps, prevState) => {
-    if (this.props.item && ((prevProps.item !== this.props.item))) {
-      this.setState({ fields: { ...this.props.item.metadata, version: this.props.item.spec.version } });
-    }
-  }
-
-  resetFields = () => {
-    if (!this.isEdit) {
-      this.setState({
-        fields: { name: "", version: "", description: "" },
-        errors: {}
-      });
-    }
-    else {
-      this.setState({ fields: { ...this.props.item.metadata, version: this.props.item.spec.version } });
-    }
-  }
-
-  handleClose = () => {
-    this.resetFields();
-    this.props.handleClose();
+  const handleAddApp = () => {
+    setOpenForm(true);
   };
-
-  submituserRegistrationForm(e) {
-    e.preventDefault();
-    if (this.validateForm()) {
-      this.resetFields();
-      this.props.handleClose(this.state.fields);
-    }
-  }
-
-  validateForm() {
-    let fields = this.state.fields;
-    let errors = {};
-    let formIsValid = true;
-
-    if (!fields["name"]) {
-      formIsValid = false;
-      errors["name"] = "*Please enter your username.";
-    }
-
-    if (typeof fields["name"] !== "string") {
-      if (!fields["name"].match(/^[a-zA-Z ]*$/)) {
-        formIsValid = false;
-        errors["name"] = "*Please enter alphabet characters only.";
-      }
-    }
-    this.setState({
-      errors: errors
-    });
-    return formIsValid;
-  }
-
-  handleChange = (e) => {
-    this.setState({ fields: { ...this.state.fields, [e.target.name]: e.target.value } });
-  }
-
-  render = () => {
-    const { classes } = this.props;
-    return (
-      <>
+  let initialValues = { name: "", description: "", apps: [] };
+  return (
+    <>
+      {open && (
         <Dialog
-          maxWidth={"xs"}
-          onClose={this.handleClose}
-          aria-labelledby="customized-dialog-title"
-          open={this.props.open}
-          disableBackdropClick
+          open={open}
+          onClose={() => {
+            handleClose();
+          }}
+          fullScreen
+          TransitionComponent={Transition}
         >
-          <MuiDialogTitle disableTypography className={classes.root} >
-            <Typography variant="h6">{this.title}</Typography>
-          </MuiDialogTitle>
+          <Formik
+            initialValues={initialValues}
+            onSubmit={(values, { setSubmitting }) => {
+              setSubmitting(false);
+              handleClose(values);
+            }}
+            validationSchema={serviceBasicValidationSchema}
+          >
+            {(props) => {
+              const {
+                values,
+                touched,
+                errors,
+                isSubmitting,
+                handleChange,
+                handleBlur,
+                handleSubmit,
+              } = props;
+              return (
+                <>
+                  <form noValidate onSubmit={handleSubmit}>
+                    <AppBar className={classes.appBar}>
+                      <Toolbar>
+                        <IconButton
+                          edge="start"
+                          color="inherit"
+                          onClick={() => {
+                            handleClose();
+                          }}
+                          aria-label="close"
+                        >
+                          <CloseIcon />
+                        </IconButton>
+                        <Typography variant="h6" className={classes.title}>
+                          Add Service
+                        </Typography>
+                        <Button
+                          type="submit"
+                          autoFocus
+                          variant="contained"
+                          disabled={isSubmitting}
+                        >
+                          SUBMIT
+                        </Button>
+                      </Toolbar>
+                    </AppBar>
+                    <div style={{ padding: "12px" }}>
+                      <Grid
+                        container
+                        direction="row"
+                        justify="center"
+                        alignItems="center"
+                        style={{ marginTop: "40px" }}
+                        spacing={3}
+                      >
+                        <Grid item xs={6}>
+                          <Grid container spacing={3}>
+                            {errors.apps &&
+                              touched.apps &&
+                              typeof errors.apps !== "object" && (
+                                <Grid item xs={12} sm={12}>
+                                  <Typography>{errors.apps}</Typography>
+                                </Grid>
+                              )}
+
+                            <Grid item xs={12} sm={6}>
+                              <TextField
+                                fullWidth
+                                name="name"
+                                id="input-name"
+                                label="Name"
+                                variant="outlined"
+                                size="small"
+                                value={values.name}
+                                onChange={handleChange}
+                                onBlur={handleBlur}
+                                required
+                                helperText={
+                                  errors.name &&
+                                  touched.name &&
+                                  "Name is required"
+                                }
+                                error={errors.name && touched.name}
+                              />
+                            </Grid>
+                            <Grid item xs={12} sm={6}>
+                              <TextField
+                                fullWidth
+                                name="description"
+                                id="input-description"
+                                label="Description"
+                                variant="outlined"
+                                size="small"
+                                value={values.description}
+                                onChange={handleChange}
+                                onBlur={handleBlur}
+                              />
+                            </Grid>
 
-          <form onSubmit={this.submituserRegistrationForm}>
-            <DialogContent dividers>
-              <TextField
-                style={{ width: "40%", marginBottom: "10px" }}
-                name="name"
-                value={this.state.fields.name}
-                id="input-name"
-                label="Name"
-                helperText="Name should be unique"
-                onChange={this.handleChange}
-                required
-              />
-              <TextField
-                style={{ width: "40%", marginBottom: "20px", float: "right" }}
-                name="version"
-                value={this.state.fields.version}
-                onChange={this.handleChange}
-                id="input-version"
-                label="Version"
-                required
-              />
-              <TextField
-                style={{ width: "100%", marginBottom: "25px" }}
-                name="description"
-                value={this.state.fields.description}
-                onChange={this.handleChange}
-                id="input-description"
-                label="Description"
-                multiline
-                rowsMax={4}
-              />
-            </DialogContent>
-            <DialogActions>
-              <Button autoFocus onClick={this.handleClose} color="secondary">
-                Cancel
-          </Button>
-              <Button autoFocus type="submit" color="primary">
-                {this.buttonLabel}
-              </Button>
-            </DialogActions>
-          </form>
+                            <FieldArray
+                              name="apps"
+                              render={(arrayHelpers) => (
+                                <>
+                                  <NewAppForm
+                                    open={openForm}
+                                    onClose={handleCloseForm}
+                                    onSubmit={(values) => {
+                                      arrayHelpers.push({
+                                        appName: values.name,
+                                        description: values.description,
+                                      });
+                                      setOpenForm(false);
+                                    }}
+                                  />
+                                  {values.apps &&
+                                    values.apps.length > 0 &&
+                                    values.apps.map((app, index) => (
+                                      <Grid key={index} item sm={12} xs={12}>
+                                        <AppForm
+                                          formikProps={props}
+                                          name={app.appName}
+                                          description={app.description}
+                                          index={index}
+                                          initialValues={values}
+                                        />
+                                      </Grid>
+                                    ))}
+                                </>
+                              )}
+                            />
+                            <Grid item xs={12}>
+                              <Button
+                                variant="outlined"
+                                size="small"
+                                fullWidth
+                                color="primary"
+                                onClick={() => {
+                                  handleAddApp();
+                                }}
+                                startIcon={<AddIcon />}
+                              >
+                                Add App
+                              </Button>
+                            </Grid>
+                          </Grid>
+                        </Grid>
+                      </Grid>
+                    </div>
+                  </form>
+                </>
+              );
+            }}
+          </Formik>
         </Dialog>
-      </>
-    );
-  }
-}
-export default withStyles(styles)(CreateCompositeAppForm)
+      )}
+    </>
+  );
+};
+export default CreateCompositeAppForm;
diff --git a/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx b/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx
new file mode 100644 (file)
index 0000000..f1a6ac2
--- /dev/null
@@ -0,0 +1,410 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableContainer from "@material-ui/core/TableContainer";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import TableSortLabel from "@material-ui/core/TableSortLabel";
+import { makeStyles } from "@material-ui/core/styles";
+import React, { useEffect, useState } from "react";
+import Checkbox from "@material-ui/core/Checkbox";
+import PropTypes from "prop-types";
+import Typography from "@material-ui/core/Typography";
+import Toolbar from "@material-ui/core/Toolbar";
+
+import clsx from "clsx";
+import TablePagination from "@material-ui/core/TablePagination";
+import { lighten } from "@material-ui/core/styles";
+
+function descendingComparator(a, b, orderBy) {
+  if (b[orderBy] < a[orderBy]) {
+    return -1;
+  }
+  if (b[orderBy] > a[orderBy]) {
+    return 1;
+  }
+  return 0;
+}
+
+function getComparator(order, orderBy) {
+  return order === "desc"
+    ? (a, b) => descendingComparator(a, b, orderBy)
+    : (a, b) => -descendingComparator(a, b, orderBy);
+}
+
+function stableSort(array, comparator) {
+  const stabilizedThis = array.map((el, index) => [el, index]);
+  stabilizedThis.sort((a, b) => {
+    const order = comparator(a[0], b[0]);
+    if (order !== 0) return order;
+    return a[1] - b[1];
+  });
+  return stabilizedThis.map((el) => el[0]);
+}
+
+const headCells = [
+  {
+    id: "name",
+    numeric: false,
+    sortable: true,
+    disablePadding: true,
+    label: "Cluster",
+  },
+  {
+    id: "description",
+    numeric: true,
+    sortable: false,
+    disablePadding: false,
+    label: "Description",
+  },
+];
+
+function EnhancedTableHead(props) {
+  const {
+    classes,
+    onSelectAllClick,
+    order,
+    orderBy,
+    numSelected,
+    rowCount,
+    onRequestSort,
+  } = props;
+  const createSortHandler = (property) => (event) => {
+    onRequestSort(event, property);
+  };
+
+  return (
+    <TableHead>
+      <TableRow>
+        <TableCell padding="checkbox">
+          <Checkbox
+            indeterminate={numSelected > 0 && numSelected < rowCount}
+            checked={rowCount > 0 && numSelected === rowCount}
+            onChange={onSelectAllClick}
+          />
+        </TableCell>
+        {headCells.map((headCell) =>
+          headCell.sortable ? (
+            <TableCell
+              style={{ fontWeight: "520" }}
+              key={headCell.id}
+              align={headCell.numeric ? "right" : "left"}
+              padding={headCell.disablePadding ? "none" : "default"}
+              sortDirection={orderBy === headCell.id ? order : false}
+            >
+              <TableSortLabel
+                active={orderBy === headCell.id}
+                direction={orderBy === headCell.id ? order : "asc"}
+                onClick={createSortHandler(headCell.id)}
+              >
+                {headCell.label}
+                {orderBy === headCell.id ? (
+                  <span className={classes.visuallyHidden}>
+                    {order === "desc"
+                      ? "sorted descending"
+                      : "sorted ascending"}
+                  </span>
+                ) : null}
+              </TableSortLabel>
+            </TableCell>
+          ) : (
+            <TableCell
+              key={headCell.id}
+              style={{ fontWeight: "520" }}
+              padding={headCell.disablePadding ? "none" : "default"}
+              align={headCell.numeric ? "right" : "left"}
+            >
+              {headCell.label}
+            </TableCell>
+          )
+        )}
+      </TableRow>
+    </TableHead>
+  );
+}
+
+EnhancedTableHead.propTypes = {
+  classes: PropTypes.object.isRequired,
+  numSelected: PropTypes.number.isRequired,
+  onRequestSort: PropTypes.func.isRequired,
+  onSelectAllClick: PropTypes.func.isRequired,
+  order: PropTypes.oneOf(["asc", "desc"]).isRequired,
+  orderBy: PropTypes.string.isRequired,
+  rowCount: PropTypes.number.isRequired,
+};
+
+const useToolbarStyles = makeStyles((theme) => ({
+  root: {
+    paddingLeft: theme.spacing(2),
+    paddingRight: theme.spacing(1),
+  },
+  highlight:
+    theme.palette.type === "light"
+      ? {
+          color: theme.palette.primary.main,
+          backgroundColor: lighten(theme.palette.primary.light, 0.85),
+        }
+      : {
+          color: theme.palette.text.primary,
+          backgroundColor: theme.palette.primary.dark,
+        },
+  title: {
+    flex: "1 1 100%",
+  },
+}));
+
+const EnhancedTableToolbar = (props) => {
+  const classes = useToolbarStyles();
+  const { numSelected } = props;
+
+  return (
+    <Toolbar
+      className={clsx(classes.root, {
+        [classes.highlight]: numSelected > 0,
+      })}
+    >
+      <Typography
+        className={classes.title}
+        variant="h6"
+        id="tableTitle"
+        component="div"
+      >
+        {props.tableName}
+      </Typography>
+
+      <Typography
+        className={classes.title}
+        style={{ textAlign: "right" }}
+        color="inherit"
+        variant="subtitle1"
+        component="div"
+      >
+        {numSelected} selected
+      </Typography>
+    </Toolbar>
+  );
+};
+
+EnhancedTableToolbar.propTypes = {
+  numSelected: PropTypes.number.isRequired,
+};
+
+const useStyles = makeStyles((theme) => ({
+  tableRoot: {
+    width: "100%",
+  },
+  paper: {
+    width: "100%",
+    marginBottom: theme.spacing(2),
+  },
+  table: {
+    minWidth: 550,
+  },
+  visuallyHidden: {
+    border: 0,
+    clip: "rect(0 0 0 0)",
+    height: 1,
+    margin: -1,
+    overflow: "hidden",
+    padding: 0,
+    position: "absolute",
+    top: 20,
+    width: 1,
+  },
+  appBar: {
+    position: "relative",
+  },
+  title: {
+    marginLeft: theme.spacing(2),
+    flex: 1,
+  },
+  demo: {
+    backgroundColor: theme.palette.background.paper,
+  },
+  root: {
+    flexGrow: 1,
+    backgroundColor: theme.palette.background.paper,
+    display: "flex",
+    height: 424,
+  },
+  tabs: {
+    borderRight: `1px solid ${theme.palette.divider}`,
+  },
+}));
+
+function EnhancedTable({
+  clusters,
+  formikValues,
+  tableName,
+  onRowSelect,
+  ...props
+}) {
+  const classes = useStyles();
+  const [order, setOrder] = useState("asc");
+  const [orderBy, setOrderBy] = useState("name");
+  const [selected, setSelected] = useState([]);
+  const [page, setPage] = useState(0);
+  const [rowsPerPage, setRowsPerPage] = useState(5);
+  const [rows, setRows] = useState([]);
+
+  const handleRequestSort = (event, property) => {
+    const isAsc = orderBy === property && order === "asc";
+    setOrder(isAsc ? "desc" : "asc");
+    setOrderBy(property);
+  };
+
+  useEffect(() => {
+    if (formikValues) {
+      let formikClusterData = formikValues.filter(
+        (cluster) => cluster.provider === tableName
+      );
+      if (formikClusterData && formikClusterData.length > 0) {
+        let data = [];
+        formikClusterData[0].selectedClusters.forEach((selectedCluster) => {
+          data.push(selectedCluster.name);
+        });
+        setSelected(data);
+      }
+    }
+    setRows(clusters);
+  }, []);
+
+  useEffect(() => {
+    onRowSelect(tableName, selected);
+  }, [selected]);
+
+  const handleSelectAllClick = (event) => {
+    if (event.target.checked) {
+      const newSelecteds = rows.map((n) => n.name);
+      setSelected(newSelecteds);
+      return;
+    }
+    setSelected([]);
+  };
+
+  const handleClick = (event, name) => {
+    const selectedIndex = selected.indexOf(name);
+    let newSelected = [];
+
+    if (selectedIndex === -1) {
+      newSelected = newSelected.concat(selected, name);
+    } else if (selectedIndex === 0) {
+      newSelected = newSelected.concat(selected.slice(1));
+    } else if (selectedIndex === selected.length - 1) {
+      newSelected = newSelected.concat(selected.slice(0, -1));
+    } else if (selectedIndex > 0) {
+      newSelected = newSelected.concat(
+        selected.slice(0, selectedIndex),
+        selected.slice(selectedIndex + 1)
+      );
+    }
+    setSelected(newSelected);
+  };
+
+  const handleChangePage = (event, newPage) => {
+    setPage(newPage);
+  };
+
+  const handleChangeRowsPerPage = (event) => {
+    setRowsPerPage(parseInt(event.target.value, 10));
+    setPage(0);
+  };
+
+  const isSelected = (name) => selected.indexOf(name) !== -1;
+
+  const emptyRows =
+    rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage);
+
+  return (
+    <div className={classes.tableRoot}>
+      <EnhancedTableToolbar
+        tableName={tableName}
+        numSelected={selected.length}
+      />
+      <TableContainer>
+        <Table
+          className={classes.table}
+          aria-labelledby="tableTitle"
+          size={"small"}
+          aria-label="enhanced table"
+        >
+          <EnhancedTableHead
+            classes={classes}
+            numSelected={selected.length}
+            order={order}
+            orderBy={orderBy}
+            onSelectAllClick={handleSelectAllClick}
+            onRequestSort={handleRequestSort}
+            rowCount={rows.length}
+          />
+          <TableBody>
+            {stableSort(rows, getComparator(order, orderBy))
+              .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
+              .map((row, index) => {
+                const isItemSelected = isSelected(row.name);
+                const labelId = `enhanced-table-checkbox-${index}`;
+
+                return (
+                  <TableRow
+                    hover
+                    onClick={(event) => handleClick(event, row.name)}
+                    role="checkbox"
+                    aria-checked={isItemSelected}
+                    tabIndex={-1}
+                    key={row.name}
+                    selected={isItemSelected}
+                  >
+                    <TableCell padding="checkbox">
+                      <Checkbox
+                        checked={isItemSelected}
+                        inputProps={{ "aria-labelledby": labelId }}
+                      />
+                    </TableCell>
+                    <TableCell
+                      component="th"
+                      id={labelId}
+                      scope="row"
+                      padding="none"
+                    >
+                      {row.name}
+                    </TableCell>
+                    <TableCell align="right">{row.description}</TableCell>
+                  </TableRow>
+                );
+              })}
+            {emptyRows > 0 && (
+              <TableRow style={{ height: 33 * emptyRows }}>
+                <TableCell colSpan={6} />
+              </TableRow>
+            )}
+          </TableBody>
+        </Table>
+      </TableContainer>
+      <TablePagination
+        rowsPerPageOptions={[5, 10, 25]}
+        component="div"
+        count={rows.length}
+        rowsPerPage={rowsPerPage}
+        page={page}
+        onChangePage={handleChangePage}
+        onChangeRowsPerPage={handleChangeRowsPerPage}
+      />
+    </div>
+  );
+}
+
+export default EnhancedTable;
index 6dfb827..09f70c9 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import {
   TableContainer,
@@ -24,7 +24,7 @@ import {
 } from "@material-ui/core";
 import Paper from "@material-ui/core/Paper";
 import TableBody from "@material-ui/core/TableBody";
-import EditIcon from "@material-ui/icons/Edit";
+// import EditIcon from "@material-ui/icons/Edit";
 import DeleteIcon from "@material-ui/icons/Delete";
 import PropTypes from "prop-types";
 import apiService from "../../services/apiService";
@@ -106,31 +106,42 @@ const AppPlacementIntentTable = ({ data, setData, ...props }) => {
                 <StyledTableCell>{entry.name}</StyledTableCell>
                 <StyledTableCell>{entry.description}</StyledTableCell>
                 <StyledTableCell>
-                  {entry.allOf.map((intent, index) => (
-                    <Paper
-                      key={index}
-                      style={{ width: "max-content" }}
-                      variant="outlined"
-                    >
-                      <label>Cluster Provider :&nbsp;</label>
-                      <label style={{ fontWeight: "bold" }}>
-                        {intent["provider-name"]}, &nbsp;
-                      </label>
-                      <label>Labels : </label>
-                      <Chip
-                        style={{ marginRight: "10px" }}
-                        size="small"
-                        label={intent["cluster-label-name"]}
-                        color="primary"
+                  {entry.allOf &&
+                    entry.allOf.map((intent, index) => (
+                      <Paper
+                        key={index}
+                        style={{ width: "max-content" }}
                         variant="outlined"
-                      />
-                    </Paper>
-                  ))}
+                      >
+                        <label>Cluster Provider :&nbsp;</label>
+                        <label style={{ fontWeight: "bold" }}>
+                          {intent["provider-name"]}
+                        </label>
+                        <label>, &nbsp; Cluster :&nbsp;</label>
+                        <label style={{ fontWeight: "bold" }}>
+                          {intent["cluster-name"]}
+                        </label>
+                        {intent["cluster-label-name"] && (
+                          <>
+                            <label>, &nbsp; Labels : </label>
+                            <Chip
+                              style={{ marginRight: "10px" }}
+                              size="small"
+                              label={intent["cluster-label-name"]}
+                              color="primary"
+                              variant="outlined"
+                            />
+                          </>
+                        )}
+                      </Paper>
+                    ))}
                 </StyledTableCell>
                 <StyledTableCell>
+                  {/* 
+                  //edit app placement api has not been implemented yet
                   <IconButton onClick={(e) => handleEdit(index)} title="Edit">
                     <EditIcon color="primary" />
-                  </IconButton>
+                  </IconButton> */}
                   <IconButton
                     onClick={(e) => handleDelete(index)}
                     title="Delete"
index bb43972..9946c92 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import { makeStyles } from "@material-ui/core/styles";
 import clsx from "clsx";
@@ -168,6 +168,10 @@ const GenericPlacementIntentCard = (props) => {
               variant="outlined"
               size="small"
               color="secondary"
+              disabled={
+                appPlacementIntentData.applications &&
+                appPlacementIntentData.applications.length > 0
+              }
               style={{ float: "right" }}
               startIcon={<DeleteIcon />}
               onClick={() => {
@@ -190,6 +194,9 @@ const GenericPlacementIntentCard = (props) => {
                   }
                 />
               )}
+            {!(props.appsData && props.appsData.length > 0) && (
+              <div>No app found for adding app placement intent</div>
+            )}
           </CardContent>
         </Collapse>
       </Card>
index f0cf1e1..ee1fec7 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
-import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import Dialog from '@material-ui/core/Dialog';
-import MuiDialogTitle from '@material-ui/core/DialogTitle';
-import MuiDialogContent from '@material-ui/core/DialogContent';
-import MuiDialogActions from '@material-ui/core/DialogActions';
-import IconButton from '@material-ui/core/IconButton';
-import CloseIcon from '@material-ui/icons/Close';
-import Typography from '@material-ui/core/Typography';
-import { TextField, InputLabel, NativeSelect, FormControl, FormHelperText } from '@material-ui/core';
-import * as Yup from "yup";
-import { Formik } from 'formik';
+// ========================================================================
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { withStyles } from "@material-ui/core/styles";
+import Button from "@material-ui/core/Button";
+import Dialog from "@material-ui/core/Dialog";
+import MuiDialogTitle from "@material-ui/core/DialogTitle";
+import MuiDialogContent from "@material-ui/core/DialogContent";
+import MuiDialogActions from "@material-ui/core/DialogActions";
+import IconButton from "@material-ui/core/IconButton";
+import CloseIcon from "@material-ui/icons/Close";
+import Typography from "@material-ui/core/Typography";
+import Stepper from "./Stepper";
 import apiService from "../services/apiService";
 
 const styles = (theme) => ({
-    root: {
-        margin: 0,
-        padding: theme.spacing(2),
-    },
-    closeButton: {
-        position: 'absolute',
-        right: theme.spacing(1),
-        top: theme.spacing(1),
-        color: theme.palette.grey[500],
-    },
+  root: {
+    margin: 0,
+    padding: theme.spacing(2),
+  },
+  closeButton: {
+    position: "absolute",
+    right: theme.spacing(1),
+    top: theme.spacing(1),
+    color: theme.palette.grey[500],
+  },
 });
 
 const DialogTitle = withStyles(styles)((props) => {
-    const { children, classes, onClose, ...other } = props;
-    return (
-        <MuiDialogTitle disableTypography className={classes.root} {...other}>
-            <Typography variant="h6">{children}</Typography>
-            {onClose ? (
-                <IconButton className={classes.closeButton} onClick={onClose}>
-                    <CloseIcon />
-                </IconButton>
-            ) : null}
-        </MuiDialogTitle>
-    );
+  const { children, classes, onClose, ...other } = props;
+  return (
+    <MuiDialogTitle disableTypography className={classes.root} {...other}>
+      <Typography variant="h6">{children}</Typography>
+      {onClose ? (
+        <IconButton className={classes.closeButton} onClick={onClose}>
+          <CloseIcon />
+        </IconButton>
+      ) : null}
+    </MuiDialogTitle>
+  );
 });
 
 const DialogActions = withStyles((theme) => ({
-    root: {
-        margin: 0,
-        padding: theme.spacing(1),
-    },
+  root: {
+    margin: 0,
+    padding: theme.spacing(1),
+  },
 }))(MuiDialogActions);
 
 const DialogContent = withStyles((theme) => ({
-    root: {
-        padding: theme.spacing(2),
-    }
+  root: {
+    padding: theme.spacing(2),
+  },
 }))(MuiDialogContent);
 
-const schema = Yup.object(
-    {
-        name: Yup.string().required(),
-        description: Yup.string(),
-        version: Yup.string().required(),
-        compositeProfile: Yup.string().required(),
-        overrideValues: Yup.array().of(Yup.object()).typeError("Invalid override values, expected array"),
-    })
-
 const DIGform = (props) => {
-    const { onClose, item, open, onSubmit } = props;
-    const buttonLabel = item ? "OK" : "Create";
-    const title = item ? "Edit Deployment Intent Group" : "Create Deployment Intent Group";
-    const [selectedAppIndex, setSelectedAppIndex] = useState(0);
-    const handleClose = () => {
-        onClose();
-    };
-    useEffect(() => {
-        props.data.compositeApps.forEach(compositeApp => {
-            let request = { projectName: props.projectName, compositeAppName: compositeApp.metadata.name, compositeAppVersion: compositeApp.spec.version }
-            apiService.getCompositeProfiles(request).then(res => {
-                compositeApp.profiles = res;
-            }).catch(error => {
-                console.log("error getting cluster providers : ", error)
-            }).finally(() => {
-            })
+  const { onClose, item, open, onSubmit } = props;
+  const title = item
+    ? "Edit Deployment Intent Group"
+    : "Create Deployment Intent Group";
+  const handleClose = () => {
+    onClose();
+  };
+  useEffect(() => {
+    props.data.compositeApps.forEach((compositeApp) => {
+      let request = {
+        projectName: props.projectName,
+        compositeAppName: compositeApp.metadata.name,
+        compositeAppVersion: compositeApp.spec.version,
+      };
+      apiService
+        .getCompositeProfiles(request)
+        .then((res) => {
+          compositeApp.profiles = res;
         })
-    }, [props.data.compositeApps, props.projectName]);
-    let initialValues = item ?
-        { name: item.metadata.name, description: item.metadata.description, overrideValues: JSON.stringify(item.spec["override-values"]), compositeApp: item.compositeAppName, compositeProfile: item.spec.profile, version: item.spec.version } :
-        { name: "", description: "", overrideValues: undefined, compositeApp: props.data.compositeApps[0].metadata.name, compositeProfile: "", version: "" }
-
-    const handleSetCompositeApp = (val) => {
-        props.data.compositeApps.forEach((ca, index) => {
-            if (ca.metadata.name === val)
-                setSelectedAppIndex(index);
-        });
-    }
-
-    return (
-        <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick>
-            <DialogTitle id="simple-dialog-title">{title}</DialogTitle>
-            <Formik
-                initialValues={initialValues}
-                onSubmit={async values => {
-                    values.compositeAppVersion = props.data.compositeApps[selectedAppIndex].spec.version;
-                    onSubmit(values);
-                }}
-                validationSchema={schema}
-            >
-                {formicProps => {
-                    const {
-                        values,
-                        touched,
-                        errors,
-                        isSubmitting,
-                        handleChange,
-                        handleBlur,
-                        handleSubmit
-                    } = formicProps;
-                    return (
-                        <form noValidate onSubmit={handleSubmit} onChange={handleChange}>
-                            <DialogContent dividers>
-                                <div style={{ width: "45%", float: "left" }}>
-                                    <InputLabel shrink htmlFor="compositeApp-label-placeholder">
-                                        Composite App
-                                    </InputLabel>
-                                    <NativeSelect
-                                        name="compositeApp"
-                                        onChange={(e) => { handleChange(e); handleSetCompositeApp(e.target.value) }}
-                                        onBlur={handleBlur}
-                                        disabled={item ? true : false}
-                                        inputProps={{
-                                            name: 'compositeApp',
-                                            id: 'compositeApps-label-placeholder',
-                                        }}
-                                    >
-                                        {item && (<option >{values.compositeApp}</option>)}
-                                        {props.data && props.data.compositeApps.map(compositeApp =>
-                                            (<option value={compositeApp.metadata.name} key={compositeApp.metadata.name} >{compositeApp.metadata.name}</option>)
-                                        )}
-                                    </NativeSelect>
-                                </div>
-
-                                <FormControl style={{ width: "45%", float: "right" }} required error={errors.compositeProfile && touched.compositeProfile}>
-                                    <InputLabel htmlFor="compositeProfile-label-placeholder">
-                                        Composite Profile
-                                    </InputLabel>
-                                    <NativeSelect
-                                        name="compositeProfile"
-                                        onChange={handleChange}
-                                        onBlur={handleBlur}
-                                        disabled={item ? true : false}
-                                        required
-                                        inputProps={{
-                                            name: 'compositeProfile',
-                                            id: 'compositeProfile-label-placeholder',
-                                        }}
-                                    >
-                                        <option value="" />
-                                        {props.data.compositeApps[selectedAppIndex].profiles && props.data.compositeApps[selectedAppIndex].profiles.map(compositeProfile =>
-                                            (<option value={compositeProfile.metadata.name} key={compositeProfile.metadata.name} >{compositeProfile.metadata.name}</option>)
-                                        )}
-                                    </NativeSelect>
-                                    {errors.compositeProfile && touched.compositeProfile && <FormHelperText>Required</FormHelperText>}
-                                </FormControl>
-                                <TextField
-                                    style={{ width: "45%", float: "left", marginTop: "10px" }}
-                                    id="name"
-                                    label="Name"
-                                    type="text"
-                                    value={values.name}
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    helperText={(errors.name && touched.name && (
-                                        "Name is required"
-                                    ))}
-                                    required
-                                    error={errors.name && touched.name}
-                                />
-                                <TextField
-                                    style={{ width: "45%", float: "right", marginTop: "10px" }}
-                                    id="version"
-                                    label="Version"
-                                    type="text"
-                                    name="version"
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    helperText={(errors.version && touched.version && (
-                                        "Version is required"
-                                    ))}
-                                    required
-                                    error={errors.version && touched.version}
-                                />
-                                <TextField
-                                    style={{ width: "100%", marginTop: "20px" }}
-                                    id="overrideValues"
-                                    label="Override Values"
-                                    type="text"
-                                    value={values.overrideValues}
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    required
-                                    multiline
-                                    rows={4}
-                                    variant="outlined"
-                                    error={errors.overrideValues && touched.overrideValues}
-                                    helperText={(errors.overrideValues && touched.overrideValues && (
-                                        (errors["overrideValues"])
-                                    ))}
-                                />
-                                <TextField
-                                    style={{ width: "100%", marginBottom: "25px", marginTop: "10px" }}
-                                    name="description"
-                                    value={values.description}
-                                    onChange={handleChange}
-                                    onBlur={handleBlur}
-                                    id="description"
-                                    label="Description"
-                                    multiline
-                                    rowsMax={4}
-                                />
-                            </DialogContent>
-                            <DialogActions>
-                                <Button autoFocus onClick={handleClose} color="secondary">
-                                    Cancel
-                                </Button>
-                                <Button autoFocus type="submit" color="primary" disabled={isSubmitting}>
-                                    {buttonLabel}
-                                </Button>
-                            </DialogActions>
-                        </form>
-                    );
-                }}
-            </Formik>
-        </Dialog>
-    );
+        .catch((error) => {
+          console.log("error getting cluster providers : ", error);
+        })
+        .finally(() => {});
+    });
+  }, [props.data.compositeApps, props.projectName]);
+  return (
+    <Dialog
+      maxWidth={"md"}
+      fullWidth={true}
+      onClose={handleClose}
+      open={open}
+      disableBackdropClick
+    >
+      <DialogTitle id="customized-dialog-title" onClose={handleClose}>
+        {title}
+      </DialogTitle>
+      <DialogContent dividers>
+        <Stepper
+          data={props.data}
+          projectName={props.projectName}
+          onSubmit={onSubmit}
+        />
+      </DialogContent>
+    </Dialog>
+  );
 };
 
 DIGform.propTypes = {
-    onClose: PropTypes.func.isRequired,
-    open: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  open: PropTypes.bool.isRequired,
 };
 
 export default DIGform;
index 5710b52..3e22dc4 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import { withStyles, makeStyles } from "@material-ui/core/styles";
 import Table from "@material-ui/core/Table";
@@ -22,14 +22,14 @@ import TableHead from "@material-ui/core/TableHead";
 import TableRow from "@material-ui/core/TableRow";
 import Paper from "@material-ui/core/Paper";
 import IconButton from "@material-ui/core/IconButton";
-import EditIcon from "@material-ui/icons/Edit";
+// import EditIcon from "@material-ui/icons/Edit";
 import DeleteDialog from "../common/Dialogue";
-import AddIcon from "@material-ui/icons/Add";
+// import AddIcon from "@material-ui/icons/Add";
 import DeleteIcon from "@material-ui/icons/Delete";
 import GetAppIcon from "@material-ui/icons/GetApp";
 import apiService from "../services/apiService";
-import { Button } from "@material-ui/core";
-import IntentsForm from "./IntentsForm";
+// import { Button } from "@material-ui/core";
+// import IntentsForm from "./IntentsForm";
 import Notification from "../common/Notification";
 
 const StyledTableCell = withStyles((theme) => ({
@@ -58,20 +58,15 @@ const useStyles = makeStyles({
 export default function DIGtable({ data, setData, ...props }) {
   const classes = useStyles();
   const [open, setOpen] = useState(false);
-  // const [openForm, setOpenForm] = useState(false);
   const [index, setIndex] = useState(0);
-  const [openIntentsForm, setOpenIntentsForm] = useState(false);
+  // const [openIntentsForm, setOpenIntentsForm] = useState(false);
   const [notificationDetails, setNotificationDetails] = useState({});
-  let handleEdit = (index) => {
-    // setIndex(index);
-    // setOpenForm(true);
-  };
   const handleClose = (el) => {
     if (el.target.innerText === "Delete") {
       let request = {
         projectName: props.projectName,
-        compositeAppName: data[index].compositeAppName,
-        compositeAppVersion: data[index].compositeAppVersion,
+        compositeAppName: data[index].metadata.compositeAppName,
+        compositeAppVersion: data[index].metadata.compositeAppVersion,
         deploymentIntentGroupName: data[index].metadata.name,
       };
       apiService
@@ -92,50 +87,46 @@ export default function DIGtable({ data, setData, ...props }) {
     setIndex(index);
     setOpen(true);
   };
-  const handleAddIntent = (index) => {
-    setIndex(index);
-    setOpenIntentsForm(true);
-  };
-  const handleCloseIntentsForm = () => {
-    setOpenIntentsForm(false);
-  };
-  const handleSubmitIntentForm = (values) => {
-    setOpenIntentsForm(false);
-    let request = {
-      projectName: props.projectName,
-      compositeAppName: values.compositeAppName,
-      compositeAppVersion: values.compositeAppVersion,
-      deploymentIntentGroupName: values.deploymentIntentGroupName,
-      payload: {
-        metadata: { name: values.name, description: values.description },
-        spec: {
-          intent: {
-            genericPlacementIntent: values.genericPlacementIntent,
-          },
-        },
-      },
-    };
-    if (values.networkControllerIntent && values.networkControllerIntent !== "")
-      request.payload.spec.intent.ovnaction = values.networkControllerIntent;
-    apiService
-      .addIntentsToDeploymentIntentGroup(request)
-      .then((res) => {
-        if (data[index].intent) {
-          data[index].intent.push(res.spec.intent);
-        } else {
-          data[index].intent = [res.spec.intent];
-        }
-        setData([...data]);
-      })
-      .catch((err) => {
-        console.log("error adding intent to deployment intent group");
-      });
-  };
+  // const handleCloseIntentsForm = () => {
+  //   setOpenIntentsForm(false);
+  // };
+  // const handleSubmitIntentForm = (values) => {
+  //   setOpenIntentsForm(false);
+  //   let request = {
+  //     projectName: props.projectName,
+  //     compositeAppName: values.compositeAppName,
+  //     compositeAppVersion: values.compositeAppVersion,
+  //     deploymentIntentGroupName: values.deploymentIntentGroupName,
+  //     payload: {
+  //       metadata: { name: values.name, description: values.description },
+  //       spec: {
+  //         intent: {
+  //           genericPlacementIntent: values.genericPlacementIntent,
+  //         },
+  //       },
+  //     },
+  //   };
+  //   if (values.networkControllerIntent && values.networkControllerIntent !== "")
+  //     request.payload.spec.intent.ovnaction = values.networkControllerIntent;
+  //   apiService
+  //     .addIntentsToDeploymentIntentGroup(request)
+  //     .then((res) => {
+  //       if (data[index].intent) {
+  //         data[index].intent.push(res.spec.intent);
+  //       } else {
+  //         data[index].intent = [res.spec.intent];
+  //       }
+  //       setData([...data]);
+  //     })
+  //     .catch((err) => {
+  //       console.log("error adding intent to deployment intent group");
+  //     });
+  // };
   const handleInstantiate = (index) => {
     let request = {
       projectName: props.projectName,
-      compositeAppName: data[index].compositeAppName,
-      compositeAppVersion: data[index].compositeAppVersion,
+      compositeAppName: data[index].metadata.compositeAppName,
+      compositeAppVersion: data[index].metadata.compositeAppVersion,
       deploymentIntentGroupName: data[index].metadata.name,
     };
     apiService
@@ -184,13 +175,6 @@ export default function DIGtable({ data, setData, ...props }) {
       <Notification notificationDetails={notificationDetails} />
       {data && data.length > 0 && (
         <>
-          <IntentsForm
-            projectName={props.projectName}
-            open={openIntentsForm}
-            onClose={handleCloseIntentsForm}
-            onSubmit={handleSubmitIntentForm}
-            data={data[index]}
-          />
           <DeleteDialog
             open={open}
             onClose={handleClose}
@@ -207,7 +191,7 @@ export default function DIGtable({ data, setData, ...props }) {
                   <StyledTableCell>Version</StyledTableCell>
                   <StyledTableCell>Profile</StyledTableCell>
                   <StyledTableCell>Composite App</StyledTableCell>
-                  <StyledTableCell>Intents</StyledTableCell>
+                  {/* <StyledTableCell>Intents</StyledTableCell> */}
                   <StyledTableCell>Description</StyledTableCell>
                   <StyledTableCell style={{ width: "15%" }}>
                     Actions
@@ -225,54 +209,39 @@ export default function DIGtable({ data, setData, ...props }) {
                       {row.spec.profile}
                     </StyledTableCell>
                     <StyledTableCell className={classes.cell}>
-                      {row.compositeAppName}
+                      {row.metadata.compositeAppName}
                     </StyledTableCell>
-                    {
+                    {/* {
                       <StyledTableCell className={classes.cell}>
-                        {row.intent
-                          ? row.intent.map((intentEntry) => {
-                              return Object.keys(intentEntry)
-                                .map(function (k) {
-                                  return intentEntry[k];
-                                })
-                                .join(" | ");
-                            })
-                          : ""}
+                        {Object.keys(row.spec.deployedIntents[0]).map(function (
+                          key,
+                          index
+                        ) {
+                          if (
+                            index === 0 ||
+                            row.spec.deployedIntents[0][key] === ""
+                          )
+                            return row.spec.deployedIntents[0][key];
+                          else return ", " + row.spec.deployedIntents[0][key];
+                        })}
                       </StyledTableCell>
-                    }
+                    } */}
                     <StyledTableCell className={classes.cell}>
                       {row.metadata.description}
                     </StyledTableCell>
                     <StyledTableCell className={classes.cell}>
-                      <Button
-                        variant="outlined"
-                        color="primary"
-                        size="small"
-                        onClick={() => {
-                          handleAddIntent(index);
-                        }}
-                        startIcon={<AddIcon />}
-                      >
-                        Intents
-                      </Button>
                       <IconButton
-                        disabled={!(row.intent && row.intent.length > 0)}
+                        color={"primary"}
+                        // disabled={
+                        //   !(
+                        //     row.spec.deployedIntents &&
+                        //     row.spec.deployedIntents.length > 0
+                        //   )
+                        // }
                         title="Instantiate"
                         onClick={(e) => handleInstantiate(index)}
                       >
-                        <GetAppIcon
-                          color={
-                            !(row.intent && row.intent.length > 0)
-                              ? ""
-                              : "primary"
-                          }
-                        />
-                      </IconButton>
-                      <IconButton
-                        onClick={(e) => handleEdit(index)}
-                        title="Edit"
-                      >
-                        <EditIcon color="primary" />
+                        <GetAppIcon />
                       </IconButton>
                       <IconButton
                         onClick={(e) => handleDelete(index)}
index c1f73bb..132b9fc 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useEffect, useState } from "react";
 import DIGtable from "./DIGtable";
-import { withStyles, Button, Grid } from "@material-ui/core";
+import { withStyles, Button, Grid, Typography } from "@material-ui/core";
 import AddIcon from "@material-ui/icons/Add";
 import apiService from "../services/apiService";
 import Spinner from "../common/Spinner";
 import DIGform from "./DIGform";
+import { ReactComponent as EmptyIcon } from "../assets/icons/empty.svg";
 
 const styles = {
   root: {
@@ -44,84 +45,59 @@ const DeploymentIntentGroups = (props) => {
     setOpen(true);
   };
   const handleSubmit = (inputFields) => {
-    let payload = {
-      metadata: {
-        name: inputFields.name,
-        description: inputFields.description,
-      },
-      spec: {
-        profile: inputFields.compositeProfile,
-        version: inputFields.version,
-      },
-      projectName: props.projectName,
-      compositeAppName: inputFields.compositeApp,
-      compositeAppVersion: inputFields.compositeAppVersion,
-    };
-    if (inputFields.overrideValues && inputFields.overrideValues !== "") {
-      payload.spec["override-values"] = JSON.parse(inputFields.overrideValues);
+    try {
+      let payload = {
+        spec: {
+          projectName: props.projectName,
+          appsData: inputFields.intents.apps,
+        },
+      };
+      if (inputFields.overrideValues && inputFields.overrideValues !== "") {
+        payload.spec["override-values"] = JSON.parse(
+          inputFields.overrideValues
+        );
+      }
+      payload = { ...payload, ...inputFields.general };
+      apiService
+        .createDeploymentIntentGroup(payload)
+        .then((response) => {
+          response.metadata.compositeAppName = inputFields.general.compositeApp;
+          response.metadata.compositeAppVersion =
+            inputFields.general.compositeAppVersion;
+          data && data.length > 0
+            ? setData([...data, response])
+            : setData([response]);
+        })
+        .catch((error) => {
+          console.log("error creating DIG : ", error);
+        })
+        .finally(() => {
+          setIsloading(false);
+          setOpen(false);
+        });
+    } catch (error) {
+      console.error(error);
     }
-    apiService
-      .createDeploymentIntentGroup(payload)
-      .then((response) => {
-        response.compositeAppName = inputFields.compositeApp;
-        response.compositeAppVersion = inputFields.compositeAppVersion;
-        data && data.length > 0
-          ? setData([...data, response])
-          : setData([response]);
-      })
-      .catch((error) => {
-        console.log("error creating DIG : ", error);
-      })
-      .finally(() => {
-        setIsloading(false);
-        setOpen(false);
-      });
   };
 
   useEffect(() => {
+    let getDigs = () => {
+      apiService
+        .getDeploymentIntentGroups({ projectName: props.projectName })
+        .then((res) => {
+          setData(res);
+        })
+        .catch((err) => {
+          console.log("error getting deplotment intent groups : " + err);
+        })
+        .finally(() => setIsloading(false));
+    };
+
     apiService
       .getCompositeApps({ projectName: props.projectName })
       .then((response) => {
-        const getDigIntents = (input) => {
-          let request = {
-            projectName: props.projectName,
-            compositeAppName: input.compositeAppName,
-            compositeAppVersion: input.compositeAppVersion,
-            deploymentIntentGroupName: input.metadata.name,
-          };
-          apiService
-            .getDeploymentIntentGroupIntents(request)
-            .then((res) => {
-              input.intent = res.intent;
-            })
-            .catch((err) => {})
-            .finally(() => {
-              setData((data) => [...data, input]);
-            });
-        };
-        response.forEach((compositeApp) => {
-          let request = {
-            projectName: props.projectName,
-            compositeAppName: compositeApp.metadata.name,
-            compositeAppVersion: compositeApp.spec.version,
-          };
-          apiService
-            .getDeploymentIntentGroups(request)
-            .then((digResponse) => {
-              digResponse.forEach((res) => {
-                res.compositeAppName = compositeApp.metadata.name;
-                res.compositeAppVersion = compositeApp.spec.version;
-                getDigIntents(res);
-              });
-            })
-            .catch((error) => {
-              console.log("unable to get deployment intent groups", error);
-            })
-            .finally(() => {
-              setCompositeApps(response);
-              setIsloading(false);
-            });
-        });
+        setCompositeApps(response);
+        getDigs();
       })
       .catch((err) => {
         console.log("Unable to get composite apps : ", err);
@@ -131,16 +107,8 @@ const DeploymentIntentGroups = (props) => {
   return (
     <>
       {isLoading && <Spinner />}
-      {!isLoading && compositeApps && compositeApps.length > 0 && (
+      {!isLoading && compositeApps && (
         <>
-          <Button
-            variant="outlined"
-            color="primary"
-            startIcon={<AddIcon />}
-            onClick={onCreateDIG}
-          >
-            Create Deployment Intent Group
-          </Button>
           <DIGform
             projectName={props.projectName}
             open={open}
@@ -148,15 +116,41 @@ const DeploymentIntentGroups = (props) => {
             onSubmit={handleSubmit}
             data={{ compositeApps: compositeApps }}
           />
-          <Grid container spacing={2} alignItems="center">
-            <Grid item xs style={{ marginTop: "20px" }}>
-              <DIGtable
-                data={data}
-                setData={setData}
-                projectName={props.projectName}
-              />
-            </Grid>
+          <Grid item xs={12}>
+            <Button
+              variant="outlined"
+              color="primary"
+              startIcon={<AddIcon />}
+              onClick={onCreateDIG}
+            >
+              Create Deployment Intent Group
+            </Button>
           </Grid>
+
+          {data && data.length > 0 && (
+            <Grid container spacing={2} alignItems="center">
+              <Grid item xs style={{ marginTop: "20px" }}>
+                <DIGtable
+                  data={data}
+                  setData={setData}
+                  projectName={props.projectName}
+                />
+              </Grid>
+            </Grid>
+          )}
+
+          {(data === null || (data && data.length < 1)) && (
+            <Grid container spacing={2} direction="column" alignItems="center">
+              <Grid style={{ marginTop: "60px" }} item xs={6}>
+                <EmptyIcon style={{ height: "100px", width: "100px" }} />
+              </Grid>
+              <Grid item xs={12}>
+                <Typography variant="h6">
+                  No deployment group found, start by adding a deployment group
+                </Typography>
+              </Grid>
+            </Grid>
+          )}
         </>
       )}
     </>
diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx
new file mode 100644 (file)
index 0000000..e066245
--- /dev/null
@@ -0,0 +1,201 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import { makeStyles } from "@material-ui/core/styles";
+import PropTypes from "prop-types";
+import Tabs from "@material-ui/core/Tabs";
+import Tab from "@material-ui/core/Tab";
+import Box from "@material-ui/core/Box";
+import React, { useState } from "react";
+import Typography from "@material-ui/core/Typography";
+import { Formik } from "formik";
+import ExpandableCard from "../common/ExpandableCard";
+import AppPlacementForm from "../compositeApps/dialogs/AppFormPlacement";
+import NetworkForm from "../compositeApps/dialogs/AppNetworkForm";
+
+const useStyles = makeStyles((theme) => ({
+  tableRoot: {
+    width: "100%",
+  },
+  paper: {
+    width: "100%",
+    marginBottom: theme.spacing(2),
+  },
+  table: {
+    minWidth: 550,
+  },
+  visuallyHidden: {
+    border: 0,
+    clip: "rect(0 0 0 0)",
+    height: 1,
+    margin: -1,
+    overflow: "hidden",
+    padding: 0,
+    position: "absolute",
+    top: 20,
+    width: 1,
+  },
+  appBar: {
+    position: "relative",
+  },
+  title: {
+    marginLeft: theme.spacing(2),
+    flex: 1,
+  },
+  demo: {
+    backgroundColor: theme.palette.background.paper,
+  },
+  root: {
+    flexGrow: 1,
+    backgroundColor: theme.palette.background.paper,
+    display: "flex",
+    height: 424,
+  },
+  tabs: {
+    borderRight: `1px solid ${theme.palette.divider}`,
+  },
+}));
+function TabPanel(props) {
+  const { children, value, index, ...other } = props;
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      id={`vertical-tabpanel-${index}`}
+      aria-labelledby={`vertical-tab-${index}`}
+      {...other}
+    >
+      {value === index && <Box style={{ padding: "0 24px" }}>{children}</Box>}
+    </div>
+  );
+}
+
+function AppDetailsForm({ formikProps, ...props }) {
+  const classes = useStyles();
+  const [value, setValue] = useState(0);
+  const handleChange = (event, newValue) => {
+    setValue(newValue);
+  };
+  const handleRowSelect = (clusterProvider, selectedClusters) => {
+    if (
+      !formikProps.values.apps[props.index].clusters ||
+      formikProps.values.apps[props.index].clusters === undefined
+    ) {
+      if (selectedClusters.length > 0) {
+        let selectedClusterData = [];
+        selectedClusters.forEach((selectedCluster) => {
+          selectedClusterData.push({ name: selectedCluster, interfaces: [] });
+        });
+        formikProps.setFieldValue(`apps[${props.index}].clusters`, [
+          {
+            provider: clusterProvider,
+            selectedClusters: selectedClusterData,
+          },
+        ]);
+      }
+    } else {
+      let selectedClusterData = [];
+      //filter out the value of cluster provider so that it can be completely replaced by the new values
+      let updatedClusterValues = formikProps.values.apps[
+        props.index
+      ].clusters.filter((cluster) => cluster.provider !== clusterProvider);
+      selectedClusters.forEach((selectedCluster) => {
+        selectedClusterData.push({ name: selectedCluster, interfaces: [] });
+      });
+      if (selectedClusters.length > 0)
+        updatedClusterValues.push({
+          provider: clusterProvider,
+          selectedClusters: selectedClusterData,
+        });
+      formikProps.setFieldValue(
+        `apps[${props.index}].clusters`,
+        updatedClusterValues
+      );
+    }
+  };
+  return (
+    <div className={classes.root}>
+      <Formik>
+        {() => {
+          return (
+            <>
+              <Tabs
+                orientation="vertical"
+                variant="scrollable"
+                value={value}
+                onChange={handleChange}
+                aria-label="Vertical tabs example"
+                className={classes.tabs}
+              >
+                <Tab label="Placement" {...a11yProps(1)} />
+                <Tab label="Network" {...a11yProps(2)} />
+              </Tabs>
+              <TabPanel style={{ width: "85%" }} value={value} index={0}>
+                <AppPlacementForm
+                  formikProps={formikProps}
+                  index={props.index}
+                  clusterProviders={props.clusterProviders}
+                  handleRowSelect={handleRowSelect}
+                />
+              </TabPanel>
+              <TabPanel style={{ width: "85%" }} value={value} index={1}>
+                <Typography variant="subtitle1">Select Network</Typography>
+                <NetworkForm
+                  clusters={formikProps.values.apps[props.index].clusters}
+                  formikProps={formikProps}
+                  index={props.index}
+                />
+              </TabPanel>
+            </>
+          );
+        }}
+      </Formik>
+    </div>
+  );
+}
+
+TabPanel.propTypes = {
+  children: PropTypes.node,
+  index: PropTypes.any.isRequired,
+  value: PropTypes.any.isRequired,
+};
+
+function a11yProps(index) {
+  return {
+    id: `vertical-tab-${index}`,
+    "aria-controls": `vertical-tabpanel-${index}`,
+  };
+}
+
+const AppForm2 = (props) => {
+  return (
+    <ExpandableCard
+      error={
+        props.formikProps.errors.apps &&
+        props.formikProps.errors.apps[props.index]
+      }
+      title={props.name}
+      description={props.description}
+      content={
+        <AppDetailsForm
+          formikProps={props.formikProps}
+          name={props.name}
+          index={props.index}
+          clusterProviders={props.clusterProviders}
+        />
+      }
+    />
+  );
+};
+export default AppForm2;
diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx
new file mode 100644 (file)
index 0000000..5b5c419
--- /dev/null
@@ -0,0 +1,265 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import React, { useEffect, useState } from "react";
+import { Formik } from "formik";
+import * as Yup from "yup";
+
+import {
+  Button,
+  DialogActions,
+  FormControl,
+  FormHelperText,
+  Grid,
+  InputLabel,
+  MenuItem,
+  Select,
+  TextField,
+} from "@material-ui/core";
+
+const schema = Yup.object({
+  name: Yup.string().required(),
+  description: Yup.string(),
+  version: Yup.string()
+    .matches(/^[A-Za-z0-9\\s]+$/, "Special characters and space not allowed")
+    .required("Version is required"),
+  compositeProfile: Yup.string().required(),
+  overrideValues: Yup.array()
+    .of(Yup.object())
+    .typeError("Invalid override values, expected array"),
+});
+
+function DigFormGeneral(props) {
+  const { item, onSubmit } = props;
+  const [selectedAppIndex, setSelectedAppIndex] = useState(0); //let the first composite app as default selection
+  useEffect(() => {
+    if (item) {
+      props.data.compositeApps.forEach((ca, index) => {
+        if (ca.metadata.name === item.compositeApp) {
+          setSelectedAppIndex(index);
+        }
+      });
+    }
+  }, []);
+
+  let initialValues = item
+    ? {
+        ...item,
+      }
+    : {
+        name: "",
+        description: "",
+        overrideValues: undefined,
+        compositeApp: props.data.compositeApps[selectedAppIndex].metadata.name,
+        compositeProfile: "",
+        version: "",
+      };
+
+  const handleSetCompositeApp = (val) => {
+    props.data.compositeApps.forEach((ca, index) => {
+      if (ca.metadata.name === val) setSelectedAppIndex(index);
+    });
+  };
+  return (
+    <Formik
+      initialValues={initialValues}
+      onSubmit={(values) => {
+        values.compositeAppVersion =
+          props.data.compositeApps[selectedAppIndex].spec.version;
+        onSubmit(values);
+      }}
+      validationSchema={schema}
+    >
+      {(formicProps) => {
+        const {
+          values,
+          touched,
+          errors,
+          isSubmitting,
+          handleChange,
+          handleBlur,
+          handleSubmit,
+        } = formicProps;
+        return (
+          <form noValidate onSubmit={handleSubmit} onChange={handleChange}>
+            <Grid container spacing={4} justify="center">
+              <Grid container item xs={12} spacing={8}>
+                <Grid item xs={12} md={6}>
+                  <TextField
+                    fullWidth
+                    id="name"
+                    label="Name"
+                    type="text"
+                    value={values.name}
+                    onChange={handleChange}
+                    onBlur={handleBlur}
+                    helperText={
+                      errors.name && touched.name && "Name is required"
+                    }
+                    required
+                    error={errors.name && touched.name}
+                  />
+                </Grid>
+                <Grid item xs={12} md={6}>
+                  <TextField
+                    fullWidth
+                    id="version"
+                    label="Version"
+                    type="text"
+                    name="version"
+                    value={values.version}
+                    onChange={handleChange}
+                    onBlur={handleBlur}
+                    helperText={
+                      errors.version && touched.version && errors["version"]
+                    }
+                    required
+                    error={errors.version && touched.version}
+                  />
+                </Grid>
+              </Grid>
+
+              <Grid item container xs={12} spacing={8}>
+                <Grid item xs={12} md={6}>
+                  <InputLabel shrink htmlFor="compositeApp-label-placeholder">
+                    Composite App
+                  </InputLabel>
+                  <Select
+                    fullWidth
+                    name="compositeApp"
+                    value={values.compositeApp}
+                    onChange={(e) => {
+                      handleChange(e);
+                      handleSetCompositeApp(e.target.value);
+                    }}
+                    onBlur={handleBlur}
+                    inputProps={{
+                      name: "compositeApp",
+                      id: "compositeApps-label-placeholder",
+                    }}
+                  >
+                    {props.data &&
+                      props.data.compositeApps.map((compositeApp) => (
+                        <MenuItem
+                          value={compositeApp.metadata.name}
+                          key={compositeApp.metadata.name}
+                        >
+                          {compositeApp.metadata.name}
+                        </MenuItem>
+                      ))}
+                  </Select>
+                </Grid>
+                <Grid item xs={12} md={6}>
+                  <FormControl
+                    fullWidth
+                    required
+                    error={errors.compositeProfile && touched.compositeProfile}
+                  >
+                    <InputLabel htmlFor="compositeProfile-label-placeholder">
+                      Composite Profile
+                    </InputLabel>
+                    <Select
+                      name="compositeProfile"
+                      onChange={handleChange}
+                      onBlur={handleBlur}
+                      required
+                      value={values.compositeProfile}
+                      inputProps={{
+                        name: "compositeProfile",
+                        id: "compositeProfile-label-placeholder",
+                      }}
+                    >
+                      {props.data.compositeApps[selectedAppIndex].profiles &&
+                        props.data.compositeApps[selectedAppIndex].profiles.map(
+                          (compositeProfile) => (
+                            <MenuItem
+                              value={compositeProfile.metadata.name}
+                              key={compositeProfile.metadata.name}
+                            >
+                              {compositeProfile.metadata.name}
+                            </MenuItem>
+                          )
+                        )}
+                    </Select>
+                    {errors.compositeProfile && touched.compositeProfile && (
+                      <FormHelperText>Required</FormHelperText>
+                    )}
+                  </FormControl>
+                </Grid>
+              </Grid>
+
+              <Grid item container xs={12} spacing={8}>
+                <Grid item xs={12} md={6}>
+                  <TextField
+                    fullWidth
+                    name="description"
+                    value={values.description}
+                    onChange={handleChange}
+                    onBlur={handleBlur}
+                    id="description"
+                    label="Description"
+                    multiline
+                    rowsMax={4}
+                  />
+                </Grid>
+                <Grid item xs={12} md={6}>
+                  <TextField
+                    fullWidth
+                    id="overrideValues"
+                    label="Override Values"
+                    type="text"
+                    value={values.overrideValues}
+                    onChange={handleChange}
+                    onBlur={handleBlur}
+                    multiline
+                    rows={4}
+                    variant="outlined"
+                    error={errors.overrideValues && touched.overrideValues}
+                    helperText={
+                      errors.overrideValues &&
+                      touched.overrideValues &&
+                      errors["overrideValues"]
+                    }
+                  />
+                </Grid>
+              </Grid>
+              <Grid item xs={12}>
+                <DialogActions>
+                  <Button
+                    autoFocus
+                    disabled
+                    onClick={props.onClickBack}
+                    color="secondary"
+                  >
+                    Back
+                  </Button>
+                  <Button
+                    autoFocus
+                    type="submit"
+                    color="primary"
+                    disabled={isSubmitting}
+                  >
+                    Next
+                  </Button>
+                </DialogActions>
+              </Grid>
+            </Grid>
+          </form>
+        );
+      }}
+    </Formik>
+  );
+}
+
+export default DigFormGeneral;
diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx
new file mode 100644 (file)
index 0000000..580044a
--- /dev/null
@@ -0,0 +1,160 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import React, { useEffect, useState } from "react";
+import { Formik } from "formik";
+import * as Yup from "yup";
+import AppForm from "./DigFormApp";
+import apiService from "../services/apiService";
+
+import { Button, DialogActions, Grid } from "@material-ui/core";
+
+DigFormIntents.propTypes = {};
+const schema = Yup.object({
+  apps: Yup.array()
+    .of(
+      Yup.object({
+        clusters: Yup.array()
+          .of(
+            Yup.object({
+              provider: Yup.string(),
+              selectedClusters: Yup.array().of(
+                Yup.object({
+                  name: Yup.string(),
+                  interfaces: Yup.array().of(
+                    Yup.object({
+                      networkName: Yup.string().required(),
+                      subnet: Yup.string().required(),
+                      ip: Yup.string().matches(
+                        /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
+                        "invalid ip address"
+                      ),
+                    })
+                  ),
+                })
+              ),
+            })
+          )
+          .required("Select at least one cluster"),
+      })
+    )
+    .required("At least one app is required"),
+});
+
+function DigFormIntents(props) {
+  const { onSubmit, appsData } = props;
+  const [isLoading, setIsloading] = useState(true);
+  const [clusterProviders, setClusterProviders] = useState([]);
+  let initialValues = { apps: appsData };
+  useEffect(() => {
+    let clusterProviderData = [];
+    apiService
+      .getClusterProviders()
+      .then((res) => {
+        res.forEach((clusterProvider, providerIndex) => {
+          clusterProviderData.push({
+            name: clusterProvider.metadata.name,
+            clusters: [],
+          });
+          apiService
+            .getClusters(clusterProvider.metadata.name)
+            .then((clusters) => {
+              clusters.forEach((cluster) => {
+                clusterProviderData[providerIndex].clusters.push({
+                  name: cluster.metadata.name,
+                  description: cluster.metadata.description,
+                });
+              });
+              if (providerIndex + 1 === res.length) {
+                setClusterProviders(clusterProviderData);
+                setIsloading(false);
+              }
+            })
+            .catch((err) => {
+              console.log(
+                `error getting clusters for ${clusterProvider.metadata.name} : ` +
+                  err
+              );
+            });
+        });
+      })
+      .catch((err) => {
+        console.log("error getting cluster providers : " + err);
+      });
+  }, []);
+  useEffect(() => {}, []);
+
+  return (
+    <Formik
+      initialValues={initialValues}
+      onSubmit={(values) => {
+        values.compositeAppVersion = onSubmit(values);
+      }}
+      validationSchema={schema}
+    >
+      {(formikProps) => {
+        const {
+          values,
+          isSubmitting,
+          handleChange,
+          handleSubmit,
+        } = formikProps;
+        return (
+          !isLoading && (
+            <form noValidate onSubmit={handleSubmit} onChange={handleChange}>
+              <Grid container spacing={4} justify="center">
+                {initialValues.apps &&
+                  initialValues.apps.length > 0 &&
+                  initialValues.apps.map((app, index) => (
+                    <Grid key={index} item sm={12} xs={12}>
+                      <AppForm
+                        clusterProviders={clusterProviders}
+                        formikProps={formikProps}
+                        name={app.metadata.name}
+                        description={app.metadata.description}
+                        index={index}
+                        initialValues={values}
+                      />
+                    </Grid>
+                  ))}
+
+                <Grid item xs={12}>
+                  <DialogActions>
+                    <Button
+                      autoFocus
+                      onClick={props.onClickBack}
+                      color="secondary"
+                    >
+                      Back
+                    </Button>
+                    <Button
+                      autoFocus
+                      type="submit"
+                      color="primary"
+                      disabled={isSubmitting}
+                    >
+                      Submit
+                    </Button>
+                  </DialogActions>
+                </Grid>
+              </Grid>
+            </form>
+          )
+        );
+      }}
+    </Formik>
+  );
+}
+
+export default DigFormIntents;
diff --git a/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx b/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx
new file mode 100644 (file)
index 0000000..746f49e
--- /dev/null
@@ -0,0 +1,118 @@
+//=======================================================================
+// Copyright (c) 2017-2020 Aarna Networks, Inc.
+// All rights reserved.
+// ======================================================================
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//           http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+import React, { useState } from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import Stepper from "@material-ui/core/Stepper";
+import Step from "@material-ui/core/Step";
+import StepLabel from "@material-ui/core/StepLabel";
+import DigFormGeneral from "./DigFormGeneral";
+import DigFormIntents from "./DigFormIntents";
+import apiService from "../services/apiService";
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    width: "100%",
+  },
+  backButton: {
+    marginRight: theme.spacing(1),
+  },
+  instructions: {
+    marginTop: theme.spacing(1),
+    marginBottom: theme.spacing(1),
+  },
+}));
+
+function getSteps() {
+  return ["General", "Intents"];
+}
+
+export default function HorizontalStepper(props) {
+  const classes = useStyles();
+  const [activeStep, setActiveStep] = useState(0);
+  const [generalData, setGeneralData] = useState(null);
+  const [intentsData, setIntentsData] = useState(null);
+  const [appsData, setAppsData] = useState([]);
+
+  const steps = getSteps();
+
+  function getStepContent(stepIndex) {
+    switch (stepIndex) {
+      case 0:
+        return (
+          <DigFormGeneral
+            data={props.data}
+            onSubmit={handleGeneralFormSubmit}
+            item={generalData}
+          />
+        );
+      case 1:
+        return (
+          <DigFormIntents
+            appsData={appsData}
+            onSubmit={handleIntentsFormSubmit}
+            onClickBack={handleBack}
+            item={intentsData}
+          />
+        );
+      default:
+        return "Unknown stepIndex";
+    }
+  }
+
+  const handleNext = () => {
+    setActiveStep((prevActiveStep) => prevActiveStep + 1);
+  };
+
+  const handleBack = () => {
+    setActiveStep((prevActiveStep) => prevActiveStep - 1);
+  };
+  const handleGeneralFormSubmit = (values) => {
+    setGeneralData(values);
+    let request = {
+      projectName: props.projectName,
+      compositeAppName: values.compositeApp,
+      compositeAppVersion: values.compositeAppVersion,
+    };
+    apiService
+      .getApps(request)
+      .then((res) => {
+        setAppsData(res);
+        handleNext((prevActiveStep) => prevActiveStep + 1);
+      })
+      .catch((err) => {
+        console.log("Error getting apps : " + err);
+      });
+  };
+
+  const handleIntentsFormSubmit = (values) => {
+    setIntentsData(values);
+    let digPayload = { general: generalData, intents: values };
+    props.onSubmit(digPayload);
+  };
+  return (
+    <div className={classes.root}>
+      <Stepper activeStep={activeStep} alternativeLabel>
+        {steps.map((label) => (
+          <Step key={label}>
+            <StepLabel>{label}</StepLabel>
+          </Step>
+        ))}
+      </Stepper>
+      <div>
+        <div>{getStepContent(activeStep)}</div>
+      </div>
+    </div>
+  );
+}
index b641737..ed93bd0 100644 (file)
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import React, { useState } from "react";
 import { makeStyles } from "@material-ui/core/styles";
 import clsx from "clsx";
@@ -54,7 +54,7 @@ const NetworkIntentCard = (props) => {
   const [expanded, setExpanded] = useState(false);
   const [workloadData, setWorkloadData] = useState([]);
   const handleExpandClick = () => {
-    if (!expanded && workloadData.length < 1) {
+    if (!expanded && workloadData && workloadData.length < 1) {
       let request = {
         projectName: props.projectName,
         compositeAppName: props.compositeAppName,
@@ -189,6 +189,7 @@ const NetworkIntentCard = (props) => {
               variant="outlined"
               size="small"
               color="secondary"
+              disabled={workloadData && workloadData.length > 0}
               style={{ float: "right" }}
               startIcon={<DeleteIcon />}
               onClick={props.onDeleteNetworkControllerIntent.bind(
@@ -210,6 +211,9 @@ const NetworkIntentCard = (props) => {
                 }
               />
             )}
+            {!(props.appsData && props.appsData.length > 0) && (
+              <div>No app found for adding workload intent</div>
+            )}
           </CardContent>
         </Collapse>
       </Card>
index 5e196ed..540de8a 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
-import React, { useState } from 'react';
-import { TableContainer, Table, TableRow, TableHead, withStyles, Chip, TableCell } from '@material-ui/core';
+// ========================================================================
+import React, { useState } from "react";
+import {
+  TableContainer,
+  Table,
+  TableRow,
+  TableHead,
+  withStyles,
+  Chip,
+  TableCell,
+} from "@material-ui/core";
 import Paper from "@material-ui/core/Paper";
 import TableBody from "@material-ui/core/TableBody";
 import EditIcon from "@material-ui/icons/Edit";
 import DeleteIcon from "@material-ui/icons/Delete";
-import PropTypes from 'prop-types';
+import PropTypes from "prop-types";
 import apiService from "../services/apiService";
 import DeleteDialog from "../common/Dialogue";
-import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';
-import IconButton from '@material-ui/core/IconButton';
-import AddIconOutline from '@material-ui/icons/AddCircleOutline';
+import InfoOutlinedIcon from "@material-ui/icons/InfoOutlined";
+import IconButton from "@material-ui/core/IconButton";
+import AddIconOutline from "@material-ui/icons/AddCircleOutline";
 import Form from "./InterfaceForm";
 import InterfaceDetailsDialog from "../common/DetailsDialog";
 
-
 const StyledTableCell = withStyles((theme) => ({
-    body: {
-        fontSize: 14,
-    },
+  body: {
+    fontSize: 14,
+  },
 }))(TableCell);
 
 const StyledTableRow = withStyles((theme) => ({
-    root: {
-        "&:nth-of-type(odd)": {
-            backgroundColor: theme.palette.action.hover,
-        },
+  root: {
+    "&:nth-of-type(odd)": {
+      backgroundColor: theme.palette.action.hover,
     },
+  },
 }))(TableRow);
 
 const WokloadIntentTable = ({ data, setData, ...props }) => {
-    const [formOpen, setFormOpen] = useState(false);
-    const [index, setIndex] = useState(0);
-    const [openDialog, setOpenDialog] = useState(false);
-    const [openInterfaceDetails, setOpenInterfaceDetails] = useState(false);
-    const [selectedInterface, setSelectedInterface] = useState({});
-    const [openInterfaceDialog, setOpenInterfaceDialog] = useState(false);
-    const handleDelete = (index) => {
-        setIndex(index);
-        setOpenDialog(true);
-    }
-    const handleEdit = () => {
+  const [formOpen, setFormOpen] = useState(false);
+  const [index, setIndex] = useState(0);
+  const [openDialog, setOpenDialog] = useState(false);
+  const [openInterfaceDetails, setOpenInterfaceDetails] = useState(false);
+  const [selectedInterface, setSelectedInterface] = useState({});
+  const [openInterfaceDialog, setOpenInterfaceDialog] = useState(false);
+  const handleDelete = (index) => {
+    setIndex(index);
+    setOpenDialog(true);
+  };
+  const handleEdit = () => {};
+  const handleInterfaceDetailOpen = (entry) => {
+    setSelectedInterface(entry);
+    setOpenInterfaceDetails(true);
+  };
 
+  const handleDeleteInterface = (index, entry) => {
+    setIndex(index);
+    setSelectedInterface(entry);
+    setOpenInterfaceDialog(true);
+  };
+  const handleAddInterface = (index) => {
+    setIndex(index);
+    setFormOpen(true);
+  };
+  const handleCloseForm = () => {
+    setFormOpen(false);
+  };
+  const handleCloseInterfaceDialog = (el) => {
+    if (el.target.innerText === "Delete") {
+      let request = {
+        projectName: props.projectName,
+        compositeAppName: props.compositeAppName,
+        compositeAppVersion: props.compositeAppVersion,
+        networkControllerIntentName: props.networkControllerIntentName,
+        workloadIntentName: data[index].metadata.name,
+        interfaceName: selectedInterface.metadata.name,
+      };
+      apiService
+        .deleteInterface(request)
+        .then(() => {
+          console.log("Interface deleted");
+          let updatedInterfaceData = data[index].interfaces.filter(function (
+            obj
+          ) {
+            return obj.metadata.name !== selectedInterface.metadata.name;
+          });
+          data[index].interfaces = updatedInterfaceData;
+          setData([...data]);
+        })
+        .catch((err) => {
+          console.log("Error deleting interface : ", err);
+        })
+        .finally(() => {
+          setIndex(0);
+          setSelectedInterface({});
+        });
     }
-    const handleInterfaceDetailOpen = (entry) => {
-        setSelectedInterface(entry);
-        setOpenInterfaceDetails(true);
+    setOpenInterfaceDialog(false);
+  };
+  const handleCloseDialog = (el) => {
+    if (el.target.innerText === "Delete") {
+      let request = {
+        projectName: props.projectName,
+        compositeAppName: props.compositeAppName,
+        compositeAppVersion: props.compositeAppVersion,
+        networkControllerIntentName: props.networkControllerIntentName,
+        workloadIntentName: data[index].metadata.name,
+      };
+      apiService
+        .deleteWorkloadIntent(request)
+        .then(() => {
+          console.log("workload intent deleted");
+          data.splice(index, 1);
+          setData([...data]);
+        })
+        .catch((err) => {
+          console.log("Error deleting workload intent : ", err);
+        })
+        .finally(() => {
+          setIndex(0);
+        });
     }
-
-    const handleDeleteInterface = (index, entry) => {
-        setIndex(index);
-        setSelectedInterface(entry);
-        setOpenInterfaceDialog(true);
-    }
-    const handleAddInterface = (index) => {
-        setIndex(index);
-        setFormOpen(true);
-    }
-    const handleCloseForm = () => {
-        setFormOpen(false);
-    }
-    const handleCloseInterfaceDialog = (el) => {
-        if (el.target.innerText === "Delete") {
-            let request = {
-                projectName: props.projectName,
-                compositeAppName: props.compositeAppName,
-                compositeAppVersion: props.compositeAppVersion,
-                networkControllerIntentName: props.networkControllerIntentName,
-                workloadIntentName: data[index].metadata.name,
-                interfaceName: selectedInterface.metadata.name
-            }
-            apiService.deleteInterface(request).then(() => {
-                console.log("Interface deleted");
-                let updatedInterfaceData = data[index].interfaces.filter(function (obj) {
-                    return obj.metadata.name !== selectedInterface.metadata.name;
-                });
-                data[index].interfaces = updatedInterfaceData;
-                setData([...data]);
-            }).catch(err => {
-                console.log("Error deleting interface : ", err)
-            }).finally(() => {
-                setIndex(0);
-                setSelectedInterface({});
-            })
-        }
-        setOpenInterfaceDialog(false);
-    }
-    const handleCloseDialog = (el) => {
-        if (el.target.innerText === "Delete") {
-            let request = {
-                projectName: props.projectName,
-                compositeAppName: props.compositeAppName,
-                compositeAppVersion: props.compositeAppVersion,
-                networkControllerIntentName: props.networkControllerIntentName,
-                workloadIntentName: data[index].metadata.name
-            }
-            apiService.deleteWorkloadIntent(request).then(() => {
-                console.log("workload intent deleted");
-                data.splice(index, 1);
-                setData([...data]);
-            }).catch(err => {
-                console.log("Error deleting workload intent : ", err)
-            }).finally(() => {
-                setIndex(0);
-            })
+    setOpenDialog(false);
+  };
+  const handleSubmit = (values) => {
+    let spec = values.spec ? JSON.parse(values.spec) : "";
+    let request = {
+      payload: {
+        metadata: { name: values.name, description: values.description },
+        spec: spec,
+      },
+      projectName: props.projectName,
+      compositeAppName: props.compositeAppName,
+      compositeAppVersion: props.compositeAppVersion,
+      networkControllerIntentName: props.networkControllerIntentName,
+      workloadIntentName: data[index].metadata.name,
+    };
+    apiService
+      .addInterface(request)
+      .then((res) => {
+        if (data[index].interfaces && data[index].interfaces.length > 0) {
+          data[index].interfaces.push(res);
+        } else {
+          data[index].interfaces = [res];
         }
-        setOpenDialog(false);
-    }
-    const handleSubmit = (values) => {
-        let spec = values.spec ? JSON.parse(values.spec) : "";
-        let request = {
-            payload: { metadata: { name: values.name, description: values.description }, spec: spec },
-            projectName: props.projectName,
-            compositeAppName: props.compositeAppName,
-            compositeAppVersion: props.compositeAppVersion,
-            networkControllerIntentName: props.networkControllerIntentName,
-            workloadIntentName: data[index].metadata.name
-        };
-        apiService.addInterface(request)
-            .then(res => {
-                if (data[index].interfaces && data[index].interfaces.length > 0) {
-                    data[index].interfaces.push(res);
-                }
-                else {
-                    data[index].interfaces = [res];
-                }
-                setData([...data]);
-            })
-            .catch(err => {
-                console.log("error creating composite profile : ", err);
-            })
-            .finally(() => { setFormOpen(false); })
-    }
-    return (
-        <>
-            <InterfaceDetailsDialog open={openInterfaceDetails} onClose={setOpenInterfaceDetails} item={selectedInterface} type="Interface" />
-            <Form open={formOpen} onClose={handleCloseForm} onSubmit={handleSubmit} />
-            <DeleteDialog open={openDialog} onClose={handleCloseDialog} title={"Delete Profile"}
-                content={`Are you sure you want to delete "${data && data[index] ? data[index].metadata.name : ""}"`} />
-            <DeleteDialog open={openInterfaceDialog} onClose={handleCloseInterfaceDialog} title={"Delete Interface"}
-                content={`Are you sure you want to delete "${selectedInterface.metadata ? selectedInterface.metadata.name : ""}"`} />
-            <TableContainer component={Paper}>
-                <Table>
-                    <TableHead>
-                        <TableRow>
-                            <StyledTableCell>Name</StyledTableCell>
-                            <StyledTableCell>Description</StyledTableCell>
-                            <StyledTableCell>App</StyledTableCell>
-                            <StyledTableCell>Workload Resource</StyledTableCell>
-                            <StyledTableCell style={{ width: "27%" }}>Interfaces</StyledTableCell>
-                            <StyledTableCell>Actions</StyledTableCell>
-                        </TableRow>
-                    </TableHead>
-                    <TableBody>
-                        {data.map((entry, index) =>
-                            <StyledTableRow key={entry.metadata.name + index}>
-                                <StyledTableCell>
-                                    {entry.metadata.name}
-                                </StyledTableCell>
-                                <StyledTableCell >
-                                    {entry.metadata.description}
-                                </StyledTableCell>
-                                <StyledTableCell >
-                                    {entry.spec["application-name"]}
-                                </StyledTableCell>
-                                <StyledTableCell>
-                                    {entry.spec["workload-resource"]}
-                                </StyledTableCell>
-                                <StyledTableCell>
-                                    {entry.interfaces && (entry.interfaces.length > 0) && entry.interfaces.map((interfaceEntry, interfacekIndex) =>
-                                        (<Chip
-                                            key={interfaceEntry.metadata.name + "" + interfacekIndex}
-                                            size="small"
-                                            icon={<InfoOutlinedIcon onClick={() => { handleInterfaceDetailOpen(interfaceEntry) }} style={{ cursor: "pointer" }} />}
-                                            onDelete={(e) => { handleDeleteInterface(index, interfaceEntry) }}
-                                            label={interfaceEntry.spec.ipAddress}
-                                            style={{ marginRight: "10px", marginBottom: "5px" }}
-                                        />)
-                                    )}
-                                    <IconButton color="primary" onClick={() => { handleAddInterface(index) }}>
-                                        <AddIconOutline />
-                                    </IconButton>
-                                </StyledTableCell>
-                                <StyledTableCell >
-                                    <IconButton onClick={(e) => handleEdit(index)} title="Edit" >
-                                        <EditIcon color="primary" />
-                                    </IconButton>
-                                    <IconButton onClick={(e) => handleDelete(index)} title="Delete" >
-                                        <DeleteIcon color="secondary" />
-                                    </IconButton>
-                                </StyledTableCell>
-                            </StyledTableRow>
-                        )}
-                    </TableBody>
-                </Table>
-            </TableContainer></>
-    );
+        setData([...data]);
+      })
+      .catch((err) => {
+        console.log("error creating composite profile : ", err);
+      })
+      .finally(() => {
+        setFormOpen(false);
+      });
+  };
+  return (
+    <>
+      <InterfaceDetailsDialog
+        open={openInterfaceDetails}
+        onClose={setOpenInterfaceDetails}
+        item={selectedInterface}
+        type="Interface"
+      />
+      <Form open={formOpen} onClose={handleCloseForm} onSubmit={handleSubmit} />
+      <DeleteDialog
+        open={openDialog}
+        onClose={handleCloseDialog}
+        title={"Delete Profile"}
+        content={`Are you sure you want to delete "${
+          data && data[index] ? data[index].metadata.name : ""
+        }"`}
+      />
+      <DeleteDialog
+        open={openInterfaceDialog}
+        onClose={handleCloseInterfaceDialog}
+        title={"Delete Interface"}
+        content={`Are you sure you want to delete "${
+          selectedInterface.metadata ? selectedInterface.metadata.name : ""
+        }"`}
+      />
+      <TableContainer component={Paper}>
+        <Table>
+          <TableHead>
+            <TableRow>
+              <StyledTableCell>Name</StyledTableCell>
+              <StyledTableCell>Description</StyledTableCell>
+              <StyledTableCell>App</StyledTableCell>
+              <StyledTableCell>Workload Resource</StyledTableCell>
+              <StyledTableCell style={{ width: "27%" }}>
+                Interfaces
+              </StyledTableCell>
+              <StyledTableCell>Actions</StyledTableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            {data.map((entry, index) => (
+              <StyledTableRow key={entry.metadata.name + index}>
+                <StyledTableCell>{entry.metadata.name}</StyledTableCell>
+                <StyledTableCell>{entry.metadata.description}</StyledTableCell>
+                <StyledTableCell>
+                  {entry.spec["application-name"]}
+                </StyledTableCell>
+                <StyledTableCell>
+                  {entry.spec["workload-resource"]}
+                </StyledTableCell>
+                <StyledTableCell>
+                  {entry.interfaces &&
+                    entry.interfaces.length > 0 &&
+                    entry.interfaces.map((interfaceEntry, interfacekIndex) => (
+                      <Chip
+                        key={
+                          interfaceEntry.metadata.name + "" + interfacekIndex
+                        }
+                        size="small"
+                        icon={
+                          <InfoOutlinedIcon
+                            onClick={() => {
+                              handleInterfaceDetailOpen(interfaceEntry);
+                            }}
+                            style={{ cursor: "pointer" }}
+                          />
+                        }
+                        onDelete={(e) => {
+                          handleDeleteInterface(index, interfaceEntry);
+                        }}
+                        label={interfaceEntry.spec.ipAddress}
+                        style={{ marginRight: "10px", marginBottom: "5px" }}
+                      />
+                    ))}
+                  <IconButton
+                    color="primary"
+                    onClick={() => {
+                      handleAddInterface(index);
+                    }}
+                  >
+                    <AddIconOutline />
+                  </IconButton>
+                </StyledTableCell>
+                <StyledTableCell>
+                  {/* 
+                  //edit workload intent api has not been added yet
+                  <IconButton onClick={(e) => handleEdit(index)} title="Edit" >
+                      <EditIcon color="primary" />
+                  </IconButton> */}
+                  <IconButton
+                    color="secondary"
+                    disabled={entry.interfaces && entry.interfaces.length > 0}
+                    onClick={(e) => handleDelete(index)}
+                    title="Delete"
+                  >
+                    <DeleteIcon />
+                  </IconButton>
+                </StyledTableCell>
+              </StyledTableRow>
+            ))}
+          </TableBody>
+        </Table>
+      </TableContainer>
+    </>
+  );
 };
 WokloadIntentTable.propTypes = {
-    data: PropTypes.arrayOf(PropTypes.object).isRequired,
-    setData: PropTypes.func.isRequired
+  data: PropTypes.arrayOf(PropTypes.object).isRequired,
+  setData: PropTypes.func.isRequired,
 };
 export default WokloadIntentTable;
index 4bff930..0c83076 100644 (file)
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-// ========================================================================  
+// ========================================================================
 import axios from "axios";
 axios.defaults.baseURL = process.env.REACT_APP_BACKEND || "";
-
 //orchestrator
 //projects
 const createProject = (request) => {
@@ -45,6 +44,14 @@ const getCompositeApps = (request) => {
       return res.data;
     });
 };
+const addService = ({ projectName, ...request }) => {
+  return axios
+    .post(`/middleend/projects/${projectName}/composite-apps`, request.payload)
+    .then((res) => {
+      return res.data;
+    });
+};
+
 const createCompositeApp = ({ projectName, ...request }) => {
   return axios
     .post(`/v2/projects/${projectName}/composite-apps`, request.payload)
@@ -65,7 +72,7 @@ const updateCompositeApp = (request) => {
 const deleteCompositeApp = (request) => {
   return axios
     .delete(
-      `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}`
+      `/middleend/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}`
     )
     .then((res) => {
       return res.data;
@@ -302,15 +309,10 @@ const deleteInterface = (request) => {
 };
 
 //deployment intent group
-const createDeploymentIntentGroup = ({
-  projectName,
-  compositeAppName,
-  compositeAppVersion,
-  ...request
-}) => {
+const createDeploymentIntentGroup = (request) => {
   return axios
     .post(
-      `/v2/projects/${projectName}/composite-apps/${compositeAppName}/${compositeAppVersion}/deployment-intent-groups`,
+      `/middleend/projects/${request.spec.projectName}/composite-apps/${request.compositeApp}/${request.compositeAppVersion}/deployment-intent-groups`,
       { ...request }
     )
     .then((res) => {
@@ -329,9 +331,7 @@ const addIntentsToDeploymentIntentGroup = (request) => {
 };
 const getDeploymentIntentGroups = (request) => {
   return axios
-    .get(
-      `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups`
-    )
+    .get(`/middleend/projects/${request.projectName}/deployment-intent-groups`)
     .then((res) => {
       return res.data;
     });
@@ -349,7 +349,7 @@ const editDeploymentIntentGroup = (request) => {
 const deleteDeploymentIntentGroup = (request) => {
   return axios
     .delete(
-      `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups/${request.deploymentIntentGroupName}`
+      `/middleend/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups/${request.deploymentIntentGroupName}`
     )
     .then((res) => {
       return res.data;
@@ -421,7 +421,7 @@ const updateClusterProvider = (request) => {
 const addCluster = (request) => {
   return axios
     .post(
-      `/v2/cluster-providers/${request.get("providerName")}/clusters`,
+      `/middleend/clusterproviders/${request.get("providerName")}/clusters`,
       request
     )
     .then((res) => {
@@ -577,6 +577,7 @@ const vimService = {
   getCompositeApps,
   getProfiles,
   createCompositeApp,
+  addService,
   updateCompositeApp,
   deleteCompositeApp,
   getApps,
index b1d42ff..8e2b90e 100755 (executable)
@@ -1,2 +1,17 @@
+#=======================================================================
+# Copyright (c) 2017-2020 Aarna Networks, Inc.
+# All rights reserved.
+# ======================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#           http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================================================================
+
 # startup script for development. Give backend address if backend server is not running locally
-REACT_APP_BACKEND= npm start
+REACT_APP_BACKEND=http://emco npm start