summaryrefslogtreecommitdiffstats
path: root/src/tools
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/emcoui/Dockerfile20
-rw-r--r--src/tools/emcoui/README.md40
-rw-r--r--src/tools/emcoui/default.conf34
-rw-r--r--src/tools/emcoui/helm/emcoui/templates/configmap.yaml27
-rw-r--r--src/tools/emcoui/helm/emcoui/templates/deployment.yaml43
-rw-r--r--src/tools/emcoui/helm/emcoui/templates/service.yaml46
-rw-r--r--src/tools/emcoui/helm/emcoui/values.yaml52
-rw-r--r--src/tools/emcoui/middle_end/Dockerfile32
-rw-r--r--src/tools/emcoui/middle_end/Makefile24
-rw-r--r--src/tools/emcoui/middle_end/app/app.go1014
-rw-r--r--src/tools/emcoui/middle_end/app/compositeapp.go247
-rw-r--r--src/tools/emcoui/middle_end/app/digp.go322
-rw-r--r--src/tools/emcoui/middle_end/app/intents.go664
-rw-r--r--src/tools/emcoui/middle_end/app/profile.go261
-rw-r--r--src/tools/emcoui/middle_end/app/projects.go187
-rw-r--r--src/tools/emcoui/middle_end/authproxy/README.md16
-rw-r--r--src/tools/emcoui/middle_end/authproxy/authproxy.go281
-rw-r--r--src/tools/emcoui/middle_end/db/dbconnection.go145
-rw-r--r--src/tools/emcoui/middle_end/go.mod14
-rw-r--r--src/tools/emcoui/middle_end/go.sum428
-rw-r--r--src/tools/emcoui/middle_end/main/main.go112
-rw-r--r--src/tools/emcoui/public/robots.txt14
-rw-r--r--src/tools/emcoui/src/App.js4
-rw-r--r--src/tools/emcoui/src/admin/AdminNavigator.js12
-rw-r--r--src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx250
-rw-r--r--src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx97
-rw-r--r--src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx6
-rw-r--r--src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx785
-rw-r--r--src/tools/emcoui/src/admin/controllers/Controllers.jsx7
-rw-r--r--src/tools/emcoui/src/admin/projects/ProjectForm.jsx249
-rw-r--r--src/tools/emcoui/src/admin/projects/ProjectsTable.jsx218
-rw-r--r--src/tools/emcoui/src/appbase/AppBase.js27
-rw-r--r--src/tools/emcoui/src/appbase/Content.js2
-rw-r--r--src/tools/emcoui/src/appbase/Header.js16
-rw-r--r--src/tools/emcoui/src/appbase/Navigator.js64
-rw-r--r--src/tools/emcoui/src/assets/icons/empty.svg1
-rw-r--r--src/tools/emcoui/src/common/ExpandableCard.jsx94
-rw-r--r--src/tools/emcoui/src/common/FileUpload.jsx94
-rw-r--r--src/tools/emcoui/src/common/Form.jsx245
-rw-r--r--src/tools/emcoui/src/compositeApps/CompositeApp.jsx30
-rw-r--r--src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx143
-rw-r--r--src/tools/emcoui/src/compositeApps/CompositeApps.jsx88
-rw-r--r--src/tools/emcoui/src/compositeApps/apps/Apps.jsx11
-rw-r--r--src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx23
-rw-r--r--src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx7
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx149
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx129
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx83
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx524
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx441
-rw-r--r--src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx410
-rw-r--r--src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx55
-rw-r--r--src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx9
-rw-r--r--src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx307
-rw-r--r--src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx167
-rw-r--r--src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx174
-rw-r--r--src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx201
-rw-r--r--src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx265
-rw-r--r--src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx160
-rw-r--r--src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx118
-rw-r--r--src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx8
-rw-r--r--src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx419
-rw-r--r--src/tools/emcoui/src/services/apiService.js31
-rwxr-xr-xsrc/tools/emcoui/startup.sh17
64 files changed, 8310 insertions, 1853 deletions
diff --git a/src/tools/emcoui/Dockerfile b/src/tools/emcoui/Dockerfile
index 6f0cc2fa..8224d92c 100644
--- a/src/tools/emcoui/Dockerfile
+++ b/src/tools/emcoui/Dockerfile
@@ -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/
diff --git a/src/tools/emcoui/README.md b/src/tools/emcoui/README.md
index 05a43237..945dde0c 100644
--- a/src/tools/emcoui/README.md
+++ b/src/tools/emcoui/README.md
@@ -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
index 00000000..4387a613
--- /dev/null
+++ b/src/tools/emcoui/default.conf
@@ -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;
+ }
+}
diff --git a/src/tools/emcoui/helm/emcoui/templates/configmap.yaml b/src/tools/emcoui/helm/emcoui/templates/configmap.yaml
index a9ba34a5..b62f35dc 100644
--- a/src/tools/emcoui/helm/emcoui/templates/configmap.yaml
+++ b/src/tools/emcoui/helm/emcoui/templates/configmap.yaml
@@ -11,7 +11,27 @@
# 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;
}
diff --git a/src/tools/emcoui/helm/emcoui/templates/deployment.yaml b/src/tools/emcoui/helm/emcoui/templates/deployment.yaml
index 11ab6f52..f1609cd3 100644
--- a/src/tools/emcoui/helm/emcoui/templates/deployment.yaml
+++ b/src/tools/emcoui/helm/emcoui/templates/deployment.yaml
@@ -12,27 +12,58 @@
# 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
diff --git a/src/tools/emcoui/helm/emcoui/templates/service.yaml b/src/tools/emcoui/helm/emcoui/templates/service.yaml
index 2c09a7de..9c88b8f7 100644
--- a/src/tools/emcoui/helm/emcoui/templates/service.yaml
+++ b/src/tools/emcoui/helm/emcoui/templates/service.yaml
@@ -12,24 +12,48 @@
# 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
diff --git a/src/tools/emcoui/helm/emcoui/values.yaml b/src/tools/emcoui/helm/emcoui/values.yaml
index f52c0719..d764f722 100644
--- a/src/tools/emcoui/helm/emcoui/values.yaml
+++ b/src/tools/emcoui/helm/emcoui/values.yaml
@@ -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
index 00000000..4e35322c
--- /dev/null
+++ b/src/tools/emcoui/middle_end/Dockerfile
@@ -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
index 00000000..a44fc785
--- /dev/null
+++ b/src/tools/emcoui/middle_end/Makefile
@@ -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
index 00000000..a8511698
--- /dev/null
+++ b/src/tools/emcoui/middle_end/app/app.go
@@ -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
index 00000000..daae5b00
--- /dev/null
+++ b/src/tools/emcoui/middle_end/app/compositeapp.go
@@ -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
index 00000000..b4c83e15
--- /dev/null
+++ b/src/tools/emcoui/middle_end/app/digp.go
@@ -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
index 00000000..992f7b66
--- /dev/null
+++ b/src/tools/emcoui/middle_end/app/intents.go
@@ -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
index 00000000..fff43cdc
--- /dev/null
+++ b/src/tools/emcoui/middle_end/app/profile.go
@@ -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
index 00000000..39fe7573
--- /dev/null
+++ b/src/tools/emcoui/middle_end/app/projects.go
@@ -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
index 00000000..1d68a431
--- /dev/null
+++ b/src/tools/emcoui/middle_end/authproxy/README.md
@@ -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
index 00000000..78819ef6
--- /dev/null
+++ b/src/tools/emcoui/middle_end/authproxy/authproxy.go
@@ -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
index 00000000..5496c39c
--- /dev/null
+++ b/src/tools/emcoui/middle_end/db/dbconnection.go
@@ -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
index 00000000..3d195979
--- /dev/null
+++ b/src/tools/emcoui/middle_end/go.mod
@@ -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
index 00000000..d8e5a43f
--- /dev/null
+++ b/src/tools/emcoui/middle_end/go.sum
@@ -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
index 00000000..97a31017
--- /dev/null
+++ b/src/tools/emcoui/middle_end/main/main.go
@@ -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())
+}
diff --git a/src/tools/emcoui/public/robots.txt b/src/tools/emcoui/public/robots.txt
index e9e57dc4..f2ce78e7 100644
--- a/src/tools/emcoui/public/robots.txt
+++ b/src/tools/emcoui/public/robots.txt
@@ -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:
diff --git a/src/tools/emcoui/src/App.js b/src/tools/emcoui/src/App.js
index 2613ecfd..3a2c5ffc 100644
--- a/src/tools/emcoui/src/App.js
+++ b/src/tools/emcoui/src/App.js
@@ -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}`}
diff --git a/src/tools/emcoui/src/admin/AdminNavigator.js b/src/tools/emcoui/src/admin/AdminNavigator.js
index be07cba0..9cee73b1 100644
--- a/src/tools/emcoui/src/admin/AdminNavigator.js
+++ b/src/tools/emcoui/src/admin/AdminNavigator.js
@@ -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";
@@ -37,15 +37,15 @@ const categories = [
url: "/projects",
},
{
- id: "Clusters",
- icon: <DnsRoundedIcon />,
- url: "/clusters",
- },
- {
id: "Controllers",
icon: <SettingsIcon />,
url: "/controllers",
},
+ {
+ id: "Clusters",
+ icon: <DnsRoundedIcon />,
+ url: "/clusters",
+ },
],
},
];
diff --git a/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx b/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx
index 150a1912..57ee7557 100644
--- a/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx
+++ b/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx
@@ -11,147 +11,157 @@
// 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;
diff --git a/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx b/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx
index 20317695..192992bc 100644
--- a/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx
+++ b/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx
@@ -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 && (
diff --git a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx
index 6d9fc83b..6c49cb85 100644
--- a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx
+++ b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx
@@ -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}
>
diff --git a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx
index 1066d472..26bc1ca9 100644
--- a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx
+++ b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx
@@ -11,309 +11,528 @@
// 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;
diff --git a/src/tools/emcoui/src/admin/controllers/Controllers.jsx b/src/tools/emcoui/src/admin/controllers/Controllers.jsx
index 4a8a502c..4316f6ea 100644
--- a/src/tools/emcoui/src/admin/controllers/Controllers.jsx
+++ b/src/tools/emcoui/src/admin/controllers/Controllers.jsx
@@ -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) => {
diff --git a/src/tools/emcoui/src/admin/projects/ProjectForm.jsx b/src/tools/emcoui/src/admin/projects/ProjectForm.jsx
index 751de5d0..4ea87b2c 100644
--- a/src/tools/emcoui/src/admin/projects/ProjectForm.jsx
+++ b/src/tools/emcoui/src/admin/projects/ProjectForm.jsx
@@ -11,146 +11,157 @@
// 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;
diff --git a/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx b/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx
index d96d44fa..fb03155d 100644
--- a/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx
+++ b/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx
@@ -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>
+ );
}
diff --git a/src/tools/emcoui/src/appbase/AppBase.js b/src/tools/emcoui/src/appbase/AppBase.js
index 5dd3b53b..76dc4d8e 100644
--- a/src/tools/emcoui/src/appbase/AppBase.js
+++ b/src/tools/emcoui/src/appbase/AppBase.js
@@ -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>
diff --git a/src/tools/emcoui/src/appbase/Content.js b/src/tools/emcoui/src/appbase/Content.js
index 2c907acb..eff9a561 100644
--- a/src/tools/emcoui/src/appbase/Content.js
+++ b/src/tools/emcoui/src/appbase/Content.js
@@ -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>
diff --git a/src/tools/emcoui/src/appbase/Header.js b/src/tools/emcoui/src/appbase/Header.js
index 0222151f..19f148a4 100644
--- a/src/tools/emcoui/src/appbase/Header.js
+++ b/src/tools/emcoui/src/appbase/Header.js
@@ -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`) {
diff --git a/src/tools/emcoui/src/appbase/Navigator.js b/src/tools/emcoui/src/appbase/Navigator.js
index 2df2c009..e8f16367 100644
--- a/src/tools/emcoui/src/appbase/Navigator.js
+++ b/src/tools/emcoui/src/appbase/Navigator.js
@@ -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
index 00000000..f4e020ed
--- /dev/null
+++ b/src/tools/emcoui/src/assets/icons/empty.svg
@@ -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
index 00000000..1d2ea9e6
--- /dev/null
+++ b/src/tools/emcoui/src/common/ExpandableCard.jsx
@@ -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;
diff --git a/src/tools/emcoui/src/common/FileUpload.jsx b/src/tools/emcoui/src/common/FileUpload.jsx
index 847951e9..97d34bc2 100644
--- a/src/tools/emcoui/src/common/FileUpload.jsx
+++ b/src/tools/emcoui/src/common/FileUpload.jsx
@@ -11,58 +11,64 @@
// 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;
diff --git a/src/tools/emcoui/src/common/Form.jsx b/src/tools/emcoui/src/common/Form.jsx
index e9fe3a2d..6e8eee2e 100644
--- a/src/tools/emcoui/src/common/Form.jsx
+++ b/src/tools/emcoui/src/common/Form.jsx
@@ -11,144 +11,155 @@
// 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;
diff --git a/src/tools/emcoui/src/compositeApps/CompositeApp.jsx b/src/tools/emcoui/src/compositeApps/CompositeApp.jsx
index 34d07fbc..8b2c2b10 100644
--- a/src/tools/emcoui/src/compositeApps/CompositeApp.jsx
+++ b/src/tools/emcoui/src/compositeApps/CompositeApp.jsx
@@ -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));
diff --git a/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx b/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx
index 220d7df8..926ab545 100644
--- a/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx
+++ b/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx
@@ -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);
diff --git a/src/tools/emcoui/src/compositeApps/CompositeApps.jsx b/src/tools/emcoui/src/compositeApps/CompositeApps.jsx
index e7901ff9..5c540039 100644
--- a/src/tools/emcoui/src/compositeApps/CompositeApps.jsx
+++ b/src/tools/emcoui/src/compositeApps/CompositeApps.jsx
@@ -11,14 +11,15 @@
// 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>
+ )}
</>
)}
</>
diff --git a/src/tools/emcoui/src/compositeApps/apps/Apps.jsx b/src/tools/emcoui/src/compositeApps/apps/Apps.jsx
index 14be60c2..0e3638b3 100644
--- a/src/tools/emcoui/src/compositeApps/apps/Apps.jsx
+++ b/src/tools/emcoui/src/compositeApps/apps/Apps.jsx
@@ -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>
diff --git a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx
index 58161e8e..d565eba7 100644
--- a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx
+++ b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx
@@ -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>
diff --git a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx
index 25ecaaee..26629e03 100644
--- a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx
+++ b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx
@@ -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
index 00000000..12dd7dd8
--- /dev/null
+++ b/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx
@@ -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
index 00000000..e2272ae8
--- /dev/null
+++ b/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx
@@ -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
index 00000000..c52c2b42
--- /dev/null
+++ b/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx
@@ -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
index 00000000..055dc3e6
--- /dev/null
+++ b/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx
@@ -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;
diff --git a/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx
index 29e17cd7..751ea8eb 100644
--- a/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx
+++ b/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx
@@ -11,185 +11,298 @@
// 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
index 00000000..f1a6ac2d
--- /dev/null
+++ b/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx
@@ -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;
diff --git a/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx b/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx
index 6dfb8279..09f70c99 100644
--- a/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx
+++ b/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx
@@ -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"
diff --git a/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx b/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx
index bb43972c..9946c92d 100644
--- a/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx
+++ b/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx
@@ -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>
diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx
index f0cf1e1d..ee1fec74 100644
--- a/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx
+++ b/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx
@@ -11,245 +11,112 @@
// 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;
diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx
index 5710b52b..3e22dc4c 100644
--- a/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx
+++ b/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx
@@ -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)}
diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx
index c1f73bb7..132b9fc3 100644
--- a/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx
+++ b/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx
@@ -11,14 +11,15 @@
// 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
index 00000000..e0662455
--- /dev/null
+++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx
@@ -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
index 00000000..5b5c4191
--- /dev/null
+++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx
@@ -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
index 00000000..580044ac
--- /dev/null
+++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx
@@ -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
index 00000000..746f49e3
--- /dev/null
+++ b/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx
@@ -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>
+ );
+}
diff --git a/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx b/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx
index b641737b..ed93bd0f 100644
--- a/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx
+++ b/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx
@@ -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>
diff --git a/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx b/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx
index 5e196ed9..540de8a0 100644
--- a/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx
+++ b/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx
@@ -11,207 +11,268 @@
// 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;
diff --git a/src/tools/emcoui/src/services/apiService.js b/src/tools/emcoui/src/services/apiService.js
index 4bff9305..0c830768 100644
--- a/src/tools/emcoui/src/services/apiService.js
+++ b/src/tools/emcoui/src/services/apiService.js
@@ -11,10 +11,9 @@
// 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,
diff --git a/src/tools/emcoui/startup.sh b/src/tools/emcoui/startup.sh
index b1d42ffc..8e2b90e0 100755
--- a/src/tools/emcoui/startup.sh
+++ b/src/tools/emcoui/startup.sh
@@ -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