diff --git a/api/pkg/api/socket_server_handlers.go b/api/pkg/api/socket_server_handlers.go index 8164df004..1033dc7a8 100644 --- a/api/pkg/api/socket_server_handlers.go +++ b/api/pkg/api/socket_server_handlers.go @@ -9,6 +9,7 @@ import ( "mizuserver/pkg/controllers" "mizuserver/pkg/models" "mizuserver/pkg/routes" + "mizuserver/pkg/up9" ) var browserClientSocketUUIDs = make([]string, 0) @@ -18,6 +19,9 @@ type RoutesEventHandlers struct { SocketHarOutChannel chan<- *tap.OutputChannelItem } +func init() { + go up9.UpdateAnalyzeStatus(broadcastToBrowserClients) +} func (h *RoutesEventHandlers) WebSocketConnect(ep *ikisocket.EventPayload) { if ep.Kws.GetAttribute("is_tapper") == true { @@ -84,7 +88,6 @@ func (h *RoutesEventHandlers) WebSocketMessage(ep *ikisocket.EventPayload) { } } - func removeSocketUUIDFromBrowserSlice(uuidToRemove string) { newUUIDSlice := make([]string, 0, len(browserClientSocketUUIDs)) for _, uuid := range browserClientSocketUUIDs { diff --git a/api/pkg/controllers/entries_controller.go b/api/pkg/controllers/entries_controller.go index fa1306c7f..89dd450c2 100644 --- a/api/pkg/controllers/entries_controller.go +++ b/api/pkg/controllers/entries_controller.go @@ -1,41 +1,18 @@ package controllers import ( - "bytes" - "compress/zlib" "encoding/json" "fmt" "github.com/gofiber/fiber/v2" "github.com/google/martian/har" - "io/ioutil" - "log" "mizuserver/pkg/database" "mizuserver/pkg/models" + "mizuserver/pkg/up9" "mizuserver/pkg/utils" "mizuserver/pkg/validation" - "net/http" - "net/url" "time" ) -const ( - OrderDesc = "desc" - OrderAsc = "asc" - LT = "lt" - GT = "gt" -) - -var ( - operatorToSymbolMapping = map[string]string{ - LT: "<", - GT: ">", - } - operatorToOrderMapping = map[string]string{ - LT: OrderDesc, - GT: OrderAsc, - } -) - func GetEntries(c *fiber.Ctx) error { entriesFilter := &models.EntriesFilter{} @@ -47,8 +24,8 @@ func GetEntries(c *fiber.Ctx) error { return c.Status(fiber.StatusBadRequest).JSON(err) } - order := operatorToOrderMapping[entriesFilter.Operator] - operatorSymbol := operatorToSymbolMapping[entriesFilter.Operator] + order := database.OperatorToOrderMapping[entriesFilter.Operator] + operatorSymbol := database.OperatorToSymbolMapping[entriesFilter.Operator] var entries []models.MizuEntry database.GetEntriesTable(). Order(fmt.Sprintf("timestamp %s", order)). @@ -57,7 +34,7 @@ func GetEntries(c *fiber.Ctx) error { Limit(entriesFilter.Limit). Find(&entries) - if len(entries) > 0 && order == OrderDesc { + if len(entries) > 0 && order == database.OrderDesc { // the entries always order from oldest to newest so we should revers utils.ReverseSlice(entries) } @@ -73,7 +50,7 @@ func GetEntries(c *fiber.Ctx) error { func GetHARs(c *fiber.Ctx) error { entriesFilter := &models.HarFetchRequestBody{} - order := OrderDesc + order := database.OrderDesc if err := c.QueryParser(entriesFilter); err != nil { return c.Status(fiber.StatusBadRequest).JSON(err) } @@ -144,67 +121,20 @@ func GetHARs(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).SendStream(buffer) } -func uploadEntriesImpl(token string, model string, envPrefix string) { - sleepTime := time.Second * 10 - - var timestampFrom int64 = 0 - - for { - timestampTo := time.Now().UnixNano() / int64(time.Millisecond) - fmt.Printf("Getting entries from %v, to %v\n", timestampFrom, timestampTo) - entriesArray := getEntriesFromDb(timestampFrom, timestampTo) - - if len(entriesArray) > 0 { - fmt.Printf("About to upload %v entries\n", len(entriesArray)) - - body, jMarshalErr := json.Marshal(entriesArray) - if jMarshalErr != nil { - log.Fatal(jMarshalErr) - } - - var in bytes.Buffer - w := zlib.NewWriter(&in) - _, _ = w.Write(body) - _ = w.Close() - reqBody := ioutil.NopCloser(bytes.NewReader(in.Bytes())) - - postUrl, _ := url.Parse(fmt.Sprintf("https://traffic.%s/dumpTrafficBulk/%s", envPrefix, model)) - fmt.Println(postUrl) - req := &http.Request{ - Method: http.MethodPost, - URL: postUrl, - Header: map[string][]string{ - "Content-Encoding": {"deflate"}, - "Content-Type": {"application/octet-stream"}, - "Guest-Auth": {token}, - }, - Body: reqBody, - } - _, postErr := http.DefaultClient.Do(req) - if postErr != nil { - log.Fatal(postErr) - } - fmt.Printf("Finish uploading %v entries to %s\n", len(entriesArray), postUrl) - - } else { - fmt.Println("Nothing to upload") - } - - fmt.Printf("Sleeping for %v...\n", sleepTime) - time.Sleep(sleepTime) - timestampFrom = timestampTo - } -} - func UploadEntries(c *fiber.Ctx) error { - entriesFilter := &models.UploadEntriesRequestBody{} - if err := c.QueryParser(entriesFilter); err != nil { + uploadRequestBody := &models.UploadEntriesRequestBody{} + if err := c.QueryParser(uploadRequestBody); err != nil { return c.Status(fiber.StatusBadRequest).JSON(err) } - if err := validation.Validate(entriesFilter); err != nil { + if err := validation.Validate(uploadRequestBody); err != nil { return c.Status(fiber.StatusBadRequest).JSON(err) } - go uploadEntriesImpl(entriesFilter.Token, entriesFilter.Model, entriesFilter.Dest) + if up9.GetAnalyzeInfo().IsAnalyzing { + return c.Status(fiber.StatusBadRequest).SendString("Cannot analyze, mizu is already analyzing") + } + + token, _ := up9.CreateAnonymousToken(uploadRequestBody.Dest) + go up9.UploadEntriesImpl(token.Token, token.Model, uploadRequestBody.Dest) return c.Status(fiber.StatusOK).SendString("OK") } @@ -231,32 +161,10 @@ func GetFullEntries(c *fiber.Ctx) error { timestampTo = entriesFilter.To } - entriesArray := getEntriesFromDb(timestampFrom, timestampTo) + entriesArray := database.GetEntriesFromDb(timestampFrom, timestampTo) return c.Status(fiber.StatusOK).JSON(entriesArray) } -func getEntriesFromDb(timestampFrom int64, timestampTo int64) []har.Entry { - order := OrderDesc - var entries []models.MizuEntry - database.GetEntriesTable(). - Where(fmt.Sprintf("timestamp BETWEEN %v AND %v", timestampFrom, timestampTo)). - Order(fmt.Sprintf("timestamp %s", order)). - Find(&entries) - - if len(entries) > 0 { - // the entries always order from oldest to newest so we should revers - utils.ReverseSlice(entries) - } - - entriesArray := make([]har.Entry, 0) - for _, entryData := range entries { - var harEntry har.Entry - _ = json.Unmarshal([]byte(entryData.Entry), &harEntry) - entriesArray = append(entriesArray, harEntry) - } - return entriesArray -} - func GetEntry(c *fiber.Ctx) error { var entryData models.EntryData database.GetEntriesTable(). diff --git a/api/pkg/controllers/status_controller.go b/api/pkg/controllers/status_controller.go index 15f30a67b..e4fed4c55 100644 --- a/api/pkg/controllers/status_controller.go +++ b/api/pkg/controllers/status_controller.go @@ -3,6 +3,7 @@ package controllers import ( "github.com/gofiber/fiber/v2" "github.com/up9inc/mizu/shared" + "mizuserver/pkg/up9" ) var TapStatus shared.TapStatus @@ -10,3 +11,7 @@ var TapStatus shared.TapStatus func GetTappingStatus(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(TapStatus) } + +func AnalyzeInformation(c *fiber.Ctx) error { + return c.Status(fiber.StatusOK).JSON(up9.GetAnalyzeInfo()) +} diff --git a/api/pkg/database/main.go b/api/pkg/database/main.go index b015a0868..4ce4f07db 100644 --- a/api/pkg/database/main.go +++ b/api/pkg/database/main.go @@ -1,9 +1,13 @@ package database import ( + "encoding/json" + "fmt" + "github.com/google/martian/har" "gorm.io/driver/sqlite" "gorm.io/gorm" "mizuserver/pkg/models" + "mizuserver/pkg/utils" ) const ( @@ -14,6 +18,24 @@ var ( DB = initDataBase(DBPath) ) +const ( + OrderDesc = "desc" + OrderAsc = "asc" + LT = "lt" + GT = "gt" +) + +var ( + OperatorToSymbolMapping = map[string]string{ + LT: "<", + GT: ">", + } + OperatorToOrderMapping = map[string]string{ + LT: OrderDesc, + GT: OrderAsc, + } +) + func GetEntriesTable() *gorm.DB { return DB.Table("mizu_entries") } @@ -23,3 +45,26 @@ func initDataBase(databasePath string) *gorm.DB { _ = temp.AutoMigrate(&models.MizuEntry{}) // this will ensure table is created return temp } + +func GetEntriesFromDb(timestampFrom int64, timestampTo int64) []har.Entry { + order := OrderDesc + var entries []models.MizuEntry + GetEntriesTable(). + Where(fmt.Sprintf("timestamp BETWEEN %v AND %v", timestampFrom, timestampTo)). + Order(fmt.Sprintf("timestamp %s", order)). + Find(&entries) + + if len(entries) > 0 { + // the entries always order from oldest to newest so we should revers + utils.ReverseSlice(entries) + } + + entriesArray := make([]har.Entry, 0) + for _, entryData := range entries { + var harEntry har.Entry + _ = json.Unmarshal([]byte(entryData.Entry), &harEntry) + entriesArray = append(entriesArray, harEntry) + } + return entriesArray +} + diff --git a/api/pkg/models/models.go b/api/pkg/models/models.go index b36ea7528..a45124255 100644 --- a/api/pkg/models/models.go +++ b/api/pkg/models/models.go @@ -50,8 +50,6 @@ type EntriesFilter struct { } type UploadEntriesRequestBody struct { - Token string `query:"token"` - Model string `query:"model"` Dest string `query:"dest"` } diff --git a/api/pkg/routes/public_routes.go b/api/pkg/routes/public_routes.go index ae78d96db..3bdbe150a 100644 --- a/api/pkg/routes/public_routes.go +++ b/api/pkg/routes/public_routes.go @@ -20,4 +20,5 @@ func EntriesRoutes(fiberApp *fiber.App) { routeGroup.Get("/generalStats", controllers.GetGeneralStats) // get general stats about entries in DB routeGroup.Get("/tapStatus", controllers.GetTappingStatus) // get tapping status + routeGroup.Get("/analyzeStatus", controllers.AnalyzeInformation) } diff --git a/api/pkg/up9/main.go b/api/pkg/up9/main.go new file mode 100644 index 000000000..17f1ab131 --- /dev/null +++ b/api/pkg/up9/main.go @@ -0,0 +1,178 @@ +package up9 + +import ( + "bytes" + "compress/zlib" + "encoding/json" + "fmt" + "github.com/up9inc/mizu/shared" + "io/ioutil" + "log" + "mizuserver/pkg/database" + "net/http" + "net/url" + "time" +) + + +const ( + AnalyzeCheckSleepTime = 5 * time.Second +) + +type GuestToken struct { + Token string `json:"token"` + Model string `json:"model"` +} + +type ModelStatus struct { + LastMajorGeneration float64 `json:"lastMajorGeneration"` +} + +func getGuestToken(url string, target *GuestToken) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + return json.NewDecoder(resp.Body).Decode(target) +} + +func CreateAnonymousToken(envPrefix string) (*GuestToken, error) { + tokenUrl := fmt.Sprintf("https://trcc.%v/anonymous/token", envPrefix) + token := &GuestToken{} + if err := getGuestToken(tokenUrl, token); err != nil { + fmt.Println(err) + return nil, err + } + return token, nil +} + +func GetRemoteUrl(analyzeDestination string, analyzeToken string) string { + return fmt.Sprintf("https://%s/share/%s", analyzeDestination, analyzeToken) +} + +func CheckIfModelReady(analyzeDestination string, analyzeModel string, analyzeToken string) bool { + statusUrl, _ := url.Parse(fmt.Sprintf("https://trcc.%s/models/%s/status", analyzeDestination, analyzeModel)) + req := &http.Request{ + Method: http.MethodGet, + URL: statusUrl, + Header: map[string][]string{ + "Content-Type": {"application/json"}, + "Guest-Auth": {analyzeToken}, + }, + } + statusResp, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + defer statusResp.Body.Close() + + target := &ModelStatus{} + _ = json.NewDecoder(statusResp.Body).Decode(&target) + + return target.LastMajorGeneration > 0 +} + +func GetTrafficDumpUrl(analyzeDestination string, analyzeModel string) *url.URL { + postUrl, _ := url.Parse(fmt.Sprintf("https://traffic.%s/dumpTrafficBulk/%s", analyzeDestination, analyzeModel)) + return postUrl +} + +type AnalyzeInformation struct { + IsAnalyzing bool + AnalyzedModel string + AnalyzeToken string + AnalyzeDestination string +} + +func (info *AnalyzeInformation) Reset() { + info.IsAnalyzing = false + info.AnalyzedModel = "" + info.AnalyzeToken = "" + info.AnalyzeDestination = "" +} + +var analyzeInformation = &AnalyzeInformation{} + +func GetAnalyzeInfo() *shared.AnalyzeStatus { + return &shared.AnalyzeStatus{ + IsAnalyzing: analyzeInformation.IsAnalyzing, + RemoteUrl: GetRemoteUrl(analyzeInformation.AnalyzeDestination, analyzeInformation.AnalyzeToken), + IsRemoteReady: CheckIfModelReady(analyzeInformation.AnalyzeDestination, analyzeInformation.AnalyzedModel, analyzeInformation.AnalyzeToken), + } +} + +func UploadEntriesImpl(token string, model string, envPrefix string) { + analyzeInformation.IsAnalyzing = true + analyzeInformation.AnalyzedModel = model + analyzeInformation.AnalyzeToken = token + analyzeInformation.AnalyzeDestination = envPrefix + + sleepTime := time.Second * 10 + + var timestampFrom int64 = 0 + + for { + timestampTo := time.Now().UnixNano() / int64(time.Millisecond) + fmt.Printf("Getting entries from %v, to %v\n", timestampFrom, timestampTo) + entriesArray := database.GetEntriesFromDb(timestampFrom, timestampTo) + + if len(entriesArray) > 0 { + fmt.Printf("About to upload %v entries\n", len(entriesArray)) + + body, jMarshalErr := json.Marshal(entriesArray) + if jMarshalErr != nil { + analyzeInformation.Reset() + fmt.Println("Stopping analyzing") + log.Fatal(jMarshalErr) + } + + var in bytes.Buffer + w := zlib.NewWriter(&in) + _, _ = w.Write(body) + _ = w.Close() + reqBody := ioutil.NopCloser(bytes.NewReader(in.Bytes())) + + req := &http.Request{ + Method: http.MethodPost, + URL: GetTrafficDumpUrl(envPrefix, model), + Header: map[string][]string{ + "Content-Encoding": {"deflate"}, + "Content-Type": {"application/octet-stream"}, + "Guest-Auth": {token}, + }, + Body: reqBody, + } + + if _, postErr := http.DefaultClient.Do(req); postErr != nil { + analyzeInformation.Reset() + log.Println("Stopping analyzing") + log.Fatal(postErr) + } + fmt.Printf("Finish uploading %v entries to %s\n", len(entriesArray), GetTrafficDumpUrl(envPrefix, model)) + + } else { + fmt.Println("Nothing to upload") + } + + fmt.Printf("Sleeping for %v...\n", sleepTime) + time.Sleep(sleepTime) + timestampFrom = timestampTo + } +} + +func UpdateAnalyzeStatus(callback func(data []byte)) { + for { + if !analyzeInformation.IsAnalyzing { + time.Sleep(AnalyzeCheckSleepTime) + continue + } + analyzeStatus := GetAnalyzeInfo() + socketMessage := shared.CreateWebSocketMessageTypeAnalyzeStatus(*analyzeStatus) + + jsonMessage, _ := json.Marshal(socketMessage) + callback(jsonMessage) + time.Sleep(AnalyzeCheckSleepTime) + } +} diff --git a/cli/cmd/tap.go b/cli/cmd/tap.go index c23afd56f..f29a59773 100644 --- a/cli/cmd/tap.go +++ b/cli/cmd/tap.go @@ -63,7 +63,7 @@ func init() { tapCmd.Flags().Uint16VarP(&mizuTapOptions.GuiPort, "gui-port", "p", 8899, "Provide a custom port for the web interface webserver") tapCmd.Flags().StringVarP(&mizuTapOptions.Namespace, "namespace", "n", "", "Namespace selector") - tapCmd.Flags().BoolVar(&mizuTapOptions.Analyze, "analyze", false, "Uploads traffic to UP9 cloud for further analysis") + tapCmd.Flags().BoolVar(&mizuTapOptions.Analyze, "analyze", false, "Uploads traffic to UP9 cloud for further analysis (Beta)") tapCmd.Flags().StringVar(&mizuTapOptions.AnalyzeDestination, "dest", "up9.app", "Destination environment") tapCmd.Flags().BoolVarP(&mizuTapOptions.AllNamespaces, "all-namespaces", "A", false, "Tap all namespaces") tapCmd.Flags().StringVarP(&mizuTapOptions.KubeConfigPath, "kube-config", "k", "", "Path to kube-config file") diff --git a/cli/cmd/tapRunner.go b/cli/cmd/tapRunner.go index 7019c4715..dbb373a2a 100644 --- a/cli/cmd/tapRunner.go +++ b/cli/cmd/tapRunner.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "encoding/json" "fmt" "net/http" "os" @@ -81,30 +80,6 @@ func RunMizuTap(podRegexQuery *regexp.Regexp, tappingOptions *MizuTapOptions) { waitForFinish(ctx, cancel) } -type GuestToken struct { - Token string `json:"token"` - Model string `json:"model"` -} - -func getGuestToken(url string, target *GuestToken) error { - resp, err := http.Get(url) - if err != nil { - return err - } - defer resp.Body.Close() - - return json.NewDecoder(resp.Body).Decode(target) -} - -func CreateAnonymousToken(envPrefix string) (*GuestToken, error) { - tokenUrl := fmt.Sprintf("https://trcc.%v/anonymous/token", envPrefix) - token := &GuestToken{} - if err := getGuestToken(tokenUrl, token); err != nil { - fmt.Println(err) - return nil, err - } - return token, nil -} func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string, tappingOptions *MizuTapOptions, mizuApiFilteringOptions *shared.TrafficFilteringOptions) error { if err := createMizuAggregator(ctx, kubernetesProvider, tappingOptions, mizuApiFilteringOptions); err != nil { @@ -225,10 +200,10 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro for { select { case newTarget := <-added: - fmt.Printf("+%s\n", newTarget.Name) + fmt.Printf(mizu.Green, fmt.Sprintf("+%s\n", newTarget.Name)) case removedTarget := <-removed: - fmt.Printf("-%s\n", removedTarget.Name) + fmt.Printf(mizu.Red, fmt.Sprintf("-%s\n", removedTarget.Name)) restartTappersDebouncer.SetOn() case modifiedTarget := <-modified: @@ -274,14 +249,13 @@ func portForwardApiPod(ctx context.Context, kubernetesProvider *kubernetes.Provi cancel() } else { fmt.Printf("Web interface is now available at http://localhost:%d\n", tappingOptions.GuiPort) - + time.Sleep(time.Second * 5) // Waiting to be sure port forwarding finished if tappingOptions.Analyze { - token, _ := CreateAnonymousToken(tappingOptions.AnalyzeDestination) - if _, err := http.Get(fmt.Sprintf("http://localhost:%d/api/uploadEntries?token=%s&model=%s&dest=%s", tappingOptions.GuiPort, token.Token, token.Model, tappingOptions.AnalyzeDestination)); err != nil { + if _, err := http.Get(fmt.Sprintf("http://localhost:%d/api/uploadEntries?dest=%s", tappingOptions.GuiPort, tappingOptions.AnalyzeDestination)); err != nil { fmt.Println(err) } else { - fmt.Println("Staring to upload and analyze the data, it may take a few minutes") - fmt.Println("https://" + tappingOptions.AnalyzeDestination + "/share/" + token.Token) + fmt.Printf(mizu.Yellow, "Traffic is uploading to UP9 cloud for further analsys") + fmt.Println() } } } diff --git a/cli/mizu/consts.go b/cli/mizu/consts.go index 9f72620ad..1f960ead2 100644 --- a/cli/mizu/consts.go +++ b/cli/mizu/consts.go @@ -15,3 +15,14 @@ const ( TapperPodName = "mizu-tapper" K8sAllNamespaces = "" ) + +const ( + Black = "\033[1;30m%s\033[0m" + Red = "\033[1;31m%s\033[0m" + Green = "\033[1;32m%s\033[0m" + Yellow = "\033[1;33m%s\033[0m" + Purple = "\033[1;34m%s\033[0m" + Magenta = "\033[1;35m%s\033[0m" + Teal = "\033[1;36m%s\033[0m" + White = "\033[1;37m%s\033[0m" +) diff --git a/shared/models.go b/shared/models.go index 854dbf1f6..f1d443cdf 100644 --- a/shared/models.go +++ b/shared/models.go @@ -1,28 +1,41 @@ package shared type WebSocketMessageType string + const ( - WebSocketMessageTypeEntry WebSocketMessageType = "entry" - WebSocketMessageTypeTappedEntry WebSocketMessageType = "tappedEntry" - WebSocketMessageTypeUpdateStatus WebSocketMessageType = "status" + WebSocketMessageTypeEntry WebSocketMessageType = "entry" + WebSocketMessageTypeTappedEntry WebSocketMessageType = "tappedEntry" + WebSocketMessageTypeUpdateStatus WebSocketMessageType = "status" + WebSocketMessageTypeAnalyzeStatus WebSocketMessageType = "analyzeStatus" ) type WebSocketMessageMetadata struct { MessageType WebSocketMessageType `json:"messageType,omitempty"` } +type WebSocketAnalyzeStatusMessage struct { + *WebSocketMessageMetadata + AnalyzeStatus AnalyzeStatus `json:"analyzeStatus"` +} + +type AnalyzeStatus struct { + IsAnalyzing bool `json:"isAnalyzing"` + RemoteUrl string `json:"remoteUrl"` + IsRemoteReady bool `json:"isRemoteReady"` +} + type WebSocketStatusMessage struct { *WebSocketMessageMetadata TappingStatus TapStatus `json:"tappingStatus"` } type TapStatus struct { - Pods []PodInfo `json:"pods"` + Pods []PodInfo `json:"pods"` } type PodInfo struct { Namespace string `json:"namespace"` - Name string `json:"name"` + Name string `json:"name"` } func CreateWebSocketStatusMessage(tappingStatus TapStatus) WebSocketStatusMessage { @@ -34,6 +47,15 @@ func CreateWebSocketStatusMessage(tappingStatus TapStatus) WebSocketStatusMessag } } +func CreateWebSocketMessageTypeAnalyzeStatus(analyzeStatus AnalyzeStatus) WebSocketAnalyzeStatusMessage { + return WebSocketAnalyzeStatusMessage{ + WebSocketMessageMetadata: &WebSocketMessageMetadata{ + MessageType: WebSocketMessageTypeAnalyzeStatus, + }, + AnalyzeStatus: analyzeStatus, + } +} + type TrafficFilteringOptions struct { PlainTextMaskingRegexes []*SerializableRegexp } diff --git a/ui/src/App.sass b/ui/src/App.sass index 564ee6c2b..ab781429e 100644 --- a/ui/src/App.sass +++ b/ui/src/App.sass @@ -10,6 +10,8 @@ display: flex align-items: center padding-left: 24px + padding-right: 24px + justify-content: space-between .title font-size: 45px diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 4195e558d..470de200c 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,18 +1,43 @@ -import React from 'react'; -import {HarPage} from "./components/HarPage"; +import React, {useState} from 'react'; import './App.sass'; import logo from './components/assets/Mizu.svg'; +import {Button, ThemeProvider} from "@material-ui/core"; +import Theme from './style/mui.theme'; +import {HarPage} from "./components/HarPage"; + const App = () => { - return ( -
-
-
logo
-
Traffic viewer for Kubernetes
-
- -
- ); + + const [analyzeStatus, setAnalyzeStatus] = useState(null); + + return ( + +
+
+
+
logo
+
Traffic viewer for Kubernetes
+
+
+ {analyzeStatus?.isAnalyzing && +
+ +
+ } +
+
+ +
+
+ ); } export default App; diff --git a/ui/src/components/HarPage.tsx b/ui/src/components/HarPage.tsx index 11edf80fa..6005cdb1b 100644 --- a/ui/src/components/HarPage.tsx +++ b/ui/src/components/HarPage.tsx @@ -35,7 +35,11 @@ enum ConnectionStatus { Paused } -export const HarPage: React.FC = () => { +interface HarPageProps { + setAnalyzeStatus: (status: any) => void; +} + +export const HarPage: React.FC = ({setAnalyzeStatus}) => { const classes = useLayoutStyles(); @@ -60,21 +64,21 @@ export const HarPage: React.FC = () => { ws.current.onclose = () => setConnection(ConnectionStatus.Closed); } - if(ws.current) { + if (ws.current) { ws.current.onmessage = e => { - if(!e?.data) return; + if (!e?.data) return; const message = JSON.parse(e.data); switch (message.messageType) { case "entry": const entry = message.data - if(connection === ConnectionStatus.Paused) { + if (connection === ConnectionStatus.Paused) { setNoMoreDataBottom(false) return; } - if(!focusedEntryId) setFocusedEntryId(entry.id) + if (!focusedEntryId) setFocusedEntryId(entry.id) let newEntries = [...entries]; - if(entries.length === 1000) { + if (entries.length === 1000) { newEntries = newEntries.splice(1); setNoMoreDataTop(false); } @@ -83,6 +87,9 @@ export const HarPage: React.FC = () => { case "status": setTappingStatus(message.tappingStatus); break + case "analyzeStatus": + setAnalyzeStatus(message.analyzeStatus); + break default: console.error(`unsupported websocket message type, Got: ${message.messageType}`) } @@ -94,19 +101,23 @@ export const HarPage: React.FC = () => { fetch(`http://localhost:8899/api/tapStatus`) .then(response => response.json()) .then(data => setTappingStatus(data)); + + fetch(`http://localhost:8899/api/analyzeStatus`) + .then(response => response.json()) + .then(data => setAnalyzeStatus(data)); }, []); useEffect(() => { - if(!focusedEntryId) return; + if (!focusedEntryId) return; setSelectedHarEntry(null) fetch(`http://localhost:8899/api/entries/${focusedEntryId}`) .then(response => response.json()) .then(data => setSelectedHarEntry(data)); - },[focusedEntryId]) + }, [focusedEntryId]) const toggleConnection = () => { - setConnection(connection === ConnectionStatus.Connected ? ConnectionStatus.Paused : ConnectionStatus.Connected ); + setConnection(connection === ConnectionStatus.Connected ? ConnectionStatus.Paused : ConnectionStatus.Connected); } const getConnectionStatusClass = (isContainer) => { @@ -135,11 +146,12 @@ export const HarPage: React.FC = () => { return (
- pause + pause
{getConnectionTitle()}
-
+
@@ -169,7 +181,8 @@ export const HarPage: React.FC = () => {
- {selectedHarEntry && } + {selectedHarEntry && + }
} {tappingStatus?.pods != null && } diff --git a/ui/src/style/mui.theme.ts b/ui/src/style/mui.theme.ts new file mode 100644 index 000000000..a9f24b8ad --- /dev/null +++ b/ui/src/style/mui.theme.ts @@ -0,0 +1,366 @@ +import createMuiTheme, { Theme, ThemeOptions } from "@material-ui/core/styles/createMuiTheme"; +import { Palette } from "@material-ui/core/styles/createPalette"; + +interface IColor { + main: string; + light: string; + dark: string; + contrastText: string; +} + +interface IPalette extends Palette { + primaryBackground: IColor + secondaryBackground: IColor +} + +export interface ITheme extends Theme { + palette: IPalette; +} + +interface IThemeOptions extends ThemeOptions { + palette: IPalette; + overrides: any; +} + +const theme = createMuiTheme({ + "palette": { + "common": { + "black": "#000", + "white": "#fff" + }, + "type": "dark", + "primary": { + "main": "#627ef7", + "light": "#a0b2ff", + "dark": "#2b3560", + "contrastText": "#090b14" + }, + "primaryBackground": { + "main": "#171c30", + "light": "#252c47", + "dark": "#090b14", + "contrastText": "#fff" + }, + "secondary": { + "main": "rgba(255, 255, 255, 0.75)", + "light": "#fff", + "dark": "rgba(255, 255, 255, 0.5)", + "contrastText": "#000" + }, + "secondaryBackground": { + "main": "rgba(255, 255, 255, 0.12)", + "light": "rgba(255, 255, 255, 0.25)", + "dark": "rgba(255, 255, 255, 0.06)", + "contrastText": "#fff" + }, + "error": { + "main": "#cb5411", + "light": "#ff3a30", + "dark": "rgba(255, 58, 48, 0.35)", + "contrastText": "#fff" + }, + "warning": { + "light": "#ffb74d", + "main": "#ff9800", + "dark": "#f57c00", + "contrastText": "#344073" + }, + "info": { + "light": "#64b5f6", + "main": "#2196f3", + "dark": "#1976d2", + "contrastText": "#fff" + }, + "success": { + "light": "#3eb545", + "main": "rgba(62, 181, 69, 0.5)", + "dark": "rgba(62, 181, 69, 0.35)", + "contrastText": "#fff" + }, + "grey": { + "50": "#fafafa", + "100": "#f5f5f5", + "200": "#eeeeee", + "300": "#e0e0e0", + "400": "#bdbdbd", + "500": "#9e9e9e", + "600": "#757575", + "700": "#616161", + "800": "#424242", + "900": "#212121", + "A100": "#d5d5d5", + "A200": "#aaaaaa", + "A400": "#303030", + "A700": "#616161" + }, + "contrastThreshold": 3, + "tonalOffset": 0.2, + "text": { + "primary": "#fff", + "secondary": "rgba(255, 255, 255, 0.85)", + "disabled": "rgba(255, 255, 255, 0.75)", + "hint": "rgba(255, 255, 255, 0.65)", + }, + "divider": "rgba(255, 255, 255, 0.12)", + "background": { + "paper": "#344073", + "default": "#252c47", + }, + "action": { + "active": "#fff", + "hover": "rgba(255, 255, 255, 0.08)", + "hoverOpacity": 0.08, + "selected": "rgba(255, 255, 255, 0.16)", + "selectedOpacity": 0.16, + "disabled": "rgba(255, 255, 255, 0.3)", + "disabledBackground": "rgba(255, 255, 255, 0.12)", + "disabledOpacity": 0.38, + "focus": "rgba(255, 255, 255, 0.12)", + "focusOpacity": 0.12, + "activatedOpacity": 0.24 + } + }, + "props": {}, + "shadows": [ + "none", + "0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12)", + "0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)", + "0px 3px 3px -2px rgba(0,0,0,0.2),0px 3px 4px 0px rgba(0,0,0,0.14),0px 1px 8px 0px rgba(0,0,0,0.12)", + "0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12)", + "0px 3px 5px -1px rgba(0,0,0,0.2),0px 5px 8px 0px rgba(0,0,0,0.14),0px 1px 14px 0px rgba(0,0,0,0.12)", + "0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12)", + "0px 4px 5px -2px rgba(0,0,0,0.2),0px 7px 10px 1px rgba(0,0,0,0.14),0px 2px 16px 1px rgba(0,0,0,0.12)", + "0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12)", + "0px 5px 6px -3px rgba(0,0,0,0.2),0px 9px 12px 1px rgba(0,0,0,0.14),0px 3px 16px 2px rgba(0,0,0,0.12)", + "0px 6px 6px -3px rgba(0,0,0,0.2),0px 10px 14px 1px rgba(0,0,0,0.14),0px 4px 18px 3px rgba(0,0,0,0.12)", + "0px 6px 7px -4px rgba(0,0,0,0.2),0px 11px 15px 1px rgba(0,0,0,0.14),0px 4px 20px 3px rgba(0,0,0,0.12)", + "0px 7px 8px -4px rgba(0,0,0,0.2),0px 12px 17px 2px rgba(0,0,0,0.14),0px 5px 22px 4px rgba(0,0,0,0.12)", + "0px 7px 8px -4px rgba(0,0,0,0.2),0px 13px 19px 2px rgba(0,0,0,0.14),0px 5px 24px 4px rgba(0,0,0,0.12)", + "0px 7px 9px -4px rgba(0,0,0,0.2),0px 14px 21px 2px rgba(0,0,0,0.14),0px 5px 26px 4px rgba(0,0,0,0.12)", + "0px 8px 9px -5px rgba(0,0,0,0.2),0px 15px 22px 2px rgba(0,0,0,0.14),0px 6px 28px 5px rgba(0,0,0,0.12)", + "0px 8px 10px -5px rgba(0,0,0,0.2),0px 16px 24px 2px rgba(0,0,0,0.14),0px 6px 30px 5px rgba(0,0,0,0.12)", + "0px 8px 11px -5px rgba(0,0,0,0.2),0px 17px 26px 2px rgba(0,0,0,0.14),0px 6px 32px 5px rgba(0,0,0,0.12)", + "0px 9px 11px -5px rgba(0,0,0,0.2),0px 18px 28px 2px rgba(0,0,0,0.14),0px 7px 34px 6px rgba(0,0,0,0.12)", + "0px 9px 12px -6px rgba(0,0,0,0.2),0px 19px 29px 2px rgba(0,0,0,0.14),0px 7px 36px 6px rgba(0,0,0,0.12)", + "0px 10px 13px -6px rgba(0,0,0,0.2),0px 20px 31px 3px rgba(0,0,0,0.14),0px 8px 38px 7px rgba(0,0,0,0.12)", + "0px 10px 13px -6px rgba(0,0,0,0.2),0px 21px 33px 3px rgba(0,0,0,0.14),0px 8px 40px 7px rgba(0,0,0,0.12)", + "0px 10px 14px -6px rgba(0,0,0,0.2),0px 22px 35px 3px rgba(0,0,0,0.14),0px 8px 42px 7px rgba(0,0,0,0.12)", + "0px 11px 14px -7px rgba(0,0,0,0.2),0px 23px 36px 3px rgba(0,0,0,0.14),0px 9px 44px 8px rgba(0,0,0,0.12)", + "0px 11px 15px -7px rgba(0,0,0,0.2),0px 24px 38px 3px rgba(0,0,0,0.14),0px 9px 46px 8px rgba(0,0,0,0.12)" + ], + "typography": { + "htmlFontSize": 16, + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontSize": 14, + "fontWeightLight": 300, + "fontWeightRegular": 400, + "fontWeightMedium": 500, + "fontWeightBold": 600, + "h1": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 300, + "fontSize": "6rem", + "lineHeight": 1.167, + "letterSpacing": "-0.01562em" + }, + "h2": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 300, + "fontSize": "3.75rem", + "lineHeight": 1.2, + "letterSpacing": "-0.00833em" + }, + "h3": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "3rem", + "lineHeight": 1.167, + "letterSpacing": "0em" + }, + "h4": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "2.125rem", + "lineHeight": 1.235, + "letterSpacing": "0.00735em" + }, + "h5": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "1.2rem", + "lineHeight": 1.334, + "letterSpacing": "0.02em" + }, + "h6": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "1rem", + "lineHeight": 1.6, + "letterSpacing": "0.0075em", + "textTransform": "uppercase" + }, + "subtitle1": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "1rem", + "lineHeight": 1.75, + "letterSpacing": "0.00938em" + }, + "subtitle2": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 500, + "fontSize": "0.875rem", + "lineHeight": 1.57, + "letterSpacing": "0.00714em" + }, + "body1": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "1rem", + "lineHeight": 1.5, + "letterSpacing": "0.00938em" + }, + "body2": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "0.875rem", + "lineHeight": 1.43, + "letterSpacing": "0.01071em" + }, + "button": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 500, + "fontSize": "0.875rem", + "lineHeight": 1.75, + "letterSpacing": "0.02857em", + "textTransform": "uppercase" + }, + "caption": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "0.8rem", + "lineHeight": 1.66, + "letterSpacing": "0.03333em" + }, + "overline": { + "fontFamily": "'Source Sans Pro', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif", + "fontWeight": 400, + "fontSize": "0.75rem", + "lineHeight": 2.66, + "letterSpacing": "0.08333em", + "textTransform": "uppercase" + } + }, + "shape": { + "borderRadius": 16 + }, + "transitions": { + "easing": { + "easeInOut": "cubic-bezier(0.4, 0, 0.2, 1)", + "easeOut": "cubic-bezier(0.0, 0, 0.2, 1)", + "easeIn": "cubic-bezier(0.4, 0, 1, 1)", + "sharp": "cubic-bezier(0.4, 0, 0.6, 1)" + }, + "duration": { + "shortest": 150, + "shorter": 200, + "short": 250, + "standard": 300, + "complex": 375, + "enteringScreen": 225, + "leavingScreen": 195 + } + }, + "zIndex": { + "mobileStepper": 1000, + "speedDial": 1050, + "appBar": 1100, + "drawer": 1200, + "modal": 1300, + "snackbar": 1400, + "tooltip": 1500 + } +} as IThemeOptions) as ITheme; + +theme.overrides = { + + ...theme.overrides, + + MuiTooltip: { + tooltip: { + backgroundColor: "#2a335a", + fontSize: "14px", + borderRadius: "8px", + } + }, + + "MuiButton": { + label: { + fontWeight: "bold", + }, + "contained": { + color: '#F7F9FC', + padding: "7px 18px", + }, + "containedPrimary": { + + color: '#F7F9FC', + + "&:disabled": { + backgroundColor: "rgba(0,0,0,0.15)", + color: "rgba(255, 255, 255, 0.8)" + } + }, + "outlinedPrimary": { + + padding: "8px 18px", + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.06)" + }, + + "&:disabled": { + backgroundColor: "rgba(255, 255, 255, 0.06)", + color: "rgba(255, 255, 255, 0.25)" + }, + + } + }, + + "MuiSwitch": { + root: { + padding: 11, + width: 44, + height: 29, + }, + switchBase: { + '&$checked': { + transform: "translateX(10px)", + "& $thumb": { + backgroundColor: theme.palette.common.white, + }, + "&.MuiSwitch-colorPrimary + $track": { + backgroundColor: theme.palette.secondaryBackground.dark, + border: "3px solid " + theme.palette.primary.light, + opacity: 1, + }, + } + }, + thumb: { + width: 8, + height: 8, + border: "3px solid " + theme.palette.primary.main, + backgroundColor: theme.palette.primary.dark, + boxShadow: 'none', + }, + track: { + width: 14, + height: 4, + backgroundColor: theme.palette.secondaryBackground.dark, + border: "3px solid " + theme.palette.primary.light, + opacity: 1, + } + } +}; + +export default theme; +