diff --git a/CHANGELOG.md b/CHANGELOG.md index f4102069d..dee562830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#262](https://github.com/kobsio/kobs/pull/262): [core] Rework variables handling in dashboards. - [#263](https://github.com/kobsio/kobs/pull/263): [core] :warning: _Breaking change:_ :warning: Refactor `cluster` and `clusters` package. - [#265](https://github.com/kobsio/kobs/pull/265): [applications] Improve tags support by allow users to filter applications by tags and showing tags on application page. +- [#269](https://github.com/kobsio/kobs/pull/269): [applications] :warning: _Breaking change:_ :warning: Improve topology graph, by allowing custom styles for applications. ## [v0.7.0](https://github.com/kobsio/kobs/releases/tag/v0.7.0) (2021-11-19) diff --git a/deploy/demo/bookinfo/productpage-application.yaml b/deploy/demo/bookinfo/productpage-application.yaml index 8d695cec5..3c490d0d3 100644 --- a/deploy/demo/bookinfo/productpage-application.yaml +++ b/deploy/demo/bookinfo/productpage-application.yaml @@ -20,11 +20,12 @@ spec: namespace: kobs - name: team-call-of-duty namespace: kobs - dependencies: - - name: details - description: Get book information. - - name: reviews - description: Get book reviews. + topology: + dependencies: + - name: details + description: Get book information. + - name: reviews + description: Get book reviews. preview: title: Incoming Success Rate plugin: diff --git a/deploy/demo/bookinfo/reviews-application.yaml b/deploy/demo/bookinfo/reviews-application.yaml index 1d5f0443b..0b59d0240 100644 --- a/deploy/demo/bookinfo/reviews-application.yaml +++ b/deploy/demo/bookinfo/reviews-application.yaml @@ -16,9 +16,10 @@ spec: teams: - name: team-resident-evil namespace: kobs - dependencies: - - name: ratings - description: Get book ranking information. + topology: + dependencies: + - name: ratings + description: Get book ranking information. preview: title: Incoming Success Rate plugin: diff --git a/deploy/demo/elastic-system/elasticsearch-application.yaml b/deploy/demo/elastic-system/elasticsearch-application.yaml index ca0fa5e0e..2307f7584 100644 --- a/deploy/demo/elastic-system/elasticsearch-application.yaml +++ b/deploy/demo/elastic-system/elasticsearch-application.yaml @@ -12,13 +12,14 @@ spec: link: https://github.com/elastic/elasticsearch - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/elastic-system/elasticsearch-application.yaml - dependencies: - - name: elastic-operator - namespace: elastic-system - description: The Elasticsearch Cluster is managed by the Elastic Operator - - name: filebeat - namespace: elastic-system - description: Filebeat is responsible for sending the Logs of all Containers to Elasticsearch + topology: + dependencies: + - name: elastic-operator + namespace: elastic-system + description: The Elasticsearch Cluster is managed by the Elastic Operator + - name: filebeat + namespace: elastic-system + description: Filebeat is responsible for sending the Logs of all Containers to Elasticsearch preview: title: All Logs plugin: diff --git a/deploy/demo/elastic-system/filebeat-application.yaml b/deploy/demo/elastic-system/filebeat-application.yaml index 8151884c6..479ce8a2d 100644 --- a/deploy/demo/elastic-system/filebeat-application.yaml +++ b/deploy/demo/elastic-system/filebeat-application.yaml @@ -12,10 +12,11 @@ spec: link: https://github.com/elastic/beats - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/elastic-system/filebeat-application.yaml - dependencies: - - name: elastic-operator - namespace: elastic-system - description: Filebeat is managed by the Elastic Operator + topology: + dependencies: + - name: elastic-operator + namespace: elastic-system + description: Filebeat is managed by the Elastic Operator preview: title: All Logs plugin: diff --git a/deploy/demo/kobs/base/kobs-application.yaml b/deploy/demo/kobs/base/kobs-application.yaml index f0749c3ee..3c53237f1 100644 --- a/deploy/demo/kobs/base/kobs-application.yaml +++ b/deploy/demo/kobs/base/kobs-application.yaml @@ -13,16 +13,17 @@ spec: link: https://github.com/kobsio/kobs - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/kobs/kobs-application.yaml - dependencies: - - name: elasticsearch - namespace: elastic-system - description: Elasticsearch is used to get logs for the services. - - name: jaeger - namespace: istio-system - description: Jaeger is used to get traces for the services. - - name: prometheus - namespace: istio-system - description: Elasticsearch is used to get metrics for the services. + topology: + dependencies: + - name: elasticsearch + namespace: elastic-system + description: Elasticsearch is used to get logs for the services. + - name: jaeger + namespace: istio-system + description: Jaeger is used to get traces for the services. + - name: prometheus + namespace: istio-system + description: Elasticsearch is used to get metrics for the services. preview: title: All Logs plugin: diff --git a/deploy/docker/kobs/config.yaml b/deploy/docker/kobs/config.yaml index d5bfb9b93..7641acb01 100644 --- a/deploy/docker/kobs/config.yaml +++ b/deploy/docker/kobs/config.yaml @@ -10,8 +10,10 @@ plugins: - secrets applications: - topologyCacheDuration: 1m - teamsCacheDuration: 1m + cache: + topologyDuration: 1m + teamsDuration: 1m + tagsDuration: 1m prometheus: - name: prometheus diff --git a/deploy/helm/kobs/Chart.yaml b/deploy/helm/kobs/Chart.yaml index 5531a8903..dde4f9ecd 100644 --- a/deploy/helm/kobs/Chart.yaml +++ b/deploy/helm/kobs/Chart.yaml @@ -4,5 +4,5 @@ description: Kubernetes Observability Platform type: application home: https://kobs.io icon: https://kobs.io/assets/images/logo.svg -version: 0.8.4 +version: 0.9.0 appVersion: v0.7.0 diff --git a/deploy/helm/kobs/crds/kobs.io_applications.yaml b/deploy/helm/kobs/crds/kobs.io_applications.yaml index 1eaa3ee4f..1e95e1a88 100644 --- a/deploy/helm/kobs/crds/kobs.io_applications.yaml +++ b/deploy/helm/kobs/crds/kobs.io_applications.yaml @@ -123,21 +123,6 @@ spec: - title type: object type: array - dependencies: - items: - properties: - cluster: - type: string - description: - type: string - name: - type: string - namespace: - type: string - required: - - name - type: object - type: array description: type: string links: @@ -192,6 +177,26 @@ spec: - name type: object type: array + topology: + properties: + dependencies: + items: + properties: + cluster: + type: string + description: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array + type: + type: string + type: object type: object type: object served: true diff --git a/deploy/kustomize/crds/kobs.io_applications.yaml b/deploy/kustomize/crds/kobs.io_applications.yaml index 1eaa3ee4f..1e95e1a88 100644 --- a/deploy/kustomize/crds/kobs.io_applications.yaml +++ b/deploy/kustomize/crds/kobs.io_applications.yaml @@ -123,21 +123,6 @@ spec: - title type: object type: array - dependencies: - items: - properties: - cluster: - type: string - description: - type: string - name: - type: string - namespace: - type: string - required: - - name - type: object - type: array description: type: string links: @@ -192,6 +177,26 @@ spec: - name type: object type: array + topology: + properties: + dependencies: + items: + properties: + cluster: + type: string + description: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array + type: + type: string + type: object type: object type: object served: true diff --git a/docs/plugins/applications.md b/docs/plugins/applications.md index 9b9d3c635..df976dde1 100644 --- a/docs/plugins/applications.md +++ b/docs/plugins/applications.md @@ -4,20 +4,42 @@ The applications plugin allows you to show a list of application on a dashboard. ## Configuration -The following configuration can be used to configure the cache duration for applications. +The following configuration can be used to configure the cache duration for applications and to customize the topology graph. ```yaml plugins: applications: - topologyCacheDuration: 5m - teamsCacheDuration: 5m + cache: + topologyDuration: 5m + teamsDuration: 5m + tagsDuration: 5m + topology: + - type: custom + shape: round-diamond + color: "#c9190b" + ``` | Field | Type | Description | Required | | ----- | ---- | ----------- | -------- | -| topologyCacheDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the topology graph should be cached. The default value is `1h`. | No | -| teamsCacheDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the teams for an application should be cached. The default value is `1h`. | No | -| tagsCacheDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the tags for all applications should be cached. The default value is `1h`. | No | +| cache | [Cache](#cache) | Customize the caching behaviour for applications. | No | +| topology | [[]Topology](#topology) | Add custom node types for the topology graph, which can then selected in the Applications CRs via the `topology.type` option. | No | + +### Cache + +| Field | Type | Description | Required | +| ----- | ---- | ----------- | -------- | +| topologyDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the topology graph should be cached. The default value is `1h`. | No | +| teamsDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the teams for an application should be cached. The default value is `1h`. | No | +| tagsDuration | [duration](https://pkg.go.dev/time#ParseDuration) | The duration for how long the tags for all applications should be cached. The default value is `1h`. | No | + +### Topology + +| Field | Type | Description | Required | +| ----- | ---- | ----------- | -------- | +| type | string | The name of the node type. The node type can be selected via the `topology.type` field in the Application CRs. | No | +| color | string | A color for the node in the topology chart. Every CSS color code is allowed, but we recommend to use an color from [Patternfly](https://www.patternfly.org/v4/guidelines/colors), so the topology graph matches the overall style of kobs. The default value is `#0066cc`. | No | +| shape | string | The shape of the node in the topology chart. Allowed values are `rectangle`, `roundrectangle`, `ellipse`, `triangle`, `pentagon`, `hexagon`, `heptagon`, `octagon`, `star`, `barrel`, `diamond`, `vee`, `rhomboid`, `polygon`, `tag`, `round-rectangle`, `round-triangle`, `round-diamond`, `round-pentagon`, `round-hexagon`, `round-heptagon`, `round-octagon`, `round-tag`, `cut-rectangle`, `bottom-round-rectangle` and `concave-hexagon`. The default value is `roundrectangle`. | No | ## Options diff --git a/docs/resources/applications.md b/docs/resources/applications.md index 415f2a89c..a14b7c2c2 100644 --- a/docs/resources/applications.md +++ b/docs/resources/applications.md @@ -22,7 +22,7 @@ In the following you can found the specification for the Application CRD. On the | tags | []string | A list of tags to describe the application. | No | | links | [[]Link](#link) | A list of links (e.g. a link to the GitHub repository for this application). | No | | teams | [[]Team](#team) | A list of teams to define the ownership for the application. | No | -| dependencies | [[]Dependency](#dependency) | Add other applications as dependencies for this application. This can be used to render a topology graph for your applications. | No | +| topology | [Topology](#topology) | Set the topology settings for your application like the type in the topology graph and dependencies to other applications. | No | | preview | [Preview](#preview) | Show the most important metrics for your application in the gallery view. | No | | dashboards | [[]Dashboard](#dashboard) | A list of dashboards, which should be shown for this application. | No | @@ -44,6 +44,13 @@ Teams can be used to define the ownership for an application. It is also possibl | name | string | Name of the team. | Yes | | description | string | The description can be used to explain, why this team is the owner of the application. | No | +### Topology + +| Field | Type | Description | Required | +| ----- | ---- | ----------- | -------- | +| type | string | The type of the application in the topology graph. This must be a node type as specified in the `topology` key in the [configuration](../plugins/applications#configuration). The default value is `application`. | No | +| dependencies | [[]Dependency](#dependency) | Add other applications as dependencies for this application. This can be used to render a topology graph for your applications. | No | + ### Dependency Dependencies can be used to render a topology graph for all your applications. For that your have to add other applications as dependencies to the Application CR. @@ -110,9 +117,10 @@ spec: teams: - name: squad-resident-evil namespace: kobs - dependencies: - - name: ratings - description: Get book ranking information. + topology: + dependencies: + - name: ratings + description: Get book ranking information. preview: title: Incoming Success Rate plugin: diff --git a/pkg/api/apis/application/v1beta1/types.go b/pkg/api/apis/application/v1beta1/types.go index 14259baaa..e50942587 100644 --- a/pkg/api/apis/application/v1beta1/types.go +++ b/pkg/api/apis/application/v1beta1/types.go @@ -28,16 +28,16 @@ type ApplicationList struct { } type ApplicationSpec struct { - Cluster string `json:"cluster,omitempty"` - Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Tags []string `json:"tags,omitempty"` - Links []Link `json:"links,omitempty"` - Teams []Reference `json:"teams,omitempty"` - Dependencies []Reference `json:"dependencies,omitempty"` - Preview *Preview `json:"preview,omitempty"` - Dashboards []dashboard.Reference `json:"dashboards,omitempty"` + Cluster string `json:"cluster,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + Links []Link `json:"links,omitempty"` + Teams []Reference `json:"teams,omitempty"` + Topology Topology `json:"topology,omitempty"` + Preview *Preview `json:"preview,omitempty"` + Dashboards []dashboard.Reference `json:"dashboards,omitempty"` } type Link struct { @@ -45,6 +45,11 @@ type Link struct { Link string `json:"link"` } +type Topology struct { + Type string `json:"type,omitempty"` + Dependencies []Reference `json:"dependencies,omitempty"` +} + type Reference struct { Cluster string `json:"cluster,omitempty"` Namespace string `json:"namespace,omitempty"` diff --git a/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go b/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go index d66021550..64d9d3372 100644 --- a/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go +++ b/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go @@ -104,11 +104,7 @@ func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { *out = make([]Reference, len(*in)) copy(*out, *in) } - if in.Dependencies != nil { - in, out := &in.Dependencies, &out.Dependencies - *out = make([]Reference, len(*in)) - copy(*out, *in) - } + in.Topology.DeepCopyInto(&out.Topology) if in.Preview != nil { in, out := &in.Preview, &out.Preview *out = new(Preview) @@ -182,3 +178,24 @@ func (in *Reference) DeepCopy() *Reference { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Topology) DeepCopyInto(out *Topology) { + *out = *in + if in.Dependencies != nil { + in, out := &in.Dependencies, &out.Dependencies + *out = make([]Reference, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Topology. +func (in *Topology) DeepCopy() *Topology { + if in == nil { + return nil + } + out := new(Topology) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/clusters/cluster/cluster.go b/pkg/api/clusters/cluster/cluster.go index 57a48ff4c..60292958a 100644 --- a/pkg/api/clusters/cluster/cluster.go +++ b/pkg/api/clusters/cluster/cluster.go @@ -391,6 +391,10 @@ func (c *client) GetApplications(ctx context.Context, namespace string) ([]appli application.Namespace = applicationItem.Namespace application.Name = applicationItem.Name + if application.Topology.Type == "" { + application.Topology.Type = "application" + } + applications = append(applications, application) } @@ -411,6 +415,10 @@ func (c *client) GetApplication(ctx context.Context, namespace, name string) (*a application.Namespace = namespace application.Name = name + if application.Topology.Type == "" { + application.Topology.Type = "application" + } + return &application, nil } diff --git a/pkg/api/clusters/cluster/cluster_test.go b/pkg/api/clusters/cluster/cluster_test.go index 732573eab..887e44e01 100644 --- a/pkg/api/clusters/cluster/cluster_test.go +++ b/pkg/api/clusters/cluster/cluster_test.go @@ -93,6 +93,11 @@ func TestGetApplications(t *testing.T) { Name: "application1", Namespace: "default", }, + Spec: application.ApplicationSpec{ + Topology: application.Topology{ + Type: "service", + }, + }, }, &application.Application{ ObjectMeta: metav1.ObjectMeta{ Name: "application2", @@ -117,7 +122,7 @@ func TestGetApplications(t *testing.T) { client := getClient() applications, err := client.GetApplications(context.Background(), "default") require.NoError(t, err) - require.Equal(t, []application.ApplicationSpec{{Cluster: "test", Namespace: "default", Name: "application1"}, {Cluster: "test", Namespace: "default", Name: "application2"}}, applications) + require.Equal(t, []application.ApplicationSpec{{Cluster: "test", Namespace: "default", Name: "application1", Topology: application.Topology{Type: "service"}}, {Cluster: "test", Namespace: "default", Name: "application2", Topology: application.Topology{Type: "application"}}}, applications) }) } @@ -153,7 +158,7 @@ func TestGetApplication(t *testing.T) { client := getClient() applications, err := client.GetApplication(context.Background(), "default", "application1") require.NoError(t, err) - require.Equal(t, &application.ApplicationSpec{Cluster: "test", Namespace: "default", Name: "application1"}, applications) + require.Equal(t, &application.ApplicationSpec{Cluster: "test", Namespace: "default", Name: "application1", Topology: application.Topology{Type: "application"}}, applications) }) } diff --git a/plugins/applications/applications.go b/plugins/applications/applications.go index b5837841f..cc14a60fa 100644 --- a/plugins/applications/applications.go +++ b/plugins/applications/applications.go @@ -24,9 +24,15 @@ const Route = "/applications" // Config is the structure of the configuration for the applications plugin. type Config struct { - TopologyCacheDuration string `json:"topologyCacheDuration"` - TeamsCacheDuration string `json:"teamsCacheDuration"` - TagsCacheDuration string `json:"tagsCacheDuration"` + Cache CacheConfig `json:"cache"` + Topology []topology.Config `json:"topology"` +} + +// CacheConfig is the structure of the cache configuration for the topology graph, teams and tags. +type CacheConfig struct { + TopologyDuration string `json:"topologyDuration"` + TeamsDuration string `json:"teamsDuration"` + TagsDuration string `json:"tagsDuration"` } // Router implements the router for the resources plugin, which can be registered in the router for our rest api. @@ -152,7 +158,7 @@ func (router *Router) getApplications(w http.ResponseWriter, r *http.Request) { // graph and generating a new one in the background. if view == "topology" { if router.topology.LastFetch.After(time.Now().Add(-1 * router.topology.CacheDuration)) { - topo := topology.Generate(router.topology.Topology, clusterNames, namespaces) + topo := topology.Generate(router.topology.Topology, clusterNames, namespaces, tagsList) log.Debug(r.Context(), "Get applications result.", zap.String("topology", "return cached topology"), zap.Int("edges", len(topo.Edges)), zap.Int("nodes", len(topo.Nodes))) render.JSON(w, r, topo) return @@ -164,7 +170,7 @@ func (router *Router) getApplications(w http.ResponseWriter, r *http.Request) { router.topology.LastFetch = time.Now() router.topology.Topology = topo - topo = topology.Generate(topo, clusterNames, namespaces) + topo = topology.Generate(topo, clusterNames, namespaces, tagsList) log.Debug(r.Context(), "Get applications result.", zap.String("topology", "get and return topology"), zap.Int("edges", len(topo.Edges)), zap.Int("nodes", len(topo.Nodes))) render.JSON(w, r, topo) return @@ -184,7 +190,7 @@ func (router *Router) getApplications(w http.ResponseWriter, r *http.Request) { } }() - topo := topology.Generate(router.topology.Topology, clusterNames, namespaces) + topo := topology.Generate(router.topology.Topology, clusterNames, namespaces, tagsList) log.Debug(r.Context(), "Get applications result.", zap.String("topology", "return topology"), zap.Int("edges", len(topo.Edges)), zap.Int("nodes", len(topo.Nodes))) render.JSON(w, r, topo) return @@ -259,16 +265,21 @@ func (router *Router) getTags(w http.ResponseWriter, r *http.Request) { // Register returns a new router which can be used in the router for the kobs rest api. func Register(clustersClient clusters.Client, plugins *plugin.Plugins, config Config) chi.Router { + var options map[string]interface{} + options = make(map[string]interface{}) + options["topology"] = config.Topology + plugins.Append(plugin.Plugin{ Name: "applications", DisplayName: "Applications", Description: "Monitor your Kubernetes workloads.", Home: true, Type: "applications", + Options: options, }) var topology topology.Cache - topologyCacheDuration, err := time.ParseDuration(config.TopologyCacheDuration) + topologyCacheDuration, err := time.ParseDuration(config.Cache.TopologyDuration) if err != nil || topologyCacheDuration.Seconds() < 60 { topology.CacheDuration = time.Duration(1 * time.Hour) } else { @@ -276,7 +287,7 @@ func Register(clustersClient clusters.Client, plugins *plugin.Plugins, config Co } var teams teams.Cache - teamsCacheDuration, err := time.ParseDuration(config.TeamsCacheDuration) + teamsCacheDuration, err := time.ParseDuration(config.Cache.TeamsDuration) if err != nil || teamsCacheDuration.Seconds() < 60 { teams.CacheDuration = time.Duration(1 * time.Hour) } else { @@ -284,7 +295,7 @@ func Register(clustersClient clusters.Client, plugins *plugin.Plugins, config Co } var tags tags.Cache - tagsCacheDuration, err := time.ParseDuration(config.TagsCacheDuration) + tagsCacheDuration, err := time.ParseDuration(config.Cache.TagsDuration) if err != nil || tagsCacheDuration.Seconds() < 60 { tags.CacheDuration = time.Duration(1 * time.Hour) } else { diff --git a/plugins/applications/applications_test.go b/plugins/applications/applications_test.go index a61f4fd39..8f487008c 100644 --- a/plugins/applications/applications_test.go +++ b/plugins/applications/applications_test.go @@ -56,7 +56,7 @@ func TestGetApplications(t *testing.T) { name: "gallery return teams with cached teams nil", url: "/applications?view=gallery&teamCluster=cluster1&teamNamespace=namespace1&teamName=team1", expectedStatusCode: http.StatusOK, - expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}]}]\n", + expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}],\"topology\":{}}]\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetTeams", mock.Anything, "").Return([]team.TeamSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}, nil) mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}, nil) @@ -77,7 +77,7 @@ func TestGetApplications(t *testing.T) { teamsCache: teams.Cache{Teams: []teams.Team{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1", Applications: []application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}}}}, url: "/applications?view=gallery&teamCluster=cluster1&teamNamespace=namespace1&teamName=team1", expectedStatusCode: http.StatusOK, - expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}]}]\n", + expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}],\"topology\":{}}]\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetTeams", mock.Anything, "").Return([]team.TeamSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}, nil) mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}, nil) @@ -106,7 +106,7 @@ func TestGetApplications(t *testing.T) { name: "gallery namespaces nil", url: "/applications?view=gallery&cluster=cluster1", expectedStatusCode: http.StatusOK, - expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}]}]\n", + expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}],\"topology\":{}}]\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}, nil) }, @@ -124,7 +124,7 @@ func TestGetApplications(t *testing.T) { name: "gallery", url: "/applications?view=gallery&cluster=cluster1&namespace=namespace1", expectedStatusCode: http.StatusOK, - expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}]}]\n", + expectedBody: "[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"teams\":[{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"team1\"}],\"topology\":{}}]\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetApplications", mock.Anything, "namespace1").Return([]application.ApplicationSpec{{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Teams: []application.Reference{{Cluster: "cluster1", Namespace: "namespace1", Name: "team1"}}}}, nil) }, @@ -144,12 +144,12 @@ func TestGetApplications(t *testing.T) { topologyCache: topology.Cache{Topology: &topology.Topology{Edges: nil, Nodes: nil}}, url: "/applications?view=topology&cluster=cluster1&namespace=namespace2", expectedStatusCode: http.StatusOK, - expectedBody: "{\"edges\":[{\"data\":{\"id\":\"cluster1-namespace1-application2-cluster1-namespace2-application3\",\"source\":\"cluster1-namespace1-application2\",\"target\":\"cluster1-namespace2-application3\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application4\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application4\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application5\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application5\",\"description\":\"\"}}],\"nodes\":[{\"data\":{\"id\":\"cluster1-namespace1-application2\",\"type\":\"application\",\"label\":\"application2\",\"parent\":\"cluster1-namespace1\",\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application2\",\"dependencies\":[{\"namespace\":\"namespace2\",\"name\":\"application3\"}]}},{\"data\":{\"id\":\"cluster1-namespace2-application3\",\"type\":\"application\",\"label\":\"application3\",\"parent\":\"cluster1-namespace2\",\"cluster\":\"cluster1\",\"namespace\":\"namespace2\",\"name\":\"application3\",\"dependencies\":[{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"},{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}]}},{\"data\":{\"id\":\"cluster2-namespace3-application4\",\"type\":\"application\",\"label\":\"application4\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"}},{\"data\":{\"id\":\"cluster2-namespace3-application5\",\"type\":\"application\",\"label\":\"application5\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}},{\"data\":{\"id\":\"cluster1\",\"type\":\"cluster\",\"label\":\"cluster1\",\"parent\":\"\"}},{\"data\":{\"id\":\"cluster2\",\"type\":\"cluster\",\"label\":\"cluster2\",\"parent\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace1\",\"type\":\"namespace\",\"label\":\"namespace1\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\"}}]}\n", + expectedBody: "{\"edges\":[{\"data\":{\"id\":\"cluster1-namespace1-application2-cluster1-namespace2-application3\",\"source\":\"cluster1-namespace1-application2\",\"target\":\"cluster1-namespace2-application3\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application4\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application4\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application5\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application5\",\"description\":\"\"}}],\"nodes\":[{\"data\":{\"id\":\"cluster1-namespace1-application2\",\"type\":\"-not-selected\",\"label\":\"application2\",\"parent\":\"cluster1-namespace1\",\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application2\",\"topology\":{\"dependencies\":[{\"namespace\":\"namespace2\",\"name\":\"application3\"}]}}},{\"data\":{\"id\":\"cluster1-namespace2-application3\",\"type\":\"\",\"label\":\"application3\",\"parent\":\"cluster1-namespace2\",\"cluster\":\"cluster1\",\"namespace\":\"namespace2\",\"name\":\"application3\",\"topology\":{\"dependencies\":[{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"},{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}]}}},{\"data\":{\"id\":\"cluster2-namespace3-application4\",\"type\":\"-not-selected\",\"label\":\"application4\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3-application5\",\"type\":\"-not-selected\",\"label\":\"application5\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1\",\"type\":\"cluster\",\"label\":\"cluster1\",\"parent\":\"\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2\",\"type\":\"cluster\",\"label\":\"cluster2\",\"parent\":\"\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace1\",\"type\":\"namespace\",\"label\":\"namespace1\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\",\"topology\":{}}}]}\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{ - {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}, - {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}, - {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}, + {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}, }, nil) @@ -175,21 +175,21 @@ func TestGetApplications(t *testing.T) { {Data: topology.EdgeData{ID: "cluster1-namespace2-application3-cluster2-namespace3-application5", Source: "cluster1-namespace2-application3", SourceCluster: "cluster1", SourceNamespace: "namespace2", SourceName: "application3", Target: "cluster2-namespace3-application5", TargetCluster: "cluster2", TargetNamespace: "namespace3", TargetName: "application5"}}, }, Nodes: []topology.Node{ - {Data: topology.NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}, - {Data: topology.NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}, - {Data: topology.NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}, + {Data: topology.NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}}, + {Data: topology.NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}}, + {Data: topology.NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}}, {Data: topology.NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}}}, {Data: topology.NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, }, }}, url: "/applications?view=topology&cluster=cluster1&namespace=namespace2", expectedStatusCode: http.StatusOK, - expectedBody: "{\"edges\":[{\"data\":{\"id\":\"cluster1-namespace1-application2-cluster1-namespace2-application3\",\"source\":\"cluster1-namespace1-application2\",\"target\":\"cluster1-namespace2-application3\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application4\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application4\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application5\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application5\",\"description\":\"\"}}],\"nodes\":[{\"data\":{\"id\":\"cluster1-namespace1-application2\",\"type\":\"application\",\"label\":\"application2\",\"parent\":\"cluster1-namespace1\",\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application2\",\"dependencies\":[{\"namespace\":\"namespace2\",\"name\":\"application3\"}]}},{\"data\":{\"id\":\"cluster1-namespace2-application3\",\"type\":\"application\",\"label\":\"application3\",\"parent\":\"cluster1-namespace2\",\"cluster\":\"cluster1\",\"namespace\":\"namespace2\",\"name\":\"application3\",\"dependencies\":[{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"},{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}]}},{\"data\":{\"id\":\"cluster2-namespace3-application4\",\"type\":\"application\",\"label\":\"application4\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"}},{\"data\":{\"id\":\"cluster2-namespace3-application5\",\"type\":\"application\",\"label\":\"application5\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}},{\"data\":{\"id\":\"cluster1\",\"type\":\"cluster\",\"label\":\"cluster1\",\"parent\":\"\"}},{\"data\":{\"id\":\"cluster2\",\"type\":\"cluster\",\"label\":\"cluster2\",\"parent\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace1\",\"type\":\"namespace\",\"label\":\"namespace1\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\"}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\"}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\"}}]}\n", + expectedBody: "{\"edges\":[{\"data\":{\"id\":\"cluster1-namespace1-application2-cluster1-namespace2-application3\",\"source\":\"cluster1-namespace1-application2\",\"target\":\"cluster1-namespace2-application3\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application4\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application4\",\"description\":\"\"}},{\"data\":{\"id\":\"cluster1-namespace2-application3-cluster2-namespace3-application5\",\"source\":\"cluster1-namespace2-application3\",\"target\":\"cluster2-namespace3-application5\",\"description\":\"\"}}],\"nodes\":[{\"data\":{\"id\":\"cluster1-namespace1-application2\",\"type\":\"application-not-selected\",\"label\":\"application2\",\"parent\":\"cluster1-namespace1\",\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application2\",\"topology\":{\"dependencies\":[{\"namespace\":\"namespace2\",\"name\":\"application3\"}]}}},{\"data\":{\"id\":\"cluster1-namespace2-application3\",\"type\":\"application\",\"label\":\"application3\",\"parent\":\"cluster1-namespace2\",\"cluster\":\"cluster1\",\"namespace\":\"namespace2\",\"name\":\"application3\",\"topology\":{\"dependencies\":[{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\"},{\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\"}]}}},{\"data\":{\"id\":\"cluster2-namespace3-application4\",\"type\":\"application-not-selected\",\"label\":\"application4\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application4\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3-application5\",\"type\":\"application-not-selected\",\"label\":\"application5\",\"parent\":\"cluster2-namespace3\",\"cluster\":\"cluster2\",\"namespace\":\"namespace3\",\"name\":\"application5\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1\",\"type\":\"cluster\",\"label\":\"cluster1\",\"parent\":\"\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2\",\"type\":\"cluster\",\"label\":\"cluster2\",\"parent\":\"\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace1\",\"type\":\"namespace\",\"label\":\"namespace1\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\",\"topology\":{}}},{\"data\":{\"id\":\"cluster1-namespace2\",\"type\":\"namespace\",\"label\":\"namespace2\",\"parent\":\"cluster1\",\"topology\":{}}},{\"data\":{\"id\":\"cluster2-namespace3\",\"type\":\"namespace\",\"label\":\"namespace3\",\"parent\":\"cluster2\",\"topology\":{}}}]}\n", prepare: func(mockClusterClient *cluster.MockClient) { mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{ - {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}, - {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}, - {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}, + {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}, }, nil) @@ -245,7 +245,7 @@ func TestGetApplication(t *testing.T) { name: "return application", url: "/application?cluster=cluster1&namespace=namespace1&name=application1", expectedStatusCode: http.StatusOK, - expectedBody: "{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\"}\n", + expectedBody: "{\"cluster\":\"cluster1\",\"namespace\":\"namespace1\",\"name\":\"application1\",\"topology\":{}}\n", }, } { t.Run(tt.name, func(t *testing.T) { @@ -349,6 +349,9 @@ func TestRegister(t *testing.T) { Description: "Monitor your Kubernetes workloads.", Home: true, Type: "applications", + Options: map[string]interface{}{ + "topology": []topology.Config(nil), + }, }, } @@ -361,7 +364,7 @@ func TestRegister(t *testing.T) { t.Run("config with durations", func(t *testing.T) { plugins := &plugin.Plugins{} - router := Register(nil, plugins, Config{TopologyCacheDuration: "1m", TeamsCacheDuration: "1m", TagsCacheDuration: "1m"}) + router := Register(nil, plugins, Config{Cache: CacheConfig{TopologyDuration: "1m", TeamsDuration: "1m", TagsDuration: "1m"}}) require.NotEmpty(t, router) require.Equal(t, expectedPlugins, plugins) }) diff --git a/plugins/applications/pkg/topology/topology.go b/plugins/applications/pkg/topology/topology.go index fb12b2949..4832030f1 100644 --- a/plugins/applications/pkg/topology/topology.go +++ b/plugins/applications/pkg/topology/topology.go @@ -8,6 +8,15 @@ import ( "github.com/kobsio/kobs/pkg/api/clusters" ) +// Config is the structure of the topology graph configuration. It contains a type which can then be used in the +// "topology.type" property in an application. It also contains a user color and shape which defines how the application +// is rendered in the topology graph. +type Config struct { + Type string `json:"type"` + Shape string `json:"shape"` + Color string `json:"color"` +} + // Cache is the structure which can be used to cache the generated topology graph in the applications plugin. type Cache struct { LastFetch time.Time @@ -76,7 +85,7 @@ func Get(ctx context.Context, clustersClient clusters.Client) *Topology { nodes = append(nodes, Node{ Data: NodeData{ application.Cluster + "-" + application.Namespace + "-" + application.Name, - "application", + application.Topology.Type, application.Name, application.Cluster + "-" + application.Namespace, application, @@ -86,7 +95,7 @@ func Get(ctx context.Context, clustersClient clusters.Client) *Topology { // The cluster and namespace field in the reference for a dependency is optional. So that we have to set the // cluster and namespace to the cluster and namespace of the current application, when it isn't defined in // the dependency reference. - for _, dependency := range application.Dependencies { + for _, dependency := range application.Topology.Dependencies { dependencyCluster := dependency.Cluster if dependencyCluster == "" { dependencyCluster = application.Cluster @@ -139,7 +148,7 @@ func Get(ctx context.Context, clustersClient clusters.Client) *Topology { // are adding the source and target nodes. We are also creating an additional slice for the cluster and namespace nodes, // which are then merged with the nodes slice. This is necessary, because we do not save the clusters and namespaces as // nodes in the cached topology. -func Generate(topology *Topology, clusters, namespaces []string) *Topology { +func Generate(topology *Topology, clusters, namespaces, tags []string) *Topology { var edges []Edge var nodes []Node var clusterNodes []Node @@ -166,6 +175,7 @@ func Generate(topology *Topology, clusters, namespaces []string) *Topology { for _, edge := range edges { for _, node := range topology.Nodes { if node.Data.ID == edge.Data.Source || node.Data.ID == edge.Data.Target { + node.Data.Type = getNodeTyp(node, namespaces, tags) nodes = appendNodeIfMissing(nodes, node) clusterNode := Node{ @@ -232,3 +242,53 @@ func appendNodeIfMissing(nodes []Node, node Node) []Node { return append(nodes, node) } + +// getNodeTyp returns the correct node type for a node in the topology graph. This is required, because we only filter +// the topology graph by the selected namespaces and not tags. To apply a different style for node which are outside of +// the selected namespaces or for nodes which are not containing a tag. To change the style we add the "-not-selected" +// suffix. The following rules are applied: +// - No namespaces and tags were selected -- NORMAL +// - The node contains a selected tag --> NORMAL +// - The node is within a selected namespaces and the user didn't selected any tags --> NORMAL +// - Node is in the list of selected namespaces but not in the list of selected tags --> WITH SUFFIX +// - Node is not in the list of selected namespaces and tags --> SUFFIX +func getNodeTyp(node Node, namespaces, tags []string) string { + if namespaces == nil && tags == nil { + return node.Data.Type + } + + isInNamespace := isItemInItems(node.Data.Namespace, namespaces) + containsTag := isItemsInItems(node.Data.Tags, tags) + + if containsTag { + return node.Data.Type + } + + if tags == nil && isInNamespace { + return node.Data.Type + } + + return node.Data.Type + "-not-selected" +} + +// isItemsInItems return true, when one item from a list of actual items is in a list of expected items. +func isItemsInItems(itemsActual, itemExpected []string) bool { + for _, tag := range itemsActual { + if isItemInItems(tag, itemExpected) { + return true + } + } + + return false +} + +// isItemInItems returns true when the item is in a list of items. +func isItemInItems(item string, items []string) bool { + for _, i := range items { + if i == item { + return true + } + } + + return false +} diff --git a/plugins/applications/pkg/topology/topology_test.go b/plugins/applications/pkg/topology/topology_test.go index 9d2dcd4b3..617821b57 100644 --- a/plugins/applications/pkg/topology/topology_test.go +++ b/plugins/applications/pkg/topology/topology_test.go @@ -21,11 +21,11 @@ var expectedGetTopology = Topology{ {Data: EdgeData{ID: "cluster1-namespace2-application3-cluster2-namespace3-application5", Source: "cluster1-namespace2-application3", SourceCluster: "cluster1", SourceNamespace: "namespace2", SourceName: "application3", Target: "cluster2-namespace3-application5", TargetCluster: "cluster2", TargetNamespace: "namespace3", TargetName: "application5"}}, }, Nodes: []Node{ - {Data: NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}, - {Data: NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}, - {Data: NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}, - {Data: NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}}}, - {Data: NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, + {Data: NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}}, + {Data: NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}}, + {Data: NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}}, + {Data: NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4", Topology: application.Topology{Type: "application"}}}}, + {Data: NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5", Topology: application.Topology{Type: "application"}}}}, }, } @@ -37,11 +37,11 @@ var expectedGenerateTopology = Topology{ {Data: EdgeData{ID: "cluster1-namespace2-application3-cluster2-namespace3-application5", Source: "cluster1-namespace2-application3", SourceCluster: "cluster1", SourceNamespace: "namespace2", SourceName: "application3", Target: "cluster2-namespace3-application5", TargetCluster: "cluster2", TargetNamespace: "namespace3", TargetName: "application5"}}, }, Nodes: []Node{ - {Data: NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}, - {Data: NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}, - {Data: NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}, - {Data: NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}}}, - {Data: NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, + {Data: NodeData{ID: "cluster1-namespace1-application1", Type: "application", Label: "application1", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}}}, + {Data: NodeData{ID: "cluster1-namespace1-application2", Type: "application", Label: "application2", Parent: "cluster1-namespace1", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}}}, + {Data: NodeData{ID: "cluster1-namespace2-application3", Type: "application", Label: "application3", Parent: "cluster1-namespace2", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}}}, + {Data: NodeData{ID: "cluster2-namespace3-application4", Type: "application", Label: "application4", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application4", Topology: application.Topology{Type: "application"}}}}, + {Data: NodeData{ID: "cluster2-namespace3-application5", Type: "application", Label: "application5", Parent: "cluster2-namespace3", ApplicationSpec: application.ApplicationSpec{Cluster: "cluster2", Namespace: "namespace3", Name: "application5", Topology: application.Topology{Type: "application"}}}}, {Data: NodeData{ID: "cluster1", Type: "cluster", Label: "cluster1", Parent: "", ApplicationSpec: application.ApplicationSpec{Cluster: "", Namespace: "", Name: ""}}}, {Data: NodeData{ID: "cluster2", Type: "cluster", Label: "cluster2", Parent: "", ApplicationSpec: application.ApplicationSpec{Cluster: "", Namespace: "", Name: ""}}}, {Data: NodeData{ID: "cluster1-namespace1", Type: "namespace", Label: "namespace1", Parent: "cluster1", ApplicationSpec: application.ApplicationSpec{Cluster: "", Namespace: "", Name: ""}}}, @@ -72,11 +72,11 @@ func TestGet(t *testing.T) { t.Run("get topology", func(t *testing.T) { mockClusterClient.On("GetApplications", mock.Anything, "").Return([]application.ApplicationSpec{ - {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}, - {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}, - {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}, - {Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, - {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application1", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "", Name: "application2"}}}}, + {Cluster: "cluster1", Namespace: "namespace1", Name: "application2", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "", Namespace: "namespace2", Name: "application3"}}}}, + {Cluster: "cluster1", Namespace: "namespace2", Name: "application3", Topology: application.Topology{Type: "application", Dependencies: []application.Reference{{Cluster: "cluster2", Namespace: "namespace3", Name: "application4"}, {Cluster: "cluster2", Namespace: "namespace3", Name: "application5"}}}}, + {Cluster: "cluster2", Namespace: "namespace3", Name: "application4", Topology: application.Topology{Type: "application"}}, + {Cluster: "cluster2", Namespace: "namespace3", Name: "application5", Topology: application.Topology{Type: "application"}}, }, nil).Once() actualTopology := Get(context.Background(), mockClustersClient) require.Equal(t, &expectedGetTopology, actualTopology) @@ -84,10 +84,10 @@ func TestGet(t *testing.T) { } func TestGenerate(t *testing.T) { - actualTopology1 := Generate(&expectedGetTopology, []string{"cluster1", "cluster2"}, []string{"namespace1", "namespace2", "namespace3"}) + actualTopology1 := Generate(&expectedGetTopology, []string{"cluster1", "cluster2"}, []string{"namespace1", "namespace2", "namespace3"}, nil) require.Equal(t, &expectedGenerateTopology, actualTopology1) - actualTopology2 := Generate(&expectedGetTopology, []string{"cluster1", "cluster2"}, nil) + actualTopology2 := Generate(&expectedGetTopology, []string{"cluster1", "cluster2"}, nil, nil) require.Equal(t, &expectedGenerateTopology, actualTopology2) } @@ -139,3 +139,21 @@ func TestAppendNodeIfMissing(t *testing.T) { }) } } + +func TestGetNodeType(t *testing.T) { + require.Equal(t, "type1", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace1", Tags: []string{"tag1"}}}}, nil, nil)) + require.Equal(t, "type1", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace1", Tags: []string{"tag1"}}}}, nil, []string{"tag1", "tag2"})) + require.Equal(t, "type1", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace1", Tags: []string{"tag1"}}}}, []string{"namespace1", "namespace2"}, nil)) + require.Equal(t, "type1", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace3", Tags: []string{"tag1"}}}}, []string{"namespace1", "namespace2"}, []string{"tag1", "tag2"})) + require.Equal(t, "type1-not-selected", getNodeTyp(Node{Data: NodeData{"id1", "type1", "name1", "parent", application.ApplicationSpec{Namespace: "namespace3", Tags: []string{"tag3"}}}}, []string{"namespace1", "namespace2"}, []string{"tag1", "tag2"})) +} + +func TestIsItemsInItems(t *testing.T) { + require.Equal(t, true, isItemsInItems([]string{"namespace2", "namespace3"}, []string{"namespace1", "namespace2", "namespace3"})) + require.Equal(t, false, isItemsInItems([]string{"namespace4", "namespace5"}, []string{"namespace1", "namespace2", "namespace3"})) +} + +func TestIsItemInItems(t *testing.T) { + require.Equal(t, true, isItemInItems("namespace1", []string{"namespace1", "namespace2", "namespace3"})) + require.Equal(t, false, isItemInItems("namespace4", []string{"namespace1", "namespace2", "namespace3"})) +} diff --git a/plugins/applications/src/components/page/Applications.tsx b/plugins/applications/src/components/page/Applications.tsx index 5faebf0f7..9cee412f2 100644 --- a/plugins/applications/src/components/page/Applications.tsx +++ b/plugins/applications/src/components/page/Applications.tsx @@ -13,6 +13,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import ApplicationsToolbar from './ApplicationsToolbar'; import { IOptions } from '../../utils/interfaces'; +import { IPluginDataOptions } from '@kobsio/plugin-core'; import Panel from '../panel/Panel'; import { getInitialOptions } from '../../utils/helpers'; @@ -20,6 +21,7 @@ export interface IApplicationsProps { name: string; displayName: string; description: string; + pluginOptions?: IPluginDataOptions; } // Applications is the page which lets the user query all the created applications by cluster and namespace. The user @@ -29,6 +31,7 @@ const Applications: React.FunctionComponent = ({ name, displayName, description, + pluginOptions, }: IApplicationsProps) => { const history = useHistory(); const location = useLocation(); @@ -92,6 +95,7 @@ const Applications: React.FunctionComponent = ({ view: options.view, }} times={options.times} + pluginOptions={pluginOptions} setDetails={setDetails} /> )} diff --git a/plugins/applications/src/components/page/Page.tsx b/plugins/applications/src/components/page/Page.tsx index 80d23dfb5..f32d981fc 100644 --- a/plugins/applications/src/components/page/Page.tsx +++ b/plugins/applications/src/components/page/Page.tsx @@ -8,11 +8,16 @@ import { IPluginPageProps } from '@kobsio/plugin-core'; // The page for the applications plugin, supports two different routes: One for showing a gallery / topology of // applications, where the user can select a list of clusters and namespaces for which he wants to get the applications // and a second one for showing a single application. -const Page: React.FunctionComponent = ({ name, displayName, description }: IPluginPageProps) => { +const Page: React.FunctionComponent = ({ + name, + displayName, + description, + pluginOptions, +}: IPluginPageProps) => { return ( - + diff --git a/plugins/applications/src/components/panel/ApplicationsTopology.tsx b/plugins/applications/src/components/panel/ApplicationsTopology.tsx index dda59d894..ee5665166 100644 --- a/plugins/applications/src/components/panel/ApplicationsTopology.tsx +++ b/plugins/applications/src/components/panel/ApplicationsTopology.tsx @@ -1,6 +1,7 @@ import { Alert, AlertActionLink, AlertVariant, Spinner } from '@patternfly/react-core'; import { QueryObserverResult, useQuery } from 'react-query'; import React, { memo } from 'react'; +import cytoscape from 'cytoscape'; import { useHistory } from 'react-router-dom'; import { IEdge, INode } from '../../utils/interfaces'; @@ -17,6 +18,7 @@ interface IApplicationsTopologyProps { namespaces: string[]; tags: string[]; times: IPluginTimes; + customStyleSheet: cytoscape.Stylesheet[]; setDetails?: (details: React.ReactNode) => void; } @@ -28,6 +30,7 @@ const ApplicationsTopology: React.FunctionComponent namespaces, tags, times, + customStyleSheet, setDetails, }: IApplicationsTopologyProps) => { const history = useHistory(); @@ -96,7 +99,12 @@ const ApplicationsTopology: React.FunctionComponent return (
- +
); }; diff --git a/plugins/applications/src/components/panel/ApplicationsTopologyGraph.tsx b/plugins/applications/src/components/panel/ApplicationsTopologyGraph.tsx index 1f397e90c..ea7ed040e 100644 --- a/plugins/applications/src/components/panel/ApplicationsTopologyGraph.tsx +++ b/plugins/applications/src/components/panel/ApplicationsTopologyGraph.tsx @@ -21,9 +21,9 @@ const layout = { rankDir: 'LR', }; -// styleSheet changes the style of the nodes and edges in the topology graph. +// defaultStyleSheet changes the style of the nodes and edges in the topology graph. // See: https://js.cytoscape.org/#style -const styleSheet: cytoscape.Stylesheet[] = [ +const defaultStyleSheet: cytoscape.Stylesheet[] = [ { selector: 'node', style: { @@ -44,6 +44,7 @@ const styleSheet: cytoscape.Stylesheet[] = [ selector: "node[type='cluster']", style: { 'background-color': '#f0f0f0', + 'border-style': 'dashed', }, }, { @@ -55,7 +56,16 @@ const styleSheet: cytoscape.Stylesheet[] = [ { selector: "node[type='application']", style: { - 'background-color': '#ffffff', + 'background-color': '#0066cc', + shape: 'roundrectangle', + }, + }, + { + selector: "node[type='application-not-selected']", + style: { + 'background-color': '#0066cc', + 'background-opacity': 0.25, + shape: 'roundrectangle', }, }, { @@ -92,6 +102,7 @@ const nodeLabel = (node: INodeData): string => { interface IApplicationsTopologyGraphProps { edges: IEdge[]; nodes: INode[]; + customStyleSheet: cytoscape.Stylesheet[]; setDetails?: (details: React.ReactNode) => void; } @@ -99,6 +110,7 @@ interface IApplicationsTopologyGraphProps { const ApplicationsTopologyGraph: React.FunctionComponent = ({ edges, nodes, + customStyleSheet, setDetails, }: IApplicationsTopologyGraphProps) => { const [width, setWidth] = useState(0); @@ -172,7 +184,7 @@ const ApplicationsTopologyGraph: React.FunctionComponent ) : null} diff --git a/plugins/applications/src/components/panel/Panel.tsx b/plugins/applications/src/components/panel/Panel.tsx index 064964066..f4ef66f1f 100644 --- a/plugins/applications/src/components/panel/Panel.tsx +++ b/plugins/applications/src/components/panel/Panel.tsx @@ -1,4 +1,5 @@ import React, { memo } from 'react'; +import cytoscape from 'cytoscape'; import { IPluginPanelProps, PluginCard, PluginOptionsMissing } from '@kobsio/plugin-core'; import ApplicationsGallery from './ApplicationsGallery'; @@ -19,6 +20,7 @@ export const Panel: React.FunctionComponent = ({ description, options, times, + pluginOptions, setDetails, }: IPanelProps) => { // We have to validate that the required options object was provided in the Application CR by a user. This is @@ -39,12 +41,35 @@ export const Panel: React.FunctionComponent = ({ // When a title is set we are sure that the component is used in a dashboard so we will wrap the topology component in // the PluginCard component. if (options.view === 'topology') { + const customStyleSheet: cytoscape.Stylesheet[] = []; + + if (pluginOptions && pluginOptions.topology && Array.isArray(pluginOptions.topology)) { + for (const topologyItem of pluginOptions.topology) { + customStyleSheet.push({ + selector: `node[type='${topologyItem.type}']`, + style: { + 'background-color': topologyItem.color || '#0066cc', + shape: topologyItem.shape || 'roundrectangle', + }, + }); + customStyleSheet.push({ + selector: `node[type='${topologyItem.type}-not-selected']`, + style: { + 'background-color': topologyItem.color || '#0066cc', + 'background-opacity': 0.25, + shape: topologyItem.shape || 'roundrectangle', + }, + }); + } + } + const topology = ( ); diff --git a/plugins/applications/src/components/panel/details/Details.tsx b/plugins/applications/src/components/panel/details/Details.tsx index ddf24776f..3a8762482 100644 --- a/plugins/applications/src/components/panel/details/Details.tsx +++ b/plugins/applications/src/components/panel/details/Details.tsx @@ -40,20 +40,20 @@ const Details: React.FunctionComponent = ({ application, close }:
- {application.tags && ( -

- {application.tags.map((tag) => ( - - {tag.toLowerCase()} - - ))} -

- )} {application.description &&

{application.description}

} - {(application.teams && application.teams.length > 0) || - (application.dependencies && application.dependencies.length > 0) || + + {(application.tags && application.tags.length > 0) || + (application.teams && application.teams.length > 0) || + (application.topology?.dependencies && application.topology.dependencies.length > 0) || (application.links && application.links.length > 0) ? ( + {application.tags && + application.tags.map((tag) => ( + + {tag.toLowerCase()} + + ))} + {application.teams && application.teams.length > 0 ? application.teams.map((team, index) => ( @@ -71,8 +71,8 @@ const Details: React.FunctionComponent = ({ application, close }: )) : null} - {application.dependencies && application.dependencies.length > 0 - ? application.dependencies.map((dependency, index) => ( + {application.topology?.dependencies && application.topology.dependencies.length > 0 + ? application.topology.dependencies.map((dependency, index) => ( = ({ // a drawer it can happen that we already show some dashboards in the main view and so we can not rely on the query // parameters. useEffect(() => { - if (setDetails !== undefined) { - setOptions(getInitialOptions(location.search, references, setDetails !== undefined)); - } + setOptions(getInitialOptions(location.search, references, setDetails !== undefined)); }, [location.search, references, setDetails]); // Fetch all dashboards. The dashboards are available via the data variable. To fetch the dashboards we have to pass diff --git a/plugins/dashboards/src/utils/dashboard.ts b/plugins/dashboards/src/utils/dashboard.ts index 3cb5da7f9..8f2e33ae6 100644 --- a/plugins/dashboards/src/utils/dashboard.ts +++ b/plugins/dashboards/src/utils/dashboard.ts @@ -31,9 +31,9 @@ export const rowHeight = (rowSize: number | undefined, rowSpan: number | undefin return `${rowSize * 150}px`; }; -// getOptionsFromSearch is used to parse the given search location and return is as options for Prometheus. This is -// needed, so that a user can explore his Prometheus data from a chart. When the user selects the explore action, we -// pass him to this page and pass the data via the URL parameters. +// getInitialOptions is used to parse the given search location and return is as options for Prometheus. This is needed, +// so that a user can explore his Prometheus data from a chart. When the user selects the explore action, we pass him to +// this page and pass the data via the URL parameters. export const getInitialOptions = ( search: string, references: IDashboardReference[],