Created Cluster Health Dashboard in Admin Portal (#2674)

* Cluster Health DashBoard

* built portal and remove some linting issues

* made the time in UTC and fixed a bug

* fixed few things and changed the code as per review

* rebased

* made few changes according to the review.

* renamed a helper component, rebuilt the portal

* minor changes

* Did few nit changes and some code improvements

* removed the mod issue

* vendor issue

* fixed lint issue

* remove lint error try1

* changed the code according to the review received.

* reverted the clusterdetaillist switch case

* small nit
This commit is contained in:
Anshul Verma 2023-04-11 17:53:39 -07:00 коммит произвёл GitHub
Родитель c05a425e10
Коммит 89d335a041
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
33 изменённых файлов: 3179 добавлений и 388 удалений

2
go.sum
Просмотреть файл

@ -960,6 +960,7 @@ github.com/jongio/azidext/go/azidext v0.4.0 h1:TOYyVFMeWGgXNhURSgrEtUCu7JAAKgsy+
github.com/jongio/azidext/go/azidext v0.4.0/go.mod h1:VrlpGde5B+pPbTUxnThE5UIQQkcebdr3jrC2MmlMVSI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@ -1151,6 +1152,7 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=

Просмотреть файл

@ -4,7 +4,5 @@
"index.html": "/index.html",
"main.61fbb3a0.js.map": "/static/js/main.61fbb3a0.js.map"
},
"entrypoints": [
"static/js/main.61fbb3a0.js"
]
}
"entrypoints": ["static/js/main.61fbb3a0.js"]
}

Просмотреть файл

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>ARO Portal</title><script defer="defer" src="/static/js/main.61fbb3a0.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>ARO Portal</title><script defer="defer" src="/static/js/main.61fbb3a0.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -11,6 +11,10 @@ import (
"time"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/gorilla/mux"
"github.com/Azure/ARO-RP/pkg/portal/cluster"
"github.com/Azure/ARO-RP/pkg/portal/prometheus"
)
type AdminOpenShiftCluster struct {
@ -190,3 +194,60 @@ func (p *portal) machineSets(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
}
func (p *portal) statistics(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
duration := r.URL.Query().Get("duration")
parsedDuration, err := time.ParseDuration(duration)
if err != nil {
p.badRequest(w, err)
return
}
endTimeString := r.URL.Query().Get("endtime")
endTime, err := time.Parse(time.RFC3339, endTimeString)
if err != nil {
p.badRequest(w, err)
return
}
apiVars := mux.Vars(r)
statisticsType := apiVars["statisticsType"]
subscriptionID := apiVars["subscription"]
resourceGroup := apiVars["resourceGroup"]
clusterName := apiVars["clusterName"]
resourceID := p.getResourceID(subscriptionID, resourceGroup, clusterName)
promQuery, err := cluster.GetPromQuery(statisticsType)
if err != nil {
p.badRequest(w, err)
return
}
prom := prometheus.New(p.log, p.dbOpenShiftClusters, p.dialer)
httpClient, err := prom.Cli(ctx, resourceID)
if err != nil {
p.internalServerError(w, err)
return
}
fetcher, err := p.makeFetcher(ctx, r)
if err != nil {
p.internalServerError(w, err)
return
}
promHost, promScheme := prom.GetPrometheusHostAndScheme()
prometheusURL := promScheme + "://" + promHost
APIStatistics, err := fetcher.Statistics(ctx, httpClient, promQuery, parsedDuration, endTime, prometheusURL)
if err != nil {
p.internalServerError(w, err)
return
}
b, err := json.MarshalIndent(APIStatistics, "", " ")
if err != nil {
p.internalServerError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)
if err != nil {
p.log.Error(err)
}
}

Просмотреть файл

@ -5,6 +5,8 @@ package cluster
import (
"context"
"net/http"
"time"
configclient "github.com/openshift/client-go/config/clientset/versioned"
machineclient "github.com/openshift/client-go/machine/clientset/versioned"
@ -23,6 +25,7 @@ type FetchClient interface {
ClusterOperators(context.Context) (*ClusterOperatorsInformation, error)
Machines(context.Context) (*MachineListInformation, error)
MachineSets(context.Context) (*MachineSetListInformation, error)
Statistics(context.Context, *http.Client, string, time.Duration, time.Time, string) ([]Metrics, error)
}
// client is an implementation of FetchClient. It currently contains a "fetcher"

Просмотреть файл

@ -0,0 +1,104 @@
package cluster
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"errors"
"net/http"
"time"
prometheusAPI "github.com/prometheus/client_golang/api"
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/prometheus/common/model"
)
// MetricValue contains the actual data of the metrics at certain timestamp, and a slice of this is used in the `Metrics` struct to combine all the metrics in one object.
type MetricValue struct {
Timestamp time.Time `json:"timestamp"`
Value float64 `json:"value"`
}
// This is a structure which actually carries, a particular type of metrics
type Metrics struct {
Name string `json:"metricname"`
Value []MetricValue `json:"metricvalue"`
}
func (c *client) Statistics(ctx context.Context, httpClient *http.Client, promQuery string, duration time.Duration, endTime time.Time, prometheusURL string) ([]Metrics, error) {
return c.fetcher.statistics(ctx, httpClient, promQuery, duration, endTime, prometheusURL)
}
func (f *realFetcher) statistics(ctx context.Context, httpClient *http.Client, promQuery string, duration time.Duration, endTime time.Time, prometheusURL string) ([]Metrics, error) {
promConfig := prometheusAPI.Config{
Address: prometheusURL,
RoundTripper: httpClient.Transport,
}
client, err := prometheusAPI.NewClient(promConfig)
if err != nil {
return nil, err
}
v1api := v1.NewAPI(client)
startTime := endTime.Add(-1 * duration)
value, warning, err := v1api.QueryRange(ctx, promQuery, v1.Range{
Start: startTime,
End: endTime,
Step: time.Minute * 2,
})
if len(warning) > 0 {
f.log.Warn(warning)
}
if err != nil {
return nil, err
}
valueMatrix := value.(model.Matrix)
return convertToTypeMetrics(valueMatrix), nil
}
func convertToTypeMetrics(v model.Matrix) []Metrics {
metrics := make([]Metrics, 0)
for _, i := range v {
metric := Metrics{}
metric.Name = i.Metric.String()
metricValues := make([]MetricValue, 0)
for _, j := range i.Values {
metricValues = append(metricValues, MetricValue{
Timestamp: j.Timestamp.Time().Local().UTC(),
Value: float64(j.Value),
})
}
metric.Value = metricValues
metrics = append(metrics, metric)
}
return metrics
}
func GetPromQuery(statisticsType string) (string, error) {
promQueries := map[string]string{
"kubeapicodes": "sum(rate(apiserver_request_total{job=\"apiserver\",code=~\"[45]..\"}[10m])) by (code, verb)",
"kubeapicpu": "rate(process_cpu_seconds_total{job=\"apiserver\"}[5m])",
"kubeapimemory": "process_resident_memory_bytes{job=\"apiserver\"}",
// kube-controller-manager
"kubecontrollermanagercodes": "sum(rate(rest_client_requests_total{job=\"kube-controller-manager\"}[5m])) by (code)",
"kubecontrollermanagercpu": "rate(process_cpu_seconds_total{job=\"kube-controller-manager\"}[5m])",
"kubecontrollermanagermemory": "process_resident_memory_bytes{job=\"kube-controller-manager\"}",
// DNS
"dnsresponsecodes": "sum(rate(coredns_dns_responses_total[5m])) by (rcode)",
"dnserrorrate": "sum(rate(coredns_dns_responses_total{rcode=~\"SERVFAIL|NXDOMAIN\"}[5m])) by (pod) / sum(rate(coredns_dns_responses_total{rcode=~\"NOERROR\"}[5m])) by (pod)",
"dnshealthcheck": "histogram_quantile(0.99, sum(rate(coredns_health_request_duration_seconds_bucket[5m])) by (le))",
"dnsforwardedtraffic": "histogram_quantile(0.95, sum(rate(coredns_forward_request_duration_seconds_bucket[5m])) by (le))",
"dnsalltraffic": "histogram_quantile(0.95, sum(rate(coredns_dns_request_duration_seconds_bucket[5m])) by (le))",
// Ingress
"ingresscontrollercondition": "sum(ingress_controller_conditions) by (condition)",
}
promQuery, ok := promQueries[statisticsType]
if !ok {
return "", errors.New("invalid statistic type '" + statisticsType + "'")
}
return promQuery, nil
}

Просмотреть файл

@ -5,10 +5,8 @@ package portal
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
@ -37,12 +35,10 @@ func (p *portal) clusterInfo(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiVars := mux.Vars(r)
subscription := apiVars["subscription"]
resourceGroup := apiVars["resourceGroup"]
clusterName := apiVars["clusterName"]
resourceId := strings.ToLower(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.RedHatOpenShift/openShiftClusters/%s", subscription, resourceGroup, clusterName))
resourceId := p.getResourceID(subscription, resourceGroup, clusterName)
doc, err := p.dbOpenShiftClusters.Get(ctx, resourceId)
if err != nil {

Просмотреть файл

@ -305,6 +305,7 @@ func (p *portal) aadAuthenticatedRoutes(r *mux.Router, prom *prometheus.Promethe
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/nodes").HandlerFunc(p.nodes)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/machines").HandlerFunc(p.machines)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/machine-sets").HandlerFunc(p.machineSets)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/statistics/{statisticsType}").HandlerFunc(p.statistics)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}").HandlerFunc(p.clusterInfo)
// prometheus
@ -360,16 +361,10 @@ func (p *portal) indexV2(w http.ResponseWriter, r *http.Request) {
// makeFetcher creates a cluster.FetchClient suitable for use by the Portal REST API
func (p *portal) makeFetcher(ctx context.Context, r *http.Request) (cluster.FetchClient, error) {
apiVars := mux.Vars(r)
subscription := apiVars["subscription"]
subscriptionID := apiVars["subscription"]
resourceGroup := apiVars["resourceGroup"]
clusterName := apiVars["clusterName"]
resourceID :=
strings.ToLower(
fmt.Sprintf(
"/subscriptions/%s/resourceGroups/%s/providers/Microsoft.RedHatOpenShift/openShiftClusters/%s",
subscription, resourceGroup, clusterName))
resourceID := p.getResourceID(subscriptionID, resourceGroup, clusterName)
if !validate.RxClusterID.MatchString(resourceID) {
return nil, fmt.Errorf("invalid resource ID")
}
@ -406,7 +401,19 @@ func (p *portal) serve(path string) func(w http.ResponseWriter, r *http.Request)
}
}
func (p *portal) getResourceID(subscriptionID, resourceGroup, clusterName string) string {
return strings.ToLower(
fmt.Sprintf(
"/subscriptions/%s/resourceGroups/%s/providers/Microsoft.RedHatOpenShift/openShiftClusters/%s",
subscriptionID, resourceGroup, clusterName))
}
func (p *portal) internalServerError(w http.ResponseWriter, err error) {
p.log.Warn(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
func (p *portal) badRequest(w http.ResponseWriter, err error) {
p.log.Debug(err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}

Просмотреть файл

@ -46,7 +46,7 @@ func (p *Prometheus) Director(r *http.Request) {
cli := p.clientCache.Get(resourceID)
if cli == nil {
var err error
cli, err = p.cli(ctx, resourceID)
cli, err = p.Cli(ctx, resourceID)
if err != nil {
p.error(r, http.StatusInternalServerError, err)
return
@ -56,8 +56,7 @@ func (p *Prometheus) Director(r *http.Request) {
}
r.RequestURI = ""
r.URL.Scheme = "http"
r.URL.Host = "prometheus-k8s-0:9090"
r.URL.Host, r.URL.Scheme = p.GetPrometheusHostAndScheme()
r.URL.Path = "/" + strings.Join(strings.Split(r.URL.Path, "/")[10:], "/")
r.Header.Del("Cookie")
r.Header.Del("Referer")
@ -69,7 +68,11 @@ func (p *Prometheus) Director(r *http.Request) {
*r = *r.WithContext(context.WithValue(ctx, contextKeyClient, cli))
}
func (p *Prometheus) cli(ctx context.Context, resourceID string) (*http.Client, error) {
func (p *Prometheus) GetPrometheusHostAndScheme() (string, string) {
return "prometheus-k8s-0:9090", "http"
}
func (p *Prometheus) Cli(ctx context.Context, resourceID string) (*http.Client, error) {
openShiftDoc, err := p.dbOpenShiftClusters.Get(ctx, resourceID)
if err != nil {
return nil, err

5
portal/v1/package-lock.json сгенерированный
Просмотреть файл

@ -6735,7 +6735,7 @@
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.1.0",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
@ -7309,8 +7309,7 @@
"dev": true
},
"minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"version": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"dev": true,
"requires": {

509
portal/v2/package-lock.json сгенерированный
Просмотреть файл

@ -10,6 +10,7 @@
"license": "Apache2",
"dependencies": {
"@fluentui/react": "^8.101.0",
"@fluentui/react-charting": "^5.14.27",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/node": "^18.11.9",
@ -2349,9 +2350,9 @@
}
},
"node_modules/@fluentui/react": {
"version": "8.101.2",
"resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.101.2.tgz",
"integrity": "sha512-onnQYXia/UANmrlzR7gGVNQH6lBsNxAYHicbjdCTfJ4ZwZ7otQIy0Z2StY2qgwhhzRil+30xyQwsv3AlCRXLKw==",
"version": "8.103.7",
"resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.103.7.tgz",
"integrity": "sha512-WXolw5VBRcwt4Ur1FJZ7QPffw6IQzt9Ba6lJrXts5PPrjTLE31F5o7R4s9xB/tX6QrWjSJ3XKWpRDqYXjtmD+g==",
"dependencies": {
"@fluentui/date-time-utilities": "^8.5.3",
"@fluentui/font-icons-mdl2": "^8.5.4",
@ -2375,6 +2376,44 @@
"react-dom": ">=16.8.0 <19.0.0"
}
},
"node_modules/@fluentui/react-charting": {
"version": "5.14.27",
"resolved": "https://registry.npmjs.org/@fluentui/react-charting/-/react-charting-5.14.27.tgz",
"integrity": "sha512-Kp1yxyfYVBtFrEi325KHoYpzVL6DFUAY1AGhd2Cji0ZD9BH9HzpEDPZDfPtyV3s2IMShzvsagfo7JKfv9qL2Cw==",
"dependencies": {
"@fluentui/react-focus": "^8.8.10",
"@fluentui/set-version": "^8.2.3",
"@microsoft/load-themed-styles": "^1.10.26",
"@types/d3-array": "1.2.1",
"@types/d3-axis": "1.0.10",
"@types/d3-format": "^1.3.1",
"@types/d3-hierarchy": "2.0.0",
"@types/d3-sankey": "^0.11.0",
"@types/d3-scale": "^4.0.0",
"@types/d3-selection": "1.4.1",
"@types/d3-shape": "^1.2.3",
"@types/d3-time": "^1.1.0",
"@types/d3-time-format": "^2.1.0",
"d3-array": "1.2.1",
"d3-axis": "1.0.8",
"d3-format": "^1.4.4",
"d3-hierarchy": "2.0.0",
"d3-sankey": "^0.12.3",
"d3-scale": "^4.0.0",
"d3-selection": "1.3.0",
"d3-shape": "^1.2.0",
"d3-time": "^1.1.0",
"d3-time-format": "^2.1.3",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@fluentui/react": "^8.103.7",
"@types/react": ">=16.8.0 <19.0.0",
"@types/react-dom": ">=16.8.0 <19.0.0",
"react": ">=16.8.0 <19.0.0",
"react-dom": ">=16.8.0 <19.0.0"
}
},
"node_modules/@fluentui/react-focus": {
"version": "8.8.10",
"resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.10.tgz",
@ -4035,6 +4074,73 @@
"integrity": "sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==",
"dev": true
},
"node_modules/@types/d3-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.1.tgz",
"integrity": "sha512-YBaAfimGdWE4nDuoGVKsH89/dkz2hWZ0i8qC+xxqmqi+XJ/aXiRF0jPtzXmN7VdkpVjy1xuDmM5/m1FNuB6VWA=="
},
"node_modules/@types/d3-axis": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.10.tgz",
"integrity": "sha512-5YF0wfdQMPKw01VAAupLIlg/T4pn5M3/vL9u0KZjiemnVnnKBEWE24na4X1iW+TfZiYJ8j+BgK2KFYnAAT54Ug==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-format": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz",
"integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ=="
},
"node_modules/@types/d3-hierarchy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz",
"integrity": "sha512-YxdskUvwzqggpnSnDQj4KVkicgjpkgXn/g/9M9iGsiToLS3nG6Ytjo1FoYhYVAAElV/fJBGVL3cQ9Hb7tcv+lw=="
},
"node_modules/@types/d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
},
"node_modules/@types/d3-sankey": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/@types/d3-sankey/-/d3-sankey-0.11.2.tgz",
"integrity": "sha512-U6SrTWUERSlOhnpSrgvMX64WblX1AxX6nEjI2t3mLK2USpQrnbwYYK+AS9SwiE7wgYmOsSSKoSdr8aoKBH0HgQ==",
"dependencies": {
"@types/d3-shape": "^1"
}
},
"node_modules/@types/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-selection": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.1.tgz",
"integrity": "sha512-bv8IfFYo/xG6dxri9OwDnK3yCagYPeRIjTlrcdYJSx+FDWlCeBDepIHUpqROmhPtZ53jyna0aUajZRk0I3rXNA=="
},
"node_modules/@types/d3-shape": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz",
"integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==",
"dependencies": {
"@types/d3-path": "^1"
}
},
"node_modules/@types/d3-time": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.1.tgz",
"integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw=="
},
"node_modules/@types/d3-time-format": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz",
"integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA=="
},
"node_modules/@types/eslint": {
"version": "8.4.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz",
@ -6613,6 +6719,122 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
},
"node_modules/d3-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.1.tgz",
"integrity": "sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw=="
},
"node_modules/d3-axis": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.8.tgz",
"integrity": "sha512-K0djTb26iQ6AsuD2d6Ka08wBHf4V30awIxV4XFuB/iLzYtTqqJlE/nIN0DBJJCX7lbOqbt2/oeX3r+sU5k2veg=="
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
"integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ=="
},
"node_modules/d3-hierarchy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz",
"integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw=="
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
"node_modules/d3-sankey": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
"integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
"dependencies": {
"d3-array": "1 - 2",
"d3-shape": "^1.2.0"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale/node_modules/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale/node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.0.tgz",
"integrity": "sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA=="
},
"node_modules/d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"dependencies": {
"d3-path": "1"
}
},
"node_modules/d3-time": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
},
"node_modules/d3-time-format": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz",
"integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==",
"dependencies": {
"d3-time": "1"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -8859,20 +9081,6 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -9619,6 +9827,14 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/ipaddr.js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
@ -19679,7 +19895,7 @@
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"dependencies": {
@ -19759,9 +19975,9 @@
}
},
"@fluentui/react": {
"version": "8.101.2",
"resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.101.2.tgz",
"integrity": "sha512-onnQYXia/UANmrlzR7gGVNQH6lBsNxAYHicbjdCTfJ4ZwZ7otQIy0Z2StY2qgwhhzRil+30xyQwsv3AlCRXLKw==",
"version": "8.103.7",
"resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.103.7.tgz",
"integrity": "sha512-WXolw5VBRcwt4Ur1FJZ7QPffw6IQzt9Ba6lJrXts5PPrjTLE31F5o7R4s9xB/tX6QrWjSJ3XKWpRDqYXjtmD+g==",
"requires": {
"@fluentui/date-time-utilities": "^8.5.3",
"@fluentui/font-icons-mdl2": "^8.5.4",
@ -19779,6 +19995,37 @@
"tslib": "^2.1.0"
}
},
"@fluentui/react-charting": {
"version": "5.14.27",
"resolved": "https://registry.npmjs.org/@fluentui/react-charting/-/react-charting-5.14.27.tgz",
"integrity": "sha512-Kp1yxyfYVBtFrEi325KHoYpzVL6DFUAY1AGhd2Cji0ZD9BH9HzpEDPZDfPtyV3s2IMShzvsagfo7JKfv9qL2Cw==",
"requires": {
"@fluentui/react-focus": "^8.8.10",
"@fluentui/set-version": "^8.2.3",
"@microsoft/load-themed-styles": "^1.10.26",
"@types/d3-array": "1.2.1",
"@types/d3-axis": "1.0.10",
"@types/d3-format": "^1.3.1",
"@types/d3-hierarchy": "2.0.0",
"@types/d3-sankey": "^0.11.0",
"@types/d3-scale": "^4.0.0",
"@types/d3-selection": "1.4.1",
"@types/d3-shape": "^1.2.3",
"@types/d3-time": "^1.1.0",
"@types/d3-time-format": "^2.1.0",
"d3-array": "1.2.1",
"d3-axis": "1.0.8",
"d3-format": "^1.4.4",
"d3-hierarchy": "2.0.0",
"d3-sankey": "^0.12.3",
"d3-scale": "^4.0.0",
"d3-selection": "1.3.0",
"d3-shape": "^1.2.0",
"d3-time": "^1.1.0",
"d3-time-format": "^2.1.3",
"tslib": "^2.1.0"
}
},
"@fluentui/react-focus": {
"version": "8.8.10",
"resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.10.tgz",
@ -19871,7 +20118,7 @@
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^5.1.0"
"minimatch": "^3.0.5"
}
},
"@humanwhocodes/module-importer": {
@ -20569,7 +20816,7 @@
"error-stack-parser": "^2.0.6",
"find-up": "^5.0.0",
"html-entities": "^2.1.0",
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.3",
"schema-utils": "^3.0.0",
"source-map": "^0.7.3"
}
@ -20981,6 +21228,73 @@
"integrity": "sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==",
"dev": true
},
"@types/d3-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.1.tgz",
"integrity": "sha512-YBaAfimGdWE4nDuoGVKsH89/dkz2hWZ0i8qC+xxqmqi+XJ/aXiRF0jPtzXmN7VdkpVjy1xuDmM5/m1FNuB6VWA=="
},
"@types/d3-axis": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.10.tgz",
"integrity": "sha512-5YF0wfdQMPKw01VAAupLIlg/T4pn5M3/vL9u0KZjiemnVnnKBEWE24na4X1iW+TfZiYJ8j+BgK2KFYnAAT54Ug==",
"requires": {
"@types/d3-selection": "*"
}
},
"@types/d3-format": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz",
"integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ=="
},
"@types/d3-hierarchy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz",
"integrity": "sha512-YxdskUvwzqggpnSnDQj4KVkicgjpkgXn/g/9M9iGsiToLS3nG6Ytjo1FoYhYVAAElV/fJBGVL3cQ9Hb7tcv+lw=="
},
"@types/d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
},
"@types/d3-sankey": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/@types/d3-sankey/-/d3-sankey-0.11.2.tgz",
"integrity": "sha512-U6SrTWUERSlOhnpSrgvMX64WblX1AxX6nEjI2t3mLK2USpQrnbwYYK+AS9SwiE7wgYmOsSSKoSdr8aoKBH0HgQ==",
"requires": {
"@types/d3-shape": "^1"
}
},
"@types/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
"requires": {
"@types/d3-time": "*"
}
},
"@types/d3-selection": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.1.tgz",
"integrity": "sha512-bv8IfFYo/xG6dxri9OwDnK3yCagYPeRIjTlrcdYJSx+FDWlCeBDepIHUpqROmhPtZ53jyna0aUajZRk0I3rXNA=="
},
"@types/d3-shape": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz",
"integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==",
"requires": {
"@types/d3-path": "^1"
}
},
"@types/d3-time": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.1.tgz",
"integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw=="
},
"@types/d3-time-format": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz",
"integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA=="
},
"@types/eslint": {
"version": "8.4.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz",
@ -21626,7 +21940,7 @@
"integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==",
"dev": true,
"requires": {
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.0",
"regex-parser": "^2.2.11"
}
},
@ -21938,7 +22252,7 @@
"dev": true,
"requires": {
"find-cache-dir": "^3.3.1",
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.0",
"make-dir": "^3.1.0",
"schema-utils": "^2.6.5"
},
@ -22942,6 +23256,109 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
},
"d3-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.1.tgz",
"integrity": "sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw=="
},
"d3-axis": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.8.tgz",
"integrity": "sha512-K0djTb26iQ6AsuD2d6Ka08wBHf4V30awIxV4XFuB/iLzYtTqqJlE/nIN0DBJJCX7lbOqbt2/oeX3r+sU5k2veg=="
},
"d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
},
"d3-format": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
"integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ=="
},
"d3-hierarchy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz",
"integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw=="
},
"d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"requires": {
"d3-color": "1 - 3"
}
},
"d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
"d3-sankey": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
"integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
"requires": {
"d3-array": "1 - 2",
"d3-shape": "^1.2.0"
}
},
"d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"requires": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"dependencies": {
"d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==",
"requires": {
"internmap": "1 - 2"
}
},
"d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"requires": {
"d3-array": "2 - 3"
}
}
}
},
"d3-selection": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.0.tgz",
"integrity": "sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA=="
},
"d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
},
"d3-time-format": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz",
"integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==",
"requires": {
"d3-time": "1"
}
},
"damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -23534,7 +23951,7 @@
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
"regexpp": "^3.2.0",
@ -23781,7 +24198,7 @@
"has": "^1.0.3",
"is-core-module": "^2.8.1",
"is-glob": "^4.0.3",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"object.values": "^1.1.5",
"resolve": "^1.22.0",
"tsconfig-paths": "^3.14.1"
@ -23839,7 +24256,7 @@
"has": "^1.0.3",
"jsx-ast-utils": "^3.3.2",
"language-tags": "^1.0.5",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"semver": "^6.3.0"
},
"dependencies": {
@ -23867,7 +24284,7 @@
"eslint-utils": "^3.0.0",
"ignore": "^5.1.1",
"is-core-module": "^2.11.0",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"resolve": "^1.22.1",
"semver": "^7.3.8"
},
@ -23903,7 +24320,7 @@
"doctrine": "^2.1.0",
"estraverse": "^5.3.0",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"object.entries": "^1.1.5",
"object.fromentries": "^2.0.5",
"object.hasown": "^1.1.1",
@ -24330,7 +24747,7 @@
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
"dev": true,
"requires": {
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
}
},
@ -24340,7 +24757,7 @@
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"dev": true,
"requires": {
"minimatch": "^5.1.0"
"minimatch": "^5.0.1"
}
},
"filesize": {
@ -24454,7 +24871,7 @@
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"memfs": "^3.1.2",
"minimatch": "^5.1.0",
"minimatch": "^3.0.4",
"schema-utils": "2.7.0",
"semver": "^7.3.2",
"tapable": "^1.0.0"
@ -24613,13 +25030,6 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -24701,7 +25111,7 @@
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.1.0",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
@ -25165,6 +25575,11 @@
"side-channel": "^1.0.4"
}
},
"internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
},
"ipaddr.js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
@ -25521,7 +25936,7 @@
"async": "^3.2.3",
"chalk": "^4.0.2",
"filelist": "^1.0.1",
"minimatch": "^5.1.0"
"minimatch": "^3.0.4"
},
"dependencies": {
"ansi-styles": {
@ -29055,7 +29470,7 @@
"gzip-size": "^6.0.0",
"immer": "^9.0.7",
"is-root": "^2.1.0",
"loader-utils": "^2.0.4",
"loader-utils": "^3.2.0",
"open": "^8.4.0",
"pkg-up": "^3.1.0",
"prompts": "^2.4.2",
@ -29158,7 +29573,7 @@
"requires": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
"@svgr/webpack": "^6.5.1",
"@svgr/webpack": "^5.5.0",
"babel-jest": "^27.4.2",
"babel-loader": "^8.2.3",
"babel-plugin-named-asset-import": "^0.3.8",
@ -29252,7 +29667,7 @@
"integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==",
"dev": true,
"requires": {
"minimatch": "^5.1.0"
"minimatch": "^3.0.5"
}
},
"regenerate": {
@ -29421,7 +29836,7 @@
"requires": {
"adjust-sourcemap-loader": "^4.0.0",
"convert-source-map": "^1.7.0",
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.0",
"postcss": "^7.0.35",
"source-map": "0.6.1"
},
@ -30296,7 +30711,7 @@
"requires": {
"@istanbuljs/schema": "^0.1.2",
"glob": "^7.1.4",
"minimatch": "^5.1.0"
"minimatch": "^3.0.4"
}
},
"text-table": {

Просмотреть файл

@ -6,6 +6,7 @@
"proxy": "https://localhost:8444",
"dependencies": {
"@fluentui/react": "^8.101.0",
"@fluentui/react-charting": "^5.14.27",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/node": "^18.11.9",

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,8 +1,4 @@
import {
IPanelStyles,
Panel,
PanelType,
} from "@fluentui/react/lib/Panel"
import { IPanelStyles, Panel, PanelType } from "@fluentui/react/lib/Panel"
import { useBoolean } from "@fluentui/react-hooks"
import { useState, useEffect, useRef, MutableRefObject, ReactElement } from "react"
import {
@ -17,10 +13,10 @@ import {
IIconStyles,
} from "@fluentui/react"
import { AxiosResponse } from "axios"
import { FetchClusterInfo } from "./Request"
import { fetchClusterInfo } from "./Request"
import { ICluster, headerStyles } from "./App"
import { Nav, INavLink, INavStyles } from "@fluentui/react/lib/Nav"
import { ClusterDetailComponent } from "./ClusterDetailList"
import { ClusterDetailComponent, MemoisedClusterDetailListComponent } from "./ClusterDetailList"
import React from "react"
const navStyles: Partial<INavStyles> = {
@ -37,15 +33,6 @@ const navStyles: Partial<INavStyles> = {
},
}
// let customPanelStyle: Partial<IPanelStyles> = {
// root: { top: "40px", left: "225px" },
// content: { paddingLeft: 30, paddingRight: 5 },
// navigation: {
// justifyContent: "flex-start",
// },
// }
const headerStyle: Partial<IStackStyles> = {
root: {
alignSelf: "flex-start",
@ -81,6 +68,10 @@ export const overviewKey = "overview"
export const nodesKey = "nodes"
export const machinesKey = "machines"
export const machineSetsKey = "machinesets"
export const apiStatisticsKey = "apistatistics"
export const kcmStatisticsKey = "kcmstatistics"
export const dnsStatisticsKey = "dnsstatistics"
export const ingressStatisticsKey = "ingressstatistics"
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
@ -122,32 +113,56 @@ export function ClusterDetailPanel(props: {
{
links: [
{
name: 'Overview',
name: "Overview",
key: overviewKey,
url: '#overview',
icon: 'ThisPC',
url: "#overview",
icon: "ThisPC",
},
{
name: 'Nodes',
name: "Nodes",
key: nodesKey,
url: '#nodes',
icon: 'BuildQueue',
url: "#nodes",
icon: "BuildQueue",
},
{
name: 'Machines',
name: "Machines",
key: machinesKey,
url: '#machines',
icon: 'BuildQueue',
url: "#machines",
icon: "BuildQueue",
},
{
name: 'MachineSets',
name: "MachineSets",
key: machineSetsKey,
url: '#machinesets',
icon: 'BuildQueue',
url: "#machinesets",
icon: "BuildQueue",
},
{
name: "APIStatistics",
key: apiStatisticsKey,
url: "#apistatistics",
icon: "BIDashboard",
},
{
name: "KCMStatistics",
key: kcmStatisticsKey,
url: "#kcmstatistics",
icon: "BIDashboard",
},
{
name: "DNSStatistics",
key: dnsStatisticsKey,
url: "#dnsstatistics",
icon: "BIDashboard",
},
{
name: "IngressStatistics",
key: ingressStatisticsKey,
url: "#ingressstatistics",
icon: "BIDashboard",
},
],
},
];
]
// updateData - updates the state of the component
// can be used if we want a refresh button.
@ -187,7 +202,7 @@ export function ClusterDetailPanel(props: {
if (fetching === "" && props.loaded === "DONE" && resourceID != "") {
setFetching("FETCHING")
setError(null)
FetchClusterInfo(props.currentCluster).then(onData)
fetchClusterInfo(props.currentCluster).then(onData)
}
}, [data, fetching, setFetching])
@ -217,15 +232,18 @@ export function ClusterDetailPanel(props: {
}
}
const [doubleChevronIconProp, setdoubleChevronIconProp] = useState({ iconName: "doublechevronleft"})
function _onClickDoubleChevronIcon() {
// const [doubleChevronIconProp, setdoubleChevronIconProp] = useState({ iconName: "doublechevronleft"})
const doubleChevronIconProp = useRef({ iconName: "doublechevronleft" })
const _onClickDoubleChevronIcon = () => {
let customPanelStyleRootLeft
if (doubleChevronIconProp.iconName == "doublechevronright") {
if (doubleChevronIconProp.current.iconName == "doublechevronright") {
customPanelStyleRootLeft = "225px"
setdoubleChevronIconProp({ iconName: "doublechevronleft"})
// setdoubleChevronIconProp({ iconName: "doublechevronleft"})
doubleChevronIconProp.current = { iconName: "doublechevronleft" }
} else {
customPanelStyleRootLeft = "0px"
setdoubleChevronIconProp({ iconName: "doublechevronright"})
// setdoubleChevronIconProp({ iconName: "doublechevronright"})
doubleChevronIconProp.current = { iconName: "doublechevronright" }
}
setcustomPanelStyle({
@ -237,16 +255,14 @@ export function ClusterDetailPanel(props: {
})
}
const onRenderHeader = (
): ReactElement => {
const onRenderHeader = (): ReactElement => {
return (
<>
<Stack styles={headerStyle} horizontal>
<Stack.Item styles={doubleChevronIconStyle}>
<IconButton
onClick={_onClickDoubleChevronIcon}
iconProps={doubleChevronIconProp}
iconProps={doubleChevronIconProp.current}
/>
</Stack.Item>
</Stack>
@ -287,7 +303,7 @@ export function ClusterDetailPanel(props: {
</Stack.Item>
<Separator vertical />
<Stack.Item grow>
<ClusterDetailComponent
<MemoisedClusterDetailListComponent
item={data}
cluster={props.currentCluster}
isDataLoaded={dataLoaded}

Просмотреть файл

@ -1,8 +1,11 @@
import { Component } from "react"
import { OverviewWrapper } from './ClusterDetailListComponents/OverviewWrapper';
import { NodesWrapper } from './ClusterDetailListComponents/NodesWrapper';
import { MachinesWrapper } from "./ClusterDetailListComponents/MachinesWrapper";
import { MachineSetsWrapper } from "./ClusterDetailListComponents/MachineSetsWrapper";
import React from "react"
import { OverviewWrapper } from "./ClusterDetailListComponents/OverviewWrapper"
import { NodesWrapper } from "./ClusterDetailListComponents/NodesWrapper"
import { MachinesWrapper } from "./ClusterDetailListComponents/MachinesWrapper"
import { MachineSetsWrapper } from "./ClusterDetailListComponents/MachineSetsWrapper"
import { Statistics } from "./ClusterDetailListComponents/Statistics/Statistics"
import { ICluster } from "./App"
interface ClusterDetailComponentProps {
@ -38,38 +41,95 @@ interface IClusterDetailComponentState {
detailPanelSelected: string
}
export class ClusterDetailComponent extends Component<ClusterDetailComponentProps, IClusterDetailComponentState> {
export class ClusterDetailComponent extends Component<
ClusterDetailComponentProps,
IClusterDetailComponentState
> {
constructor(props: ClusterDetailComponentProps | Readonly<ClusterDetailComponentProps>) {
super(props);
super(props)
}
public render() {
switch (this.props.detailPanelVisible.toLowerCase()) {
case "overview":
{
case "overview": {
return (
<OverviewWrapper clusterName= {this.props.item.name} currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
<OverviewWrapper
clusterName={this.props.item.name}
currentCluster={this.props.cluster!}
detailPanelSelected={this.props.detailPanelVisible}
loaded={this.props.isDataLoaded}
/>
)
}
case "nodes":
{
return (
<NodesWrapper currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
);
}
case "machines":
{
return (
<MachinesWrapper currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
);
}
case "machinesets":
{
return (
<MachineSetsWrapper currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
);
}
case "nodes": {
return (
<NodesWrapper
currentCluster={this.props.cluster!}
detailPanelSelected={this.props.detailPanelVisible}
loaded={this.props.isDataLoaded}
/>
)
}
case "machines": {
return (
<MachinesWrapper
currentCluster={this.props.cluster!}
detailPanelSelected={this.props.detailPanelVisible}
loaded={this.props.isDataLoaded}
/>
)
}
case "machinesets": {
return (
<MachineSetsWrapper
currentCluster={this.props.cluster!}
detailPanelSelected={this.props.detailPanelVisible}
loaded={this.props.isDataLoaded}
/>
)
}
case "apistatistics": {
return (
<Statistics
currentCluster={this.props.cluster!}
detailPanelSelected={this.props.detailPanelVisible}
loaded={this.props.isDataLoaded}
statisticsType={"api"}
/>
)
}
case "kcmstatistics": {
return (
<Statistics
currentCluster={this.props.cluster!}
detailPanelSelected={this.props.detailPanelVisible}
loaded={this.props.isDataLoaded}
statisticsType={"kcm"}
/>
)
}
case "dnsstatistics": {
return (
<Statistics
currentCluster={this.props.cluster!}
detailPanelSelected={this.props.detailPanelVisible}
loaded={this.props.isDataLoaded}
statisticsType={"dns"}
/>
)
}
case "ingressstatistics": {
return (
<Statistics
currentCluster={this.props.cluster!}
detailPanelSelected={this.props.detailPanelVisible}
loaded={this.props.isDataLoaded}
statisticsType={"ingress"}
/>
)
}
}
}
}
export const MemoisedClusterDetailListComponent = React.memo(ClusterDetailComponent)

Просмотреть файл

@ -1,18 +1,18 @@
import { useState, useEffect, useRef } from "react"
import { AxiosResponse } from 'axios';
import { FetchMachineSets } from '../Request';
import { AxiosResponse } from "axios"
import { fetchMachineSets } from "../Request"
import { ICluster } from "../App"
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
import { machineSetsKey } from "../ClusterDetail";
import { MachineSetsListComponent } from "./MachineSetsList";
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from "@fluentui/react"
import { machineSetsKey } from "../ClusterDetail"
import { MachineSetsListComponent } from "./MachineSetsList"
export interface IMachineSet {
name?: string,
type?: string,
createdAt?: string,
desiredReplicas?: string,
replicas?: string,
errorReason?: string,
name?: string
type?: string
createdAt?: string
desiredReplicas?: string
replicas?: string
errorReason?: string
errorMessage?: string
publicLoadBalancerName?: string
subnet?: string
@ -22,9 +22,9 @@ export interface IMachineSet {
}
export interface IOSDisk {
diskSettings: string,
diskSizeGB: string,
managedDisk: IManagedDisk,
diskSettings: string
diskSizeGB: string
managedDisk: IManagedDisk
osType: string
}
@ -51,8 +51,7 @@ export function MachineSetsWrapper(props: {
isMultiline={false}
onDismiss={() => setError(null)}
dismissButtonAriaLabel="Close"
styles={errorBarStyles}
>
styles={errorBarStyles}>
{error?.statusText}
</MessageBar>
)
@ -65,33 +64,37 @@ export function MachineSetsWrapper(props: {
setData(newData)
const machineSetList: IMachineSet[] = []
if (state && state.current) {
newData.machines.forEach((element: { name: string;
type: string;
createdat: string;
desiredreplicas: number;
replicas: number;
errorreason: string;
errormessage: string;
publicloadbalancername: string;
subnet: string;
accountstoragetype: string;
vNet: string;}) => {
const machineSet: IMachineSet = {
name: element.name,
type: element.type,
createdAt: element.createdat,
desiredReplicas: element.desiredreplicas.toString(),
replicas: element.replicas.toString(),
errorReason: element.errorreason,
errorMessage: element.errormessage,
publicLoadBalancerName: element.publicloadbalancername,
subnet: element.subnet,
vNet: element.vNet,
accountStorageType: element.accountstoragetype
}
newData.machines.forEach(
(element: {
name: string
type: string
createdat: string
desiredreplicas: number
replicas: number
errorreason: string
errormessage: string
publicloadbalancername: string
subnet: string
accountstoragetype: string
vNet: string
}) => {
const machineSet: IMachineSet = {
name: element.name,
type: element.type,
createdAt: element.createdat,
desiredReplicas: element.desiredreplicas.toString(),
replicas: element.replicas.toString(),
errorReason: element.errorreason,
errorMessage: element.errormessage,
publicLoadBalancerName: element.publicloadbalancername,
subnet: element.subnet,
vNet: element.vNet,
accountStorageType: element.accountstoragetype,
}
machineSetList.push(machineSet)
});
machineSetList.push(machineSet)
}
)
state.current.setState({ machineSets: machineSetList })
}
}
@ -106,12 +109,14 @@ export function MachineSetsWrapper(props: {
setFetching(props.currentCluster.name)
}
if (props.detailPanelSelected.toLowerCase() == machineSetsKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != "") {
if (
props.detailPanelSelected.toLowerCase() == machineSetsKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != ""
) {
setFetching("FETCHING")
FetchMachineSets(props.currentCluster).then(onData)
fetchMachineSets(props.currentCluster).then(onData)
}
}, [data, props.loaded, props.detailPanelSelected])
@ -119,8 +124,12 @@ export function MachineSetsWrapper(props: {
<Stack>
<Stack.Item grow>{error && errorBar()}</Stack.Item>
<Stack>
<MachineSetsListComponent machineSets={data!} ref={state} clusterName={props.currentCluster != null ? props.currentCluster.name : ""}/>
<MachineSetsListComponent
machineSets={data!}
ref={state}
clusterName={props.currentCluster != null ? props.currentCluster.name : ""}
/>
</Stack>
</Stack>
</Stack>
)
}

Просмотреть файл

@ -1,19 +1,19 @@
import { useState, useEffect, useRef } from "react"
import { AxiosResponse } from 'axios';
import { FetchMachines } from '../Request';
import { AxiosResponse } from "axios"
import { fetchMachines } from "../Request"
import { ICluster } from "../App"
import { MachinesListComponent } from './MachinesList';
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
import { machinesKey } from "../ClusterDetail";
import { MachinesListComponent } from "./MachinesList"
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from "@fluentui/react"
import { machinesKey } from "../ClusterDetail"
export interface IMachine {
name?: string,
createdTime: string,
lastUpdated: string,
errorReason: string,
errorMessage: string,
lastOperation: string,
lastOperationDate: string,
name?: string
createdTime: string
lastUpdated: string
errorReason: string
errorMessage: string
lastOperation: string
lastOperationDate: string
status: string
}
@ -36,8 +36,7 @@ export function MachinesWrapper(props: {
isMultiline={false}
onDismiss={() => setError(null)}
dismissButtonAriaLabel="Close"
styles={errorBarStyles}
>
styles={errorBarStyles}>
{error?.statusText}
</MessageBar>
)
@ -50,26 +49,30 @@ export function MachinesWrapper(props: {
setData(newData)
const machineList: IMachine[] = []
if (state && state.current) {
newData.machines.forEach((element: { name: string;
createdTime: string;
lastUpdated: string;
errorReason: string;
errorMessage: string;
lastOperation: string;
lastOperationDate: string;
status: string; }) => {
const machine: IMachine = {
name: element.name,
createdTime: element.createdTime,
lastUpdated: element.lastUpdated,
errorReason: element.errorReason,
errorMessage: element.errorMessage,
lastOperation: element.lastOperation,
lastOperationDate: element.lastOperationDate,
status: element.status,
newData.machines.forEach(
(element: {
name: string
createdTime: string
lastUpdated: string
errorReason: string
errorMessage: string
lastOperation: string
lastOperationDate: string
status: string
}) => {
const machine: IMachine = {
name: element.name,
createdTime: element.createdTime,
lastUpdated: element.lastUpdated,
errorReason: element.errorReason,
errorMessage: element.errorMessage,
lastOperation: element.lastOperation,
lastOperationDate: element.lastOperationDate,
status: element.status,
}
machineList.push(machine)
}
machineList.push(machine)
});
)
state.current.setState({ machines: machineList })
}
}
@ -84,12 +87,14 @@ export function MachinesWrapper(props: {
setFetching(props.currentCluster.name)
}
if (props.detailPanelSelected.toLowerCase() == machinesKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != "") {
if (
props.detailPanelSelected.toLowerCase() == machinesKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != ""
) {
setFetching("FETCHING")
FetchMachines(props.currentCluster).then(onData)
fetchMachines(props.currentCluster).then(onData)
}
}, [data, props.loaded, props.detailPanelSelected])
@ -97,8 +102,12 @@ export function MachinesWrapper(props: {
<Stack>
<Stack.Item grow>{error && errorBar()}</Stack.Item>
<Stack>
<MachinesListComponent machines={data!} ref={state} clusterName={props.currentCluster != null ? props.currentCluster.name : ""} />
<MachinesListComponent
machines={data!}
ref={state}
clusterName={props.currentCluster != null ? props.currentCluster.name : ""}
/>
</Stack>
</Stack>
</Stack>
)
}

Просмотреть файл

@ -1,24 +1,24 @@
import { useState, useEffect, useRef } from "react"
import { AxiosResponse } from 'axios';
import { FetchNodes } from '../Request';
import { AxiosResponse } from "axios"
import { fetchNodes } from "../Request"
import { ICluster } from "../App"
import { NodesListComponent } from './NodesList';
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
import { nodesKey } from "../ClusterDetail";
import { NodesListComponent } from "./NodesList"
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from "@fluentui/react"
import { nodesKey } from "../ClusterDetail"
export interface ICondition {
status: string,
lastHeartbeatTime: string,
lastTransitionTime: string,
status: string
lastHeartbeatTime: string
lastTransitionTime: string
message: string
}
export interface ITaint {
key: string,
key: string
}
export interface IVolume {
Path: string,
Path: string
}
export interface IResourceUsage {
@ -29,11 +29,11 @@ export interface IResourceUsage {
}
export interface INode {
name: string,
createdTime: string,
capacity: IResourceUsage,
name: string
createdTime: string
capacity: IResourceUsage
allocatable: IResourceUsage
conditions?: ICondition[],
conditions?: ICondition[]
taints?: ITaint[]
labels?: Map<string, string>
annotations?: Map<string, string>
@ -52,7 +52,7 @@ export function NodesWrapper(props: {
const [data, setData] = useState<any>([])
const [error, setError] = useState<AxiosResponse | null>(null)
const state = useRef<NodesListComponent>(null)
const [fetching, setFetching] = useState("")
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
@ -64,8 +64,7 @@ export function NodesWrapper(props: {
isMultiline={false}
onDismiss={() => setError(null)}
dismissButtonAriaLabel="Close"
styles={errorBarStyles}
>
styles={errorBarStyles}>
{error?.statusText}
</MessageBar>
)
@ -78,43 +77,47 @@ export function NodesWrapper(props: {
setData(newData)
const nodeList: INode[] = []
if (state && state.current) {
newData.nodes.forEach((element: { name: any;
createdTime: any;
capacity: any;
allocatable: any;
taints: ITaint[],
conditions: ICondition[],
labels: Record<string, string>,
annotations: Record<string, string>,
volumes: IVolume[]}) => {
const node: INode = {
name: element.name,
createdTime: element.createdTime,
capacity: element.capacity,
allocatable: element.allocatable,
}
node.taints = []
element.taints.forEach((taint: ITaint) => {
node.taints!.push(taint)
});
node.conditions = []
element.conditions.forEach((condition: ICondition) => {
node.conditions!.push(condition)
});
node.labels = new Map([])
Object.entries(element.labels).forEach((label: [string, string]) => {
newData.nodes.forEach(
(element: {
name: any
createdTime: any
capacity: any
allocatable: any
taints: ITaint[]
conditions: ICondition[]
labels: Record<string, string>
annotations: Record<string, string>
volumes: IVolume[]
}) => {
const node: INode = {
name: element.name,
createdTime: element.createdTime,
capacity: element.capacity,
allocatable: element.allocatable,
}
node.taints = []
element.taints.forEach((taint: ITaint) => {
node.taints!.push(taint)
})
node.conditions = []
element.conditions.forEach((condition: ICondition) => {
node.conditions!.push(condition)
})
node.labels = new Map([])
Object.entries(element.labels).forEach((label: [string, string]) => {
node.labels?.set(label[0], label[1])
});
node.volumes = []
element.volumes.forEach((volume: IVolume) => {
node.volumes!.push(volume)
});
node.annotations = new Map([])
Object.entries(element.annotations).forEach((annotation: [string, string]) => {
node.annotations?.set(annotation[0], annotation[1])
});
nodeList.push(node)
});
})
node.volumes = []
element.volumes.forEach((volume: IVolume) => {
node.volumes!.push(volume)
})
node.annotations = new Map([])
Object.entries(element.annotations).forEach((annotation: [string, string]) => {
node.annotations?.set(annotation[0], annotation[1])
})
nodeList.push(node)
}
)
state.current.setState({ nodes: nodeList })
}
}
@ -129,21 +132,27 @@ export function NodesWrapper(props: {
setFetching(props.currentCluster.name)
}
if (props.detailPanelSelected.toLowerCase() == nodesKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != "") {
if (
props.detailPanelSelected.toLowerCase() == nodesKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != ""
) {
setFetching("FETCHING")
FetchNodes(props.currentCluster).then(onData)
fetchNodes(props.currentCluster).then(onData)
}
}, [data, props.loaded, props.detailPanelSelected])
return (
<Stack>
<Stack.Item grow>{error && errorBar()}</Stack.Item>
<Stack>
<NodesListComponent nodes={data!} ref={state} clusterName={props.currentCluster != null ? props.currentCluster.name : ""} />
<NodesListComponent
nodes={data!}
ref={state}
clusterName={props.currentCluster != null ? props.currentCluster.name : ""}
/>
</Stack>
</Stack>
</Stack>
)
}

Просмотреть файл

@ -1,11 +1,11 @@
import { useState, useEffect, useRef } from "react"
import { AxiosResponse } from 'axios';
import { FetchClusterInfo } from '../Request';
import { AxiosResponse } from "axios"
import { fetchClusterInfo } from "../Request"
import { ICluster } from "../App"
import { ClusterDetailComponent } from '../ClusterDetailList'
import { OverviewComponent } from './Overview';
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
import { overviewKey } from "../ClusterDetail";
import { ClusterDetailComponent } from "../ClusterDetailList"
import { OverviewComponent } from "./Overview"
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from "@fluentui/react"
import { overviewKey } from "../ClusterDetail"
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
@ -27,8 +27,7 @@ export function OverviewWrapper(props: {
isMultiline={false}
onDismiss={() => setError(null)}
dismissButtonAriaLabel="Close"
styles={errorBarStyles}
>
styles={errorBarStyles}>
{error?.statusText}
</MessageBar>
)
@ -54,12 +53,14 @@ export function OverviewWrapper(props: {
setFetching(props.currentCluster.name)
}
if (props.detailPanelSelected.toLowerCase() == overviewKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != "") {
if (
props.detailPanelSelected.toLowerCase() == overviewKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != ""
) {
setFetching("FETCHING")
FetchClusterInfo(props.currentCluster).then(onData)
fetchClusterInfo(props.currentCluster).then(onData)
}
}, [data, props.loaded, props.clusterName])
@ -67,8 +68,11 @@ export function OverviewWrapper(props: {
<Stack>
<Stack.Item grow>{error && errorBar()}</Stack.Item>
<Stack>
<OverviewComponent item={data} clusterName={props.currentCluster != null ? props.currentCluster.name : ""}/>
<OverviewComponent
item={data}
clusterName={props.currentCluster != null ? props.currentCluster.name : ""}
/>
</Stack>
</Stack>
</Stack>
)
}

Просмотреть файл

@ -0,0 +1,177 @@
import { TimePicker, IComboBox, DatePicker, IDatePickerStyles, Stack, Text, TooltipHost, IStackStyles, IconButton, mergeStyleSets } from "@fluentui/react"
import { iconButtonStyles } from "./Statistics"
export function GraphOptionsComponent(props: {duration: string, setDuration: React.Dispatch<React.SetStateAction<string>> , endDate: Date, setEndDate: React.Dispatch<React.SetStateAction<Date>>}): any {
const calculatorAddition = { iconName: "CalculatorAddition" }
const calculatorSubtract = { iconName: "CalculatorSubtract" }
const _increaseDuration = () => {
let setToDuration = getIncreaseDurationMap().get(props.duration)
if (setToDuration != undefined) {
props.setDuration(setToDuration)
}
}
const _decreaseDuration = () => {
let setToDuration = getDecreaseDurationMap().get(props.duration)
if (setToDuration != undefined) {
props.setDuration(setToDuration)
}
}
const durationStyle: Partial<IStackStyles> = {
root: {
alignSelf: "flex-start",
border: "2px",
marginLeft: "3px",
marginRight: "3px",
},
}
const dateTimePickerStyles = mergeStyleSets({
iconContainer: {
marginLeft: 5,
},
})
const classNames = mergeStyleSets({
iconContainer: {
margin: "0px 0px",
height: 25,
width: "90px",
},
})
const datePickerStyles: Partial<IDatePickerStyles> = { root: { maxWidth: 145, marginTop: 5, } };
const timePickerStyles: Partial<IDatePickerStyles> = { root: { maxWidth: 75, marginLeft: 5, marginRight: 5 } };
const onTimeChange = (event: React.FormEvent<IComboBox>, date: Date) => {
var localDate = new Date()
localDate.setUTCFullYear(props.endDate.getFullYear(), props.endDate.getMonth(), props.endDate.getDate())
localDate.setUTCHours(date.getHours());
localDate.setUTCMinutes(date.getMinutes());
props.setEndDate(localDate)
};
const onDateChange = (date: Date | null | undefined): void => {
let localDate: Date = new Date();
localDate.setUTCFullYear(date!.getFullYear(), date!.getMonth(), date!.getDate())
localDate.setHours(props.endDate.getHours());
localDate.setMinutes(props.endDate.getMinutes());
props.setEndDate(localDate)
};
return (
<Stack horizontal verticalAlign="center">
<Stack horizontal verticalAlign="center" className={classNames.iconContainer} /* style={{ boxShadow: theme.effects.elevation8 }}*/>
<TooltipHost>
<IconButton
onClick={_decreaseDuration}
iconProps={calculatorSubtract}
styles={iconButtonStyles}
/>
</TooltipHost>
<TooltipHost>
<Text styles={durationStyle}>{props.duration}</Text>
</TooltipHost>
<TooltipHost >
<IconButton
onClick={_increaseDuration}
iconProps={calculatorAddition}
styles={iconButtonStyles}
/>
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center" className={dateTimePickerStyles.iconContainer} /* style={{ boxShadow: theme.effects.elevation8 }}*/>
<DatePicker
styles={datePickerStyles}
placeholder="End Date"
ariaLabel="End Date"
onSelectDate={onDateChange}
value={convertToUTC(props.endDate)}
allowTextInput
/>
<TimePicker
styles={timePickerStyles}
allowFreeform
placeholder={timeToString(convertToUTC(props.endDate))}
autoComplete="on"
onChange={onTimeChange}
defaultValue={convertToUTC(props.endDate)}
useComboBoxAsMenuWidth
/>
</Stack>
</Stack>
)
}
function timeToString(date: Date): string {
var str
let hourString = date.getHours().toString()
str = hourString + ":"
if (hourString.length === 1) {
str = "0" + hourString + ":"
}
let minuteString = date.getMinutes().toString()
if (minuteString.length === 1) {
str = str + "0"
}
str += minuteString
return str
}
export const convertToUTC = (date: Date): Date => {
let localDate = new Date()
localDate.setFullYear(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
localDate.setHours(date.getUTCHours())
localDate.setMinutes(date.getUTCMinutes())
return localDate
}
export const convertTimeToHours = (duration: string): string => {
let durationMap = new Map<string, string>()
durationMap.set("1d", "24h")
durationMap.set("2d", "48h")
durationMap.set("1w", "168h")
durationMap.set("2w", "336h")
durationMap.set("4w", "672h")
durationMap.set("8w", "1344h")
if (durationMap.has(duration)) {
return durationMap.get(duration)!
}
return duration
}
const getIncreaseDurationMap = (): Map<string, string> => {
let increaseDurationMap = new Map<string, string>()
increaseDurationMap.set("1m", "5m")
increaseDurationMap.set("5m", "10m")
increaseDurationMap.set("10m", "30m")
increaseDurationMap.set("30m", "1h")
increaseDurationMap.set("1h", "2h")
increaseDurationMap.set("2h", "6h")
increaseDurationMap.set("6h", "12h")
increaseDurationMap.set("12h", "1d")
increaseDurationMap.set("1d", "2d")
increaseDurationMap.set("2d", "1w")
increaseDurationMap.set("1w", "2w")
increaseDurationMap.set("2w", "4w")
increaseDurationMap.set("4w", "8w")
return increaseDurationMap
}
const getDecreaseDurationMap = (): Map<string, string> => {
let decreaseDurationMap = new Map<string, string>()
decreaseDurationMap.set("8w", "4w")
decreaseDurationMap.set("4w", "2w")
decreaseDurationMap.set("2w", "1w")
decreaseDurationMap.set("1w", "2d")
decreaseDurationMap.set("2d", "1d")
decreaseDurationMap.set("1d", "12h")
decreaseDurationMap.set("12h", "6h")
decreaseDurationMap.set("6h", "2h")
decreaseDurationMap.set("2h", "1h")
decreaseDurationMap.set("1h", "30m")
decreaseDurationMap.set("30m", "10m")
decreaseDurationMap.set("10m", "5m")
decreaseDurationMap.set("5m", "1m")
return decreaseDurationMap
}

Просмотреть файл

@ -0,0 +1,278 @@
import { useState } from "react"
import { ICluster } from "../../App"
import {
Modal,
Stack,
Text,
IStackStyles,
IconButton,
mergeStyleSets,
getTheme,
Label,
ThemeProvider,
IStackTokens,
PartialTheme,
DefaultPalette,
} from "@fluentui/react"
import { useBoolean } from "@fluentui/react-hooks"
import { StatisticsWrapper } from "./StatisticsWrapper"
import { GraphOptionsComponent } from "./GraphOptionsComponent"
export const iconButtonStyles = mergeStyleSets({
icon: {
color: "white",
},
root: {
selectors: {
":hover .ms-Button-icon": {
color: DefaultPalette.accent,
},
},
},
})
const global = new Date()
export function Statistics(props: {
currentCluster: ICluster
detailPanelSelected: string
loaded: boolean
statisticsType: string
}) {
const [globalDuration, setGlobalDuration] = useState<string>("1h")
const [globalEndDate, setGlobalEndDate] = useState<Date>(global)
function GlobalGraphOptionsBar() {
const stackStyles: IStackStyles = {
root: [
{
width: "100%",
padding: 0,
},
],
}
const stackNavStyles: IStackStyles = {
root: {
padding: "0px 15px",
height: 40,
},
}
const containerStackTokens: IStackTokens = {}
const appStackTokens: IStackTokens = { childrenGap: 10 }
return (
<Stack styles={stackStyles} tokens={containerStackTokens} horizontalAlign={"stretch"}>
<ThemeProvider theme={darkTheme}>
<Stack
grow
tokens={appStackTokens}
horizontalAlign={"start"}
verticalAlign={"center"}
horizontal
styles={stackNavStyles}>
<Stack.Item grow>
<Text>{"Global Graph Options"}</Text>
</Stack.Item>
<Stack.Item>
<GraphOptionsComponent
duration={globalDuration}
setDuration={setGlobalDuration}
endDate={globalEndDate}
setEndDate={setGlobalEndDate}
/>
</Stack.Item>
</Stack>
</ThemeProvider>
</Stack>
)
}
const darkTheme: PartialTheme = {
semanticColors: {
bodyBackground: DefaultPalette.accent,
bodyText: DefaultPalette.white,
},
defaultFontStyle: {
fontWeight: 500,
},
}
const theme = getTheme()
function GraphWrapper(lprops: { heading: string; statisticsName: string }) {
const [isModalOpen, { setTrue: showModal, setFalse: hideModal }] = useBoolean(false)
const [duration, setDuration] = useState<string>(globalDuration)
const [endDate, setEndDate] = useState<Date>(globalEndDate)
return (
<>
<Modal
titleAriaId={lprops.heading}
isOpen={isModalOpen}
onDismiss={hideModal}
isBlocking={false}>
<Stack
style={{ boxShadow: theme.effects.elevation8 }}
styles={{ root: { margin: "2px" } }}>
<ThemeProvider theme={darkTheme}>
<Stack
horizontalAlign="stretch"
horizontal /*className={classNames.iconContainer} /*style={{ boxShadow: theme.effects.elevation64 }}*/
>
<Stack.Item grow={0.5}>
<GraphOptionsComponent
duration={duration}
setDuration={setDuration}
endDate={endDate}
setEndDate={setEndDate}
/>
</Stack.Item>
<Stack.Item align="center" grow={1}>
<Label> {lprops.heading} </Label>
</Stack.Item>
<Stack.Item align="center">
<IconButton
iconProps={{ iconName: "Cancel" }}
ariaLabel="Close popup modal"
onClick={hideModal}
styles={iconButtonStyles}
/>
</Stack.Item>
</Stack>
</ThemeProvider>
<StatisticsWrapper
currentCluster={props.currentCluster}
detailPanelSelected={props.detailPanelSelected}
loaded={props.loaded}
statisticsName={lprops.statisticsName}
duration={duration}
endDate={endDate}
graphHeight={500}
graphWidth={1500}
/>
</Stack>
</Modal>
<Stack style={{ boxShadow: theme.effects.elevation8 }} styles={{ root: { margin: "2px" } }}>
<ThemeProvider theme={darkTheme}>
<Stack horizontalAlign="stretch" horizontal>
<Stack.Item align="center">
<IconButton
onClick={showModal}
iconProps={{ iconName: "FullScreen" }}
styles={iconButtonStyles}
/>
</Stack.Item>
<Stack.Item align="center" grow={1}>
<Text> {lprops.heading} </Text>
</Stack.Item>
<Stack.Item>
<GraphOptionsComponent
duration={duration}
setDuration={setDuration}
endDate={endDate}
setEndDate={setEndDate}
/>
</Stack.Item>
</Stack>
</ThemeProvider>
<StatisticsWrapper
currentCluster={props.currentCluster}
detailPanelSelected={props.detailPanelSelected}
loaded={props.loaded}
statisticsName={lprops.statisticsName}
duration={duration}
endDate={endDate}
graphHeight={200}
graphWidth={740}
/>
</Stack>
</>
)
}
interface Map {
[key: string]: {
stasticsName: string
heading: string
}[]
}
let statisticsDataMap: Map = {
api: [
{ stasticsName: "kubeapicodes", heading: "KubeAPI Server response sizes by code and verb" },
{ stasticsName: "kubeapicpu", heading: "KubeAPI CPU per instance" },
{ stasticsName: "kubeapimemory", heading: "KubeAPI Memory per instance" },
],
kcm: [
{
stasticsName: "kubecontrollermanagercodes",
heading: "Kube Controller Manager Server response sizes by code and verb",
},
{
stasticsName: "kubecontrollermanagercpu",
heading: "Kube Controller Manager CPU per instance",
},
{
stasticsName: "kubecontrollermanagermemory",
heading: "Kube Controller Manager Memory per instance",
},
],
dns: [
{
stasticsName: "dnsresponsecodes",
heading: "Response Codes",
},
{
stasticsName: "dnsalltraffic",
heading: "All Traffic",
},
{
stasticsName: "dnserrorrate",
heading: "Error Rate",
},
{
stasticsName: "dnshealthcheck",
heading: "Health Check",
},
{
stasticsName: "dnsforwardedtraffic",
heading: "Forwarded Traffic",
},
],
ingress: [
{
stasticsName: "ingresscontrollercondition",
heading: "Ingress Controller Condition",
},
],
}
const statisticsJSX = (sDataMap: Map): JSX.Element[] => {
let sDataArray = sDataMap[props.statisticsType]
let sDataArrayLength = sDataArray.length
let stackItems: JSX.Element[] = []
let stacks: JSX.Element[] = []
sDataArray.map((sData, i) => {
if (i % 2 != 0 || i === sDataArrayLength - 1) {
stackItems.push(
<Stack.Item>
<GraphWrapper statisticsName={sData.stasticsName} heading={sData.heading} />
</Stack.Item>
)
stacks.push(<Stack horizontal>{stackItems}</Stack>)
stackItems = []
return
}
stackItems.push(
<Stack.Item>
<GraphWrapper statisticsName={sData.stasticsName} heading={sData.heading} />
</Stack.Item>
)
})
return stacks
}
return (
<>
<GlobalGraphOptionsBar />
{statisticsJSX(statisticsDataMap)}
</>
)
}

Просмотреть файл

@ -0,0 +1,168 @@
import { useEffect, useState } from "react"
import { Stack, StackItem, IStackProps } from "@fluentui/react"
import { Spinner, SpinnerSize } from "@fluentui/react/lib/Spinner"
import {
ILineChartPoints,
ILegendsProps,
IChartProps,
LineChart,
ILineChartDataPoint,
ILineChartProps,
} from "@fluentui/react-charting"
import { DefaultPalette } from "@fluentui/react/lib/Styling"
import { IMetrics } from "./StatisticsWrapper"
import { convertToUTC } from "./GraphOptionsComponent"
export function StatisticsComponent(props: {
metrics: IMetrics[]
clusterName: any
duration: string
height: number
width: number
endDate: Date
fetchStatus: string
}) {
const width = props.width
const height = props.height
const [points, setPoints] = useState<ILineChartPoints[]>([])
const [data, setData] = useState<IChartProps>({})
const [spinnerVisible, setSpinnerVisible] = useState<boolean>(true)
const timeFormat = "%H:%M"
const colors: string[] = [
DefaultPalette.blue,
DefaultPalette.blueLight,
DefaultPalette.blueDark,
DefaultPalette.blueMid,
DefaultPalette.black,
DefaultPalette.red,
DefaultPalette.redDark,
DefaultPalette.yellow,
DefaultPalette.yellowDark,
DefaultPalette.yellowLight,
DefaultPalette.green,
DefaultPalette.greenLight,
DefaultPalette.greenDark,
DefaultPalette.purple,
DefaultPalette.purpleLight,
DefaultPalette.purpleDark,
DefaultPalette.orange,
DefaultPalette.orangeLight,
DefaultPalette.orangeLighter,
DefaultPalette.magenta,
DefaultPalette.magentaDark,
DefaultPalette.magentaLight,
DefaultPalette.themePrimary,
DefaultPalette.neutralPrimary,
DefaultPalette.neutralDark,
DefaultPalette.neutralSecondary,
DefaultPalette.neutralTertiary,
DefaultPalette.teal,
DefaultPalette.tealDark,
DefaultPalette.tealLight,
DefaultPalette.accent,
DefaultPalette.themeDarker,
DefaultPalette.themeDarkAlt,
DefaultPalette.themeDark,
DefaultPalette.themeLight,
DefaultPalette.themeLighter,
DefaultPalette.themeLighterAlt,
DefaultPalette.themePrimary,
DefaultPalette.themeSecondary,
DefaultPalette.themeTertiary,
]
function StatisticsHelperComponent(): JSX.Element {
useEffect(() => {
if (props.fetchStatus === "error") {
setSpinnerVisible(false)
return
}
const newPoints: ILineChartPoints[] = []
props.metrics.forEach((metric, i) => {
var dataPoints: ILineChartDataPoint[] = []
metric.MetricValue.forEach((metricValue) => {
let timeStamp = new Date(metricValue.timestamp)
let metricsTime = convertToUTC(timeStamp)
var data: ILineChartDataPoint = {
x: metricsTime,
y: metricValue.value,
}
dataPoints.push(data)
})
var lineChartPoint: ILineChartPoints = {
legend: metric.Name,
data: dataPoints,
color: colors[i],
}
newPoints.push(lineChartPoint)
})
setPoints(newPoints)
}, [props.metrics, props.fetchStatus])
useEffect(() => {
setData({
chartTitle: "Line Chart",
lineChartData: points,
})
props.fetchStatus === "success" ? setSpinnerVisible(false) : setSpinnerVisible(true)
}, [points])
useEffect(() => {
setSpinnerVisible(true)
}, [props.duration, props.endDate])
const rootStyle = { width: `${width}px`, height: `${height}px` }
const tokens = {
sectionStack: {
childrenGap: 10,
},
spinnerStack: {
childrenGap: 20,
},
}
const rowProps: IStackProps = { horizontal: false, verticalAlign: "center" }
const legendProps: Partial<ILegendsProps> = {
canSelectMultipleLegends: true,
allowFocusOnLegends: true,
}
let lineChartProps: ILineChartProps = {
data: data,
strokeWidth: 2,
tickFormat: timeFormat,
height: height,
width: width,
legendProps: legendProps,
}
const renderLineChart = (lineChartProps: ILineChartProps) => {
return (
<div style={rootStyle}>
<LineChart {...lineChartProps} />
</div>
)
}
return (
<Stack>
<StackItem>
{spinnerVisible ? (
<Stack {...rowProps} tokens={tokens.spinnerStack}>
<StackItem>
<Spinner size={SpinnerSize.large} />
</StackItem>
{renderLineChart(lineChartProps)}
</Stack>
) : (
renderLineChart(lineChartProps)
)}
</StackItem>
</Stack>
)
}
return (
<>
<div>{StatisticsHelperComponent()}</div>
</>
)
}

Просмотреть файл

@ -0,0 +1,121 @@
import { useState, useEffect } from "react"
import { AxiosResponse } from "axios"
import { ICluster } from "../../App"
import { StatisticsComponent } from "./StatisticsComponent"
import { fetchStatistics } from "../../Request"
import {
apiStatisticsKey,
dnsStatisticsKey,
ingressStatisticsKey,
kcmStatisticsKey,
} from "../../ClusterDetail"
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from "@fluentui/react"
export interface IMetricValue {
timestamp: Date
value: number
}
export interface IMetrics {
Name: string
MetricValue: IMetricValue[]
}
export function StatisticsWrapper(props: {
currentCluster: ICluster
detailPanelSelected: string
loaded: boolean
statisticsName: string
duration: string
endDate: Date
graphHeight: number
graphWidth: number
}) {
const [error, setError] = useState<AxiosResponse | null>(null)
const [metrics, setMetrics] = useState<IMetrics[]>([])
const [fetching, setFetching] = useState("")
const [localDuration, setLocalDuration] = useState(props.duration)
const [localEndDate, setLocalEndDate] = useState(props.endDate)
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
const statisticsKeys = [
apiStatisticsKey,
dnsStatisticsKey,
ingressStatisticsKey,
kcmStatisticsKey,
]
const errorBar = (): any => {
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={false}
onDismiss={() => setError(null)}
dismissButtonAriaLabel="Close"
styles={errorBarStyles}>
{error?.statusText}
</MessageBar>
)
}
// updateData - updates the state of the component
// can be used if we want a refresh button.
// api/clusterdetail returns a single item.
const updateData = (newData: any) => {
const metrics: IMetrics[] = []
newData.forEach((element: { metricname: any; metricvalue: IMetricValue[] }) => {
const metric: IMetrics = {
Name: element.metricname,
MetricValue: element.metricvalue,
}
metrics.push(metric)
})
setMetrics(metrics)
}
useEffect(() => {
const onData = (result: AxiosResponse | null) => {
if (result?.status === 200) {
setFetching("success")
updateData(result.data)
setError(null)
} else {
setError(result)
setFetching("error")
}
}
if (
statisticsKeys.includes(props.detailPanelSelected.toLowerCase()) &&
(fetching === "" || localDuration != props.duration || localEndDate != props.endDate) &&
props.loaded &&
props.currentCluster.name != ""
) {
setLocalDuration(props.duration)
setLocalEndDate(props.endDate)
setFetching("FETCHING")
fetchStatistics(
props.currentCluster,
props.statisticsName,
props.duration,
props.endDate
).then(onData)
}
}, [props.loaded, props.detailPanelSelected, props.duration, props.endDate])
return (
<Stack>
<Stack.Item grow>{error && errorBar()}</Stack.Item>
<Stack>
<StatisticsComponent
metrics={metrics}
fetchStatus={fetching}
duration={props.duration}
clusterName={props.currentCluster != null ? props.currentCluster.name : ""}
height={props.graphHeight}
width={props.graphWidth}
endDate={props.endDate}
/>
</Stack>
</Stack>
)
}

Просмотреть файл

@ -26,7 +26,7 @@ import {
IDetailsListStyles,
} from "@fluentui/react/lib/DetailsList"
import { useBoolean } from "@fluentui/react-hooks"
import { FetchClusters } from "./Request"
import { fetchClusters } from "./Request"
import { KubeconfigButton } from "./Kubeconfig"
import { AxiosResponse } from "axios"
import { ICluster, headerStyles } from "./App"
@ -78,55 +78,53 @@ const separatorStyle = {
const popupStyles = mergeStyleSets({
root: {
background: 'rgba(0, 0, 0, 0.2)',
bottom: '0',
left: '0',
position: 'fixed',
right: '0',
top: '0',
background: "rgba(0, 0, 0, 0.2)",
bottom: "0",
left: "0",
position: "fixed",
right: "0",
top: "0",
},
content: {
background: 'white',
left: '50%',
maxWidth: '400px',
padding: '0 2em 2em',
position: 'absolute',
top: '50%',
transform: 'translate(-50%, -50%)',
background: "white",
left: "50%",
maxWidth: "400px",
padding: "0 2em 2em",
position: "absolute",
top: "50%",
transform: "translate(-50%, -50%)",
},
});
})
const PopupModal = (props: {title: string, text: string, hidePopup: any}) => {
const PopupModal = (props: { title: string; text: string; hidePopup: any }) => {
return (
<>
<Layer>
<Popup
className={popupStyles.root}
role="dialog"
aria-modal="true"
onDismiss={props.hidePopup}
enableAriaHiddenSiblings={true}
>
<FocusTrapZone>
<div role="document" className={popupStyles.content}>
<h2>{props.title}</h2>
<p>
{props.text}
</p>
<DefaultButton onClick={() => {
<Layer>
<Popup
className={popupStyles.root}
role="dialog"
aria-modal="true"
onDismiss={props.hidePopup}
enableAriaHiddenSiblings={true}>
<FocusTrapZone>
<div role="document" className={popupStyles.content}>
<h2>{props.title}</h2>
<p>{props.text}</p>
<DefaultButton
onClick={() => {
// this is to change the URL in the address bar
window.history.replaceState({}, "", "/v2")
props.hidePopup()
}}>
Close
</DefaultButton>
</div>
</FocusTrapZone>
</Popup>
</Layer>
Close
</DefaultButton>
</div>
</FocusTrapZone>
</Popup>
</Layer>
</>
);
};
)
}
interface IClusterListState {
columns: IColumn[]
@ -301,7 +299,9 @@ class ClusterListComponent extends Component<ClusterListComponentProps, ICluster
<IconButton
iconProps={{ iconName: "BIDashboard" }}
aria-label="Prometheus"
href={item.resourceId + (+item.version >= 4.11 ? `/prometheus` : `/prometheus/graph`)}
href={
item.resourceId + (+item.version >= 4.11 ? `/prometheus` : `/prometheus/graph`)
}
/>
</TooltipHost>
<TooltipHost content={`SSH`}>
@ -346,7 +346,9 @@ class ClusterListComponent extends Component<ClusterListComponentProps, ICluster
<div className={classNames.controlWrapper}>
<TextField placeholder="Filter on resource ID" onChange={this._onChangeText} />
</div>
<Text id="ClusterCount" className={classNames.itemsCount}>Showing {items.length} items</Text>
<Text id="ClusterCount" className={classNames.itemsCount}>
Showing {items.length} items
</Text>
<DetailsList
items={items}
columns={columns}
@ -372,7 +374,9 @@ class ClusterListComponent extends Component<ClusterListComponentProps, ICluster
): void => {
this.setState({
items: text
? this.props.items.filter((i) => i.resourceId.toLowerCase().indexOf(text.trim().toLowerCase()) != -1)
? this.props.items.filter(
(i) => i.resourceId.toLowerCase().indexOf(text.trim().toLowerCase()) != -1
)
: this.props.items,
})
}
@ -425,7 +429,7 @@ export function ClusterList(props: {
}) {
const [data, setData] = useState<any>([])
const [error, setError] = useState<AxiosResponse | null>(null)
const [isPopupVisible, { setTrue: showPopup, setFalse: hidePopup }] = useBoolean(false);
const [isPopupVisible, { setTrue: showPopup, setFalse: hidePopup }] = useBoolean(false)
const state = useRef<ClusterListComponent>(null)
const [fetching, setFetching] = useState("")
@ -463,13 +467,15 @@ export function ClusterList(props: {
if (fetching === "" && props.csrfTokenAvailable === "DONE") {
setFetching("FETCHING")
FetchClusters().then(onData)
fetchClusters().then(onData)
}
if (props.params) {
const resourceID: string = props.params["resourceid"]
const clusterList = data as ICluster[]
const currentCluster = clusterList.find((item): item is ICluster => resourceID === item.resourceId)
const currentCluster = clusterList.find(
(item): item is ICluster => resourceID === item.resourceId
)
if (fetching === "DONE" && !currentCluster) {
showPopup()
@ -478,7 +484,6 @@ export function ClusterList(props: {
props.setCurrentCluster(currentCluster)
}
}, [data, fetching, setFetching, props.csrfTokenAvailable])
const _items: ICommandBarItemProps[] = [
@ -504,10 +509,15 @@ export function ClusterList(props: {
styles={controlStyles}
/>
<Separator styles={separatorStyle} />
{error && errorBar()}
{isPopupVisible && PopupModal({title: "Resource Not Found", text: "No resource found due to Invalid/Non-existent resource ID in the URL.", hidePopup: hidePopup})}
{isPopupVisible &&
PopupModal({
title: "Resource Not Found",
text: "No resource found due to Invalid/Non-existent resource ID in the URL.",
hidePopup: hidePopup,
})}
<ClusterListComponent
items={data}

Просмотреть файл

@ -1,5 +1,6 @@
import axios, { AxiosResponse } from "axios"
import { ICluster } from "./App"
import { convertTimeToHours } from "./ClusterDetailListComponents/Statistics/GraphOptionsComponent"
const OnError = (err: AxiosResponse): AxiosResponse | null => {
if (err.status === 403) {
@ -10,7 +11,7 @@ const OnError = (err: AxiosResponse): AxiosResponse | null => {
}
}
export const FetchClusters = async (): Promise<AxiosResponse | null> => {
export const fetchClusters = async (): Promise<AxiosResponse | null> => {
try {
const result = await axios("/api/clusters")
return result
@ -20,7 +21,7 @@ export const FetchClusters = async (): Promise<AxiosResponse | null> => {
}
}
export const FetchClusterInfo = async (cluster: ICluster): Promise<AxiosResponse | null> => {
export const fetchClusterInfo = async (cluster: ICluster): Promise<AxiosResponse | null> => {
try {
const result = await axios(
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name
@ -32,7 +33,7 @@ export const FetchClusterInfo = async (cluster: ICluster): Promise<AxiosResponse
}
}
export const FetchInfo = async (): Promise<AxiosResponse | null> => {
export const fetchInfo = async (): Promise<AxiosResponse | null> => {
try {
const result = await axios("/api/info")
return result
@ -42,10 +43,11 @@ export const FetchInfo = async (): Promise<AxiosResponse | null> => {
}
}
export const FetchNodes = async (cluster: ICluster): Promise<AxiosResponse | null> => {
export const fetchNodes = async (cluster: ICluster): Promise<AxiosResponse | null> => {
try {
const result = await axios(
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name + "/nodes")
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name + "/nodes"
)
return result
} catch (e: any) {
const err = e.response as AxiosResponse
@ -53,10 +55,17 @@ export const FetchNodes = async (cluster: ICluster): Promise<AxiosResponse | nul
}
}
export const FetchMachines = async (cluster: ICluster): Promise<AxiosResponse | null> => {
export const fetchMachines = async (cluster: ICluster): Promise<AxiosResponse | null> => {
try {
const result = await axios(
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name + "/machines")
"/api/" +
cluster.subscription +
"/" +
cluster.resourceGroup +
"/" +
cluster.name +
"/machines"
)
return result
} catch (e: any) {
const err = e.response as AxiosResponse
@ -64,10 +73,17 @@ export const FetchMachines = async (cluster: ICluster): Promise<AxiosResponse |
}
}
export const FetchMachineSets = async (cluster: ICluster): Promise<AxiosResponse | null> => {
export const fetchMachineSets = async (cluster: ICluster): Promise<AxiosResponse | null> => {
try {
const result = await axios(
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name + "/machine-sets")
"/api/" +
cluster.subscription +
"/" +
cluster.resourceGroup +
"/" +
cluster.name +
"/machine-sets"
)
return result
} catch (e: any) {
const err = e.response as AxiosResponse
@ -75,7 +91,7 @@ export const FetchMachineSets = async (cluster: ICluster): Promise<AxiosResponse
}
}
export const FetchRegions = async (): Promise<AxiosResponse | null> => {
export const fetchRegions = async (): Promise<AxiosResponse | null> => {
try {
const result = await axios("/api/regions")
return result
@ -87,7 +103,7 @@ export const FetchRegions = async (): Promise<AxiosResponse | null> => {
export const ProcessLogOut = async (): Promise<any> => {
try {
const result = await axios({method: "POST", url: "/api/logout"})
const result = await axios({ method: "POST", url: "/api/logout" })
return result
} catch (e: any) {
const err = e.response as AxiosResponse
@ -114,3 +130,22 @@ export const RequestKubeconfig = async (
return OnError(err)
}
}
export const fetchStatistics = async (
cluster: ICluster,
statisticsName: string,
duration: string,
endDate: Date
): Promise<AxiosResponse | null> => {
duration = convertTimeToHours(duration)
let endDateJSON = endDate.toJSON()
try {
const result = await axios(
`/api/${cluster.subscription}/${cluster.resourceGroup}/${cluster.name}/statistics/${statisticsName}?duration=${duration}&endtime=${endDateJSON}`
)
return result
} catch (e: any) {
const err = e.response as AxiosResponse
return OnError(err)
}
}

131
vendor/github.com/prometheus/client_golang/api/client.go сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,131 @@
// Copyright 2015 The Prometheus Authors
// 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 api provides clients for the HTTP APIs.
package api
import (
"bytes"
"context"
"net"
"net/http"
"net/url"
"path"
"strings"
"time"
)
// DefaultRoundTripper is used if no RoundTripper is set in Config.
var DefaultRoundTripper http.RoundTripper = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
}
// Config defines configuration parameters for a new client.
type Config struct {
// The address of the Prometheus to connect to.
Address string
// RoundTripper is used by the Client to drive HTTP requests. If not
// provided, DefaultRoundTripper will be used.
RoundTripper http.RoundTripper
}
func (cfg *Config) roundTripper() http.RoundTripper {
if cfg.RoundTripper == nil {
return DefaultRoundTripper
}
return cfg.RoundTripper
}
// Client is the interface for an API client.
type Client interface {
URL(ep string, args map[string]string) *url.URL
Do(context.Context, *http.Request) (*http.Response, []byte, error)
}
// NewClient returns a new Client.
//
// It is safe to use the returned Client from multiple goroutines.
func NewClient(cfg Config) (Client, error) {
u, err := url.Parse(cfg.Address)
if err != nil {
return nil, err
}
u.Path = strings.TrimRight(u.Path, "/")
return &httpClient{
endpoint: u,
client: http.Client{Transport: cfg.roundTripper()},
}, nil
}
type httpClient struct {
endpoint *url.URL
client http.Client
}
func (c *httpClient) URL(ep string, args map[string]string) *url.URL {
p := path.Join(c.endpoint.Path, ep)
for arg, val := range args {
arg = ":" + arg
p = strings.Replace(p, arg, val, -1)
}
u := *c.endpoint
u.Path = p
return &u
}
func (c *httpClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
if ctx != nil {
req = req.WithContext(ctx)
}
resp, err := c.client.Do(req)
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
if err != nil {
return nil, nil, err
}
var body []byte
done := make(chan struct{})
go func() {
var buf bytes.Buffer
_, err = buf.ReadFrom(resp.Body)
body = buf.Bytes()
close(done)
}()
select {
case <-ctx.Done():
<-done
err = resp.Body.Close()
if err == nil {
err = ctx.Err()
}
case <-done:
}
return resp, body, err
}

1161
vendor/github.com/prometheus/client_golang/api/prometheus/v1/api.go сгенерированный поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

2
vendor/modules.txt поставляемый
Просмотреть файл

@ -1058,6 +1058,8 @@ github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/typed/mo
github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/typed/monitoring/v1alpha1
# github.com/prometheus/client_golang v1.12.1
## explicit; go 1.13
github.com/prometheus/client_golang/api
github.com/prometheus/client_golang/api/prometheus/v1
github.com/prometheus/client_golang/prometheus
github.com/prometheus/client_golang/prometheus/collectors
github.com/prometheus/client_golang/prometheus/internal