diff --git a/.storybook/main.ts b/.storybook/main.ts index 7644575af..2545115e3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -6,7 +6,8 @@ const config: StorybookConfig = { "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", - "@storybook/addon-a11y" + "@storybook/addon-a11y", + "@storybook/addon-designs" ], framework: { name: "@storybook/react-webpack5", diff --git a/package-lock.json b/package-lock.json index 8889da379..690f5fdac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "react-syntax-highlighter": "^15.5.0", "react-transition-group": "^4.4.5", "recharts": "^2.6.2", + "semver": "^7.5.4", "styled-components": "^6.1.0" }, "devDependencies": { @@ -32,6 +33,7 @@ "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.2", "@storybook/addon-a11y": "^7.4.6", + "@storybook/addon-designs": "^7.0.5", "@storybook/addon-essentials": "^7.4.6", "@storybook/addon-interactions": "^7.4.6", "@storybook/addon-links": "^7.4.6", @@ -44,6 +46,7 @@ "@types/react-helmet": "^6.1.7", "@types/react-syntax-highlighter": "^15.5.7", "@types/react-transition-group": "^4.4.5", + "@types/semver": "^7.5.6", "@typescript-eslint/eslint-plugin": "^5.49.0", "@typescript-eslint/parser": "^5.49.0", "babel-loader": "^9.1.3", @@ -2868,6 +2871,28 @@ "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", "dev": true }, + "node_modules/@figspec/components": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@figspec/components/-/components-1.0.2.tgz", + "integrity": "sha512-rTjjH7wvM55ZuX+MRVPND1cs4Z4JspJvKc9lzGxm/8gD4dLfgeFztQuNy+daGglaxcGXLXTuJ2oJtZ0/lmRKmw==", + "dev": true, + "dependencies": { + "lit": "^2.1.3" + } + }, + "node_modules/@figspec/react": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.3.tgz", + "integrity": "sha512-r683qOko+5CbT48Ox280fMx2MNAtaFPgCNJvldOqN3YtmAzlcTT+YSxd3OahA+kjXGGrnzDbUgeTOX1cPLII+g==", + "dev": true, + "dependencies": { + "@figspec/components": "^1.0.1", + "@lit-labs/react": "^1.0.2" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", @@ -3235,6 +3260,27 @@ "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, + "node_modules/@lit-labs/react": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.2.1.tgz", + "integrity": "sha512-DiZdJYFU0tBbdQkfwwRSwYyI/mcWkg3sWesKRsHUd4G+NekTmmeq9fzsurvcKTNVa0comNljwtg4Hvi1ds3V+A==", + "dev": true + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", + "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==", + "dev": true + }, + "node_modules/@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "dev": true, + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, "node_modules/@mdx-js/react": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", @@ -4266,6 +4312,33 @@ } } }, + "node_modules/@storybook/addon-designs": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@storybook/addon-designs/-/addon-designs-7.0.5.tgz", + "integrity": "sha512-yB1YwkVhnTI28mS+00avAf7vPAppZi2pdXQF91725g+RoiM7llY87q+c1z2/YiQNQYNm2QXpYcrcYiLQzyr0NQ==", + "dev": true, + "dependencies": { + "@figspec/react": "^1.0.0" + }, + "peerDependencies": { + "@storybook/addon-docs": "^7.0.0", + "@storybook/addons": "^7.0.0", + "@storybook/components": "^7.0.0", + "@storybook/manager-api": "^7.0.0", + "@storybook/preview-api": "^7.0.0", + "@storybook/theming": "^7.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@storybook/addon-docs": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.4.6.tgz", @@ -6528,9 +6601,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/send": { @@ -6559,6 +6632,12 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.2.tgz", "integrity": "sha512-Rm17MsTpQQP5Jq4BF7CdrxJsDufoiL/q5IbJZYZmOZAJALyijgF7BzLgobXUqraNcQdqFYLYGeglDp6QzaxPpg==" }, + "node_modules/@types/trusted-types": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.6.tgz", + "integrity": "sha512-HYtNooPvUY9WAVRBr4u+4Qa9fYD1ze2IUlAD3HoA6oehn1taGwBx3Oa52U4mTslTS+GAExKpaFu39Y5xUEwfjg==", + "dev": true + }, "node_modules/@types/unist": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", @@ -13749,6 +13828,37 @@ "node": ">=8" } }, + "node_modules/lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "dev": true, + "dependencies": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "dev": true, + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "dev": true, + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -16677,10 +16787,9 @@ } }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -16695,7 +16804,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16706,8 +16814,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -20919,6 +21026,25 @@ "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", "dev": true }, + "@figspec/components": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@figspec/components/-/components-1.0.2.tgz", + "integrity": "sha512-rTjjH7wvM55ZuX+MRVPND1cs4Z4JspJvKc9lzGxm/8gD4dLfgeFztQuNy+daGglaxcGXLXTuJ2oJtZ0/lmRKmw==", + "dev": true, + "requires": { + "lit": "^2.1.3" + } + }, + "@figspec/react": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.3.tgz", + "integrity": "sha512-r683qOko+5CbT48Ox280fMx2MNAtaFPgCNJvldOqN3YtmAzlcTT+YSxd3OahA+kjXGGrnzDbUgeTOX1cPLII+g==", + "dev": true, + "requires": { + "@figspec/components": "^1.0.1", + "@lit-labs/react": "^1.0.2" + } + }, "@floating-ui/core": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", @@ -21211,6 +21337,27 @@ "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, + "@lit-labs/react": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.2.1.tgz", + "integrity": "sha512-DiZdJYFU0tBbdQkfwwRSwYyI/mcWkg3sWesKRsHUd4G+NekTmmeq9fzsurvcKTNVa0comNljwtg4Hvi1ds3V+A==", + "dev": true + }, + "@lit-labs/ssr-dom-shim": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", + "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==", + "dev": true + }, + "@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "dev": true, + "requires": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, "@mdx-js/react": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", @@ -21783,6 +21930,15 @@ "ts-dedent": "^2.0.0" } }, + "@storybook/addon-designs": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@storybook/addon-designs/-/addon-designs-7.0.5.tgz", + "integrity": "sha512-yB1YwkVhnTI28mS+00avAf7vPAppZi2pdXQF91725g+RoiM7llY87q+c1z2/YiQNQYNm2QXpYcrcYiLQzyr0NQ==", + "dev": true, + "requires": { + "@figspec/react": "^1.0.0" + } + }, "@storybook/addon-docs": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.4.6.tgz", @@ -23461,9 +23617,9 @@ "dev": true }, "@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "@types/send": { @@ -23492,6 +23648,12 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.2.tgz", "integrity": "sha512-Rm17MsTpQQP5Jq4BF7CdrxJsDufoiL/q5IbJZYZmOZAJALyijgF7BzLgobXUqraNcQdqFYLYGeglDp6QzaxPpg==" }, + "@types/trusted-types": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.6.tgz", + "integrity": "sha512-HYtNooPvUY9WAVRBr4u+4Qa9fYD1ze2IUlAD3HoA6oehn1taGwBx3Oa52U4mTslTS+GAExKpaFu39Y5xUEwfjg==", + "dev": true + }, "@types/unist": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", @@ -28729,6 +28891,37 @@ } } }, + "lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "dev": true, + "requires": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "dev": true, + "requires": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "dev": true, + "requires": { + "@types/trusted-types": "^2.0.2" + } + }, "loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -30853,10 +31046,9 @@ } }, "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" }, @@ -30865,7 +31057,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -30873,8 +31064,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, diff --git a/package.json b/package.json index 7ab1d325a..92a6b349f 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.2", "@storybook/addon-a11y": "^7.4.6", + "@storybook/addon-designs": "^7.0.5", "@storybook/addon-essentials": "^7.4.6", "@storybook/addon-interactions": "^7.4.6", "@storybook/addon-links": "^7.4.6", @@ -66,6 +67,7 @@ "@types/react-helmet": "^6.1.7", "@types/react-syntax-highlighter": "^15.5.7", "@types/react-transition-group": "^4.4.5", + "@types/semver": "^7.5.6", "@typescript-eslint/eslint-plugin": "^5.49.0", "@typescript-eslint/parser": "^5.49.0", "babel-loader": "^9.1.3", @@ -111,6 +113,7 @@ "react-syntax-highlighter": "^15.5.0", "react-transition-group": "^4.4.5", "recharts": "^2.6.2", + "semver": "^7.5.4", "styled-components": "^6.1.0" } } diff --git a/src/actions.ts b/src/actions.ts index 77398f814..94adb2d90 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -21,5 +21,7 @@ export const actions = addPrefix(ACTION_PREFIX, { SET_USER_EMAIL: "SET_USER_EMAIL", SET_ENVIRONMENT: "SET_ENVIRONMENT", SET_IS_OBSERVABILITY_ENABLED: "SET_IS_OBSERVABILITY_ENABLED", - SET_OBSERVABILITY: "SET_OBSERVABILITY" + SET_OBSERVABILITY: "SET_OBSERVABILITY", + GET_BACKEND_INFO: "GET_BACKEND_INFO", + SET_BACKEND_INFO: "SET_BACKEND_INFO" }); diff --git a/src/api/web/sendMessageToWebService.ts b/src/api/web/sendMessageToWebService.ts index fe3141ca0..7483897d6 100644 --- a/src/api/web/sendMessageToWebService.ts +++ b/src/api/web/sendMessageToWebService.ts @@ -1,6 +1,9 @@ +import { actions as globalActions } from "../../actions"; import { actions as dashboardActions } from "../../components/Dashboard/actions"; import { DigmaOutgoingMessageData } from "../types"; +import { getAboutInfo } from "./services/about"; import { GetDashboardParams, getDashboard } from "./services/dashboard"; +import { GetEnvironmentParams, getEnvironment } from "./services/environments"; export const sendMessageToWebService = (message: DigmaOutgoingMessageData) => { switch (message.action) { @@ -9,5 +12,13 @@ export const sendMessageToWebService = (message: DigmaOutgoingMessageData) => { message.payload as GetDashboardParams> ); break; + case globalActions.GET_BACKEND_INFO: + void getAboutInfo(); + break; + case dashboardActions.GET_ENVIRONMENT_INFO: + void getEnvironment( + message.payload as GetEnvironmentParams, + dashboardActions.SET_ENVIRONMENT_INFO + ); } }; diff --git a/src/api/web/services/about.ts b/src/api/web/services/about.ts new file mode 100644 index 000000000..b06421466 --- /dev/null +++ b/src/api/web/services/about.ts @@ -0,0 +1,19 @@ +import { actions as globalActions } from "../../../actions"; +import { BackendInfo } from "../../../components/common/App/types"; +import { client } from "../client"; + +type GetAboutResponse = BackendInfo; + +export const getAboutInfo = async () => { + try { + const response = await client.get("/about"); + + window.postMessage({ + type: "digma", + action: globalActions.SET_BACKEND_INFO, + payload: response.data + }); + } catch (e) { + console.error(e); + } +}; diff --git a/src/api/web/services/dashboard.ts b/src/api/web/services/dashboard.ts index 099c98c09..c8031d5c0 100644 --- a/src/api/web/services/dashboard.ts +++ b/src/api/web/services/dashboard.ts @@ -2,17 +2,17 @@ import { AxiosError } from "axios"; import { actions as dashboardActions } from "../../../components/Dashboard/actions"; import { client } from "../client"; -export type GetDashboardResponse = { - type: string; - data: T; -}; - export interface GetDashboardParams { environment: string; type: string; query: T; } +export type GetDashboardResponse = { + type: string; + data: T; +}; + export const getDashboard = async ( data: GetDashboardParams> ) => { diff --git a/src/api/web/services/environments.ts b/src/api/web/services/environments.ts new file mode 100644 index 000000000..6535c0ff4 --- /dev/null +++ b/src/api/web/services/environments.ts @@ -0,0 +1,45 @@ +import { client } from "../client"; + +export interface GetEnvironmentParams { + environmentId: string; +} + +export interface GetEnvironmentResponse { + environmentName: string; + environmentId: string; + displayName: string; +} + +const REQUEST_TIMEOUT = 3000; + +export const getEnvironment = async ( + data: GetEnvironmentParams, + responseAction: string +) => { + try { + const response = await client.get( + "/environments/getEnvironment", + { signal: AbortSignal.timeout(REQUEST_TIMEOUT), params: data } + ); + + window.postMessage({ + type: "digma", + action: responseAction, + payload: response.data + }); + } catch (e) { + console.error(e); + const errorMessage = e instanceof Error ? e.message : "Unknown error"; + + window.postMessage({ + type: "digma", + action: responseAction, + payload: { + data: null, + error: { + message: errorMessage + } + } + }); + } +}; diff --git a/src/components/Assets/AssetList/AssetEntry/index.tsx b/src/components/Assets/AssetList/AssetEntry/index.tsx index d009d3661..825d1dff1 100644 --- a/src/components/Assets/AssetList/AssetEntry/index.tsx +++ b/src/components/Assets/AssetList/AssetEntry/index.tsx @@ -4,6 +4,7 @@ import { formatTimeDistance } from "../../../../utils/formatTimeDistance"; import { getInsightImportanceColor } from "../../../../utils/getInsightImportanceColor"; import { getInsightTypeInfo } from "../../../../utils/getInsightTypeInfo"; import { getInsightTypeOrderPriority } from "../../../../utils/getInsightTypeOrderPriority"; +import { ImpactScore } from "../../../common/ImpactScore"; import { Tooltip } from "../../../common/Tooltip"; import { GlobeIcon } from "../../../common/icons/GlobeIcon"; import { getAssetTypeInfo } from "../../utils"; @@ -11,34 +12,6 @@ import { SORTING_CRITERION } from "../types"; import * as s from "./styles"; import { AssetEntryProps } from "./types"; -const getImpactScoreIndicator = (score: number) => { - if (score < 0) { - return null; - } - - return ( - - - - ); -}; - -const getImpactScoreLabel = (score: number) => { - if (score < 0) { - return "No data"; - } - - if (score < 0.4) { - return "Low"; - } - - if (score < 0.8) { - return "Medium"; - } - - return "High"; -}; - const getServiceIconColor = (theme: DefaultTheme) => { switch (theme.mode) { case "light": @@ -176,12 +149,13 @@ export const AssetEntry = (props: AssetEntryProps) => { Performance impact - {getImpactScoreLabel(props.entry.impactScores.ScoreExp25)} - {props.sortingCriterion === - SORTING_CRITERION.PERFORMANCE_IMPACT && - getImpactScoreIndicator( - props.entry.impactScores.ScoreExp25 - )} + @@ -189,12 +163,13 @@ export const AssetEntry = (props: AssetEntryProps) => { Overall impact - {getImpactScoreLabel(props.entry.impactScores.ScoreExp1000)} - {props.sortingCriterion === - SORTING_CRITERION.OVERALL_IMPACT && - getImpactScoreIndicator( - props.entry.impactScores.ScoreExp1000 - )} + diff --git a/src/components/Dashboard/DashboardCard/index.tsx b/src/components/Dashboard/DashboardCard/index.tsx new file mode 100644 index 000000000..d9f170dd3 --- /dev/null +++ b/src/components/Dashboard/DashboardCard/index.tsx @@ -0,0 +1,17 @@ +import * as s from "./styles"; +import { DashboardCardProps } from "./types"; + +export const DashboardCard = (props: DashboardCardProps) => ( + + + + + + + {props.title} + + {props.headerContent} + + {props.content} + +); diff --git a/src/components/Dashboard/DashboardCard/styles.ts b/src/components/Dashboard/DashboardCard/styles.ts new file mode 100644 index 000000000..0d4ac15ee --- /dev/null +++ b/src/components/Dashboard/DashboardCard/styles.ts @@ -0,0 +1,61 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + flex-basis: calc(50% - 4px); + min-width: 300px; + border-radius: 4px; + padding: 12px; + box-sizing: border-box; + overflow: auto; + height: 231px; + background: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#ebecf0"; + case "dark": + case "dark-jetbrains": + return "#393b40"; + } + }}; +`; + +export const Header = styled.div` + display: flex; + justify-content: space-between; + padding-bottom: 12px; + border-bottom: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#c9ccd6"; + case "dark": + case "dark-jetbrains": + return "#4e5157"; + } + }}; + min-height: 31px; +`; + +export const Title = styled.div` + display: flex; + gap: 4px; + font-size: 14px; + font-weight: 700; + align-items: center; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; +`; + +export const IconContainer = styled.div` + display: flex; + align-items: center; +`; diff --git a/src/components/Dashboard/DashboardCard/types.ts b/src/components/Dashboard/DashboardCard/types.ts new file mode 100644 index 000000000..01ac54ec6 --- /dev/null +++ b/src/components/Dashboard/DashboardCard/types.ts @@ -0,0 +1,9 @@ +import { MemoExoticComponent, ReactNode } from "react"; +import { IconProps } from "../../common/icons/types"; + +export interface DashboardCardProps { + icon: MemoExoticComponent<(props: IconProps) => JSX.Element>; + title: string; + content: ReactNode; + headerContent?: ReactNode; +} diff --git a/src/components/Dashboard/ListWidget/index.tsx b/src/components/Dashboard/ListWidget/index.tsx new file mode 100644 index 000000000..1eeafdba7 --- /dev/null +++ b/src/components/Dashboard/ListWidget/index.tsx @@ -0,0 +1,251 @@ +import { useEffect, useRef, useState } from "react"; +import { PERCENTILES } from "../../../constants"; +import { dispatcher } from "../../../dispatcher"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { isNumber } from "../../../typeGuards/isNumber"; +import { isString } from "../../../typeGuards/isString"; +import { getPercentileKey } from "../../../utils/getPercentileKey"; +import { NewCircleLoader } from "../../common/NewCircleLoader"; +import { Pagination } from "../../common/Pagination"; +import { Toggle } from "../../common/Toggle"; +import { ToggleValue } from "../../common/Toggle/types"; +import { LightBulbSmallCrossedIcon } from "../../common/icons/LightBulbSmallCrossedIcon"; +import { WarningCircleLargeIcon } from "../../common/icons/WarningCircleLargeIcon"; +import { DashboardCard } from "../DashboardCard"; +import { actions } from "../actions"; +import * as s from "./styles"; +import { GetDataPayload, ListWidgetData, ListWidgetProps } from "./types"; + +const PAGE_SIZE = 4; +const DEFAULT_PERCENTILE = 0.5; +const REFRESH_INTERVAL = isNumber(window.dashboardRefreshInterval) + ? window.dashboardRefreshInterval + : 10 * 1000; // in milliseconds + +const getErrorMessage = (message?: string) => { + if (message?.endsWith("response code 400")) { + return "Failed to get widget data"; + } + + return isString(message) ? message : undefined; +}; + +const getData = (payload: GetDataPayload) => { + const { + environment, + type, + query: { page, pageSize, percentile } + } = payload; + window.sendMessageToDigma({ + action: actions.GET_DATA, + payload: { + type, + environment, + query: { + page, + pageSize: isNumber(pageSize) ? pageSize : PAGE_SIZE, + ...(isString(percentile) ? { percentile } : {}) + } + } + }); +}; + +export const ListWidget = (props: ListWidgetProps) => { + const [percentileViewMode, setPercentileViewMode] = + useState(DEFAULT_PERCENTILE); + const [isInitialLoading, setIsInitialLoading] = useState(false); + const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); + const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); + const refreshTimerId = useRef(); + const [page, setPage] = useState(0); + const previousPage = usePrevious(page); + const [data, setData] = useState>(); + const previousData = usePrevious(data); + const entries = data?.data?.entries || []; + const totalCount = data?.data?.totalCount || 0; + const errorMessage = getErrorMessage(data?.error?.message); + const percentile = getPercentileKey(percentileViewMode); + const previousPercentile = usePrevious(percentile); + const previousEnvironment = usePrevious(props.environment); + const previousType = usePrevious(props.type); + + useEffect(() => { + getData({ + type: props.type, + environment: props.environment, + query: { + page, + percentile: getPercentileKey(DEFAULT_PERCENTILE) + } + }); + setIsInitialLoading(true); + + const handleSetData = (data: unknown, timeStamp: number) => { + if ((data as ListWidgetData).type === props.type) { + setData(data as ListWidgetData); + setLastSetDataTimeStamp(timeStamp); + } + }; + + dispatcher.addActionListener(actions.SET_DATA, handleSetData); + + return () => { + dispatcher.removeActionListener(actions.SET_DATA, handleSetData); + window.clearTimeout(refreshTimerId.current); + }; + }, []); + + useEffect(() => { + if (previousLastSetDataTimeStamp !== lastSetDataTimeStamp) { + window.clearTimeout(refreshTimerId.current); + refreshTimerId.current = window.setTimeout(() => { + getData({ + type: props.type, + environment: props.environment, + query: { + page, + percentile + } + }); + }, REFRESH_INTERVAL); + } + }, [ + previousLastSetDataTimeStamp, + lastSetDataTimeStamp, + page, + percentile, + props.type, + props.environment + ]); + + useEffect(() => { + if ( + (isNumber(previousPage) && previousPage !== page) || + (isString(previousPercentile) && previousPercentile !== percentile) || + (isString(previousEnvironment) && + previousEnvironment !== props.environment) || + (isString(previousType) && previousType !== props.type) + ) { + getData({ + type: props.type, + environment: props.environment, + query: { + page, + percentile + } + }); + } + }, [ + previousPage, + page, + previousPercentile, + percentile, + props.type, + previousType, + props.environment, + previousEnvironment + ]); + + useEffect(() => { + setPage(0); + }, [props.environment]); + + useEffect(() => { + if (!props.data) { + return; + } + + setData(props.data); + }, [props.data]); + + useEffect(() => { + if (!previousData && data) { + setIsInitialLoading(false); + } + }, [previousData, data]); + + const handlePercentileToggleValueChange = (value: ToggleValue) => { + if (value !== percentileViewMode) { + setPercentileViewMode(value as number); + } + }; + + const pageStartItemNumber = page * PAGE_SIZE + 1; + const pageEndItemNumber = Math.min( + pageStartItemNumber + PAGE_SIZE - 1, + totalCount + ); + + const headerContent = props.showPercentileToggleSwitch ? ( + ({ + value: percentile.percentile, + label: percentile.label + }))} + value={percentileViewMode} + onValueChange={handlePercentileToggleValueChange} + /> + ) : undefined; + + return ( + + {entries.length === 0 ? ( + + {isInitialLoading ? ( + + ) : errorMessage ? ( + + + + + {errorMessage} + + ) : ( + <> + + + + No data + + )} + + ) : ( + <> + + + {entries.map((x) => + props.renderListItem(x, percentileViewMode) + )} + + + + + Showing{" "} + + {pageStartItemNumber} - {pageEndItemNumber} + {" "} + of {totalCount} + + + + + )} + + } + /> + ); +}; diff --git a/src/components/Dashboard/SlowQueries/styles.ts b/src/components/Dashboard/ListWidget/styles.ts similarity index 62% rename from src/components/Dashboard/SlowQueries/styles.ts rename to src/components/Dashboard/ListWidget/styles.ts index fc3381fb3..eba545400 100644 --- a/src/components/Dashboard/SlowQueries/styles.ts +++ b/src/components/Dashboard/ListWidget/styles.ts @@ -1,5 +1,4 @@ import styled from "styled-components"; -import { Link } from "../../common/Link"; export const Container = styled.div` display: flex; @@ -7,39 +6,6 @@ export const Container = styled.div` flex-grow: 1; `; -export const Header = styled.div` - display: flex; - justify-content: space-between; - padding-bottom: 12px; - border-bottom: 1px solid - ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#c9ccd6"; - case "dark": - case "dark-jetbrains": - return "#4e5157"; - } - }}; -`; - -export const Title = styled.div` - display: flex; - gap: 4px; - font-size: 14px; - font-weight: 700; - align-items: center; - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#494b57"; - case "dark": - case "dark-jetbrains": - return "#dfe1e5"; - } - }}; -`; - export const ContentContainer = styled.div` padding: 12px 0 18px; flex-grow: 1; @@ -51,33 +17,6 @@ export const EntryList = styled.div` gap: 8px; `; -export const Entry = styled.div` - display: flex; - justify-content: space-between; - gap: 4px; -`; - -export const SpanLink = styled(Link)` - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -`; - -export const Duration = styled.span` - flex-shrink: 0; - font-size: 14px; - font-weight: 500; - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#818594"; - case "dark": - case "dark-jetbrains": - return "#b4b8bf"; - } - }}; -`; - export const Footer = styled.div` display: flex; justify-content: space-between; @@ -160,5 +99,5 @@ export const ErrorMessage = styled.span` export const IconContainer = styled.div` display: flex; - align-self: center; + align-items: center; `; diff --git a/src/components/Dashboard/ListWidget/types.ts b/src/components/Dashboard/ListWidget/types.ts new file mode 100644 index 000000000..f788b9969 --- /dev/null +++ b/src/components/Dashboard/ListWidget/types.ts @@ -0,0 +1,31 @@ +import { MemoExoticComponent } from "react"; +import { PercentileKey } from "../../../types"; +import { IconProps } from "../../common/icons/types"; +import { WidgetType } from "../widgets/types"; + +export interface ListWidgetProps { + icon: MemoExoticComponent<(props: IconProps) => JSX.Element>; + title: string; + environment: string; + type: WidgetType; + data?: ListWidgetData; + showPercentileToggleSwitch?: boolean; + renderListItem: (item: T, percentileViewMode?: number) => JSX.Element; +} + +export interface ListWidgetData { + data: { + entries: T[]; + totalCount: number; + } | null; + error: { + message: string; + } | null; + type: string; +} + +export interface GetDataPayload { + environment: string; + type: WidgetType; + query: { page: number; pageSize?: number; percentile?: PercentileKey | null }; +} diff --git a/src/components/Dashboard/SlowQueries/index.tsx b/src/components/Dashboard/SlowQueries/index.tsx deleted file mode 100644 index 3993de3ff..000000000 --- a/src/components/Dashboard/SlowQueries/index.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { PERCENTILES } from "../../../constants"; -import { dispatcher } from "../../../dispatcher"; -import { usePrevious } from "../../../hooks/usePrevious"; -import { isNumber } from "../../../typeGuards/isNumber"; -import { isString } from "../../../typeGuards/isString"; -import { getPercentileKey } from "../../../utils/getPercentileKey"; -import { NewCircleLoader } from "../../common/NewCircleLoader"; -import { Pagination } from "../../common/Pagination"; -import { Toggle } from "../../common/Toggle"; -import { ToggleValue } from "../../common/Toggle/types"; -import { Tooltip } from "../../common/Tooltip"; -import { LightBulbSmallCrossedIcon } from "../../common/icons/LightBulbSmallCrossedIcon"; -import { SnailIcon } from "../../common/icons/SnailIcon"; -import { WarningCircleLargeIcon } from "../../common/icons/WarningCircleLargeIcon"; -import { actions } from "../actions"; -import * as s from "./styles"; -import { SlowQueriesData, SlowQueriesProps } from "./types"; - -const DASHBOARD_TYPE = "SlowQuery"; -const DEFAULT_PERCENTILE = 0.5; -const PAGE_SIZE = 4; -const REFRESH_INTERVAL = isNumber(window.dashboardRefreshInterval) - ? window.dashboardRefreshInterval - : 10 * 1000; // in milliseconds - -export const SlowQueries = (props: SlowQueriesProps) => { - const [percentileViewMode, setPercentileViewMode] = - useState(DEFAULT_PERCENTILE); - const [isInitialLoading, setIsInitialLoading] = useState(false); - const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); - const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); - const refreshTimerId = useRef(); - const [page, setPage] = useState(0); - const previousPage = usePrevious(page); - const [data, setData] = useState(); - const previousData = usePrevious(data); - const entries = data?.data?.entries || []; - const totalCount = data?.data?.totalCount || 0; - const error = data?.error?.message; - const percentile = getPercentileKey(percentileViewMode); - const previousPercentile = usePrevious(percentile); - const previousEnvironment = usePrevious(props.environment); - - useEffect(() => { - window.sendMessageToDigma({ - action: actions.GET_DATA, - payload: { - type: DASHBOARD_TYPE, - environment: props.environment, - query: { - page, - pageSize: PAGE_SIZE, - percentile: getPercentileKey(DEFAULT_PERCENTILE) - } - } - }); - setIsInitialLoading(true); - - const handleSetData = (data: unknown, timeStamp: number) => { - setData(data as SlowQueriesData); - setLastSetDataTimeStamp(timeStamp); - }; - - dispatcher.addActionListener(actions.SET_DATA, handleSetData); - - return () => { - dispatcher.removeActionListener(actions.SET_DATA, handleSetData); - window.clearTimeout(refreshTimerId.current); - }; - }, []); - - useEffect(() => { - if (previousLastSetDataTimeStamp !== lastSetDataTimeStamp) { - window.clearTimeout(refreshTimerId.current); - refreshTimerId.current = window.setTimeout(() => { - window.sendMessageToDigma({ - action: actions.GET_DATA, - payload: { - type: DASHBOARD_TYPE, - environment: props.environment, - query: { - page, - pageSize: PAGE_SIZE, - percentile - } - } - }); - }, REFRESH_INTERVAL); - } - }, [ - previousLastSetDataTimeStamp, - lastSetDataTimeStamp, - page, - percentile, - props.environment - ]); - - useEffect(() => { - if ( - (isNumber(previousPage) && previousPage !== page) || - (isString(previousPercentile) && previousPercentile !== percentile) || - (isString(previousEnvironment) && - previousEnvironment !== props.environment) - ) { - window.sendMessageToDigma({ - action: actions.GET_DATA, - payload: { - type: DASHBOARD_TYPE, - environment: props.environment, - query: { - page, - pageSize: PAGE_SIZE, - percentile - } - } - }); - } - }, [ - previousPage, - page, - previousPercentile, - percentile, - props.environment, - previousEnvironment - ]); - - useEffect(() => { - setPage(0); - }, [props.environment]); - - useEffect(() => { - if (!props.data) { - return; - } - - setData(props.data); - }, [props.data]); - - useEffect(() => { - if (!previousData && data) { - setIsInitialLoading(false); - } - }, [previousData, data]); - - const handlePercentileToggleValueChange = (value: ToggleValue) => { - if (value !== percentileViewMode) { - setPercentileViewMode(value as number); - } - }; - - const handleSpanClick = (spanCodeObjectId: string) => { - window.sendMessageToDigma({ - action: actions.GO_TO_SPAN, - payload: { - spanCodeObjectId, - type: DASHBOARD_TYPE - } - }); - }; - - const pageStartItemNumber = page * PAGE_SIZE + 1; - const pageEndItemNumber = Math.min( - pageStartItemNumber + PAGE_SIZE - 1, - totalCount - ); - - return ( - - - -
- -
- Slow Queries -
- ({ - value: percentile.percentile, - label: percentile.label - }))} - value={percentileViewMode} - onValueChange={handlePercentileToggleValueChange} - /> -
- {entries.length === 0 ? ( - - {isInitialLoading ? ( - - ) : error ? ( - - - - - {error} - - ) : ( - <> - - - - No data - - )} - - ) : ( - <> - - - {entries.map((x) => { - const durationKey = getPercentileKey(percentileViewMode); - const duration = durationKey ? x[durationKey] : undefined; - const durationString = duration - ? `${duration.value} ${duration.unit}` - : ""; - - return ( - - - handleSpanClick(x.spanCodeObjectId)} - > - {x.displayName} - - - {durationString} - - ); - })} - - - - - Showing{" "} - - {pageStartItemNumber} - {pageEndItemNumber} - {" "} - of {totalCount} - - - - - )} -
- ); -}; diff --git a/src/components/Dashboard/SlowQueries/types.ts b/src/components/Dashboard/SlowQueries/types.ts deleted file mode 100644 index c61489c1f..000000000 --- a/src/components/Dashboard/SlowQueries/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Duration } from "../../../globals"; - -export interface SlowQueryEntry { - spanCodeObjectId: string; - displayName: string; - p50: Duration | null; - p95: Duration | null; -} - -export interface SlowQueriesData { - data: { - entries: SlowQueryEntry[]; - totalCount: number; - } | null; - error: { - message: string; - } | null; - type: string; -} - -export interface SlowQueriesProps { - data?: SlowQueriesData; - environment: string; -} diff --git a/src/components/Dashboard/actions.ts b/src/components/Dashboard/actions.ts index db8c3bf90..3b10f1180 100644 --- a/src/components/Dashboard/actions.ts +++ b/src/components/Dashboard/actions.ts @@ -6,5 +6,7 @@ export const actions = addPrefix(ACTION_PREFIX, { INITIALIZE: "INITIALIZE", GET_DATA: "GET_DATA", SET_DATA: "SET_DATA", - GO_TO_SPAN: "GO_TO_SPAN" + GO_TO_SPAN: "GO_TO_SPAN", + GET_ENVIRONMENT_INFO: "GET_ENVIRONMENT_INFO", + SET_ENVIRONMENT_INFO: "SET_ENVIRONMENT_INFO" }); diff --git a/src/components/Dashboard/index.tsx b/src/components/Dashboard/index.tsx index 3e160d241..7fa6512b7 100644 --- a/src/components/Dashboard/index.tsx +++ b/src/components/Dashboard/index.tsx @@ -1,27 +1,57 @@ -import { useContext, useLayoutEffect } from "react"; +import { useContext, useEffect, useLayoutEffect, useState } from "react"; import { Helmet } from "react-helmet"; +import { gte, valid } from "semver"; import { useTheme } from "styled-components"; +import { actions as globalActions } from "../../actions"; +import { dispatcher } from "../../dispatcher"; import { platform } from "../../platform"; import { isString } from "../../typeGuards/isString"; import { openURLInDefaultBrowser } from "../../utils/openURLInDefaultBrowser"; import { ConfigContext } from "../common/App/ConfigContext"; import { getThemeKind } from "../common/App/styles"; +import { DeploymentType } from "../common/App/types"; +import { CircleLoader } from "../common/CircleLoader"; import { DigmaLogoIcon } from "../common/icons/DigmaLogoIcon"; import { OpenLinkIcon } from "../common/icons/OpenLinkIcon"; -import { SlowQueries } from "./SlowQueries"; import { actions } from "./actions"; import * as s from "./styles"; +import { EnvironmentInfoData } from "./types"; +import { ClientSpansPerformanceImpact } from "./widgets/ClientSpansPerformanceImpact"; +import { SlowQueries } from "./widgets/SlowQueries"; const DIGMA_UI_DEFAULT_PORT = 5280; +const formatEnvironmentName = (environment: string) => { + const suffixes = ["LOCAL", "LOCAL-TESTS"]; + + for (const suffix of suffixes) { + if (environment.endsWith(`[${suffix}]`)) { + return suffix; + } + } + + return environment; +}; + const environment = isString(window.dashboardEnvironment) ? window.dashboardEnvironment : new URLSearchParams(window.location.search).get("environment") || ""; export const Dashboard = () => { + const [isInitialLoading, setIsInitialLoading] = useState(true); const config = useContext(ConfigContext); const theme = useTheme(); const themeKind = getThemeKind(theme); + const [environmentName, setEnvironmentName] = useState( + platform === "Web" ? "" : formatEnvironmentName(environment) + ); + + const backendVersion = config.backendInfo?.applicationVersion; + + const isClientSpansOverallImpactEnabled = + backendVersion && + (backendVersion === "unknown" || + (valid(backendVersion) && gte(backendVersion, "v0.2.172-alpha.8"))); const handleOpenInBrowserLinkClick = () => { const hostname = new URL(config.digmaApiUrl).hostname; @@ -39,12 +69,49 @@ export const Dashboard = () => { window.sendMessageToDigma({ action: actions.INITIALIZE }); + window.sendMessageToDigma({ + action: globalActions.GET_BACKEND_INFO + }); + window.sendMessageToDigma({ + action: actions.GET_ENVIRONMENT_INFO + }); + + const handleSetEnvironmentInfoData = (data: unknown) => { + const environmentInfo = data as EnvironmentInfoData; + if (environmentInfo.data) { + setEnvironmentName(environmentInfo.data.displayName); + } else { + setEnvironmentName(formatEnvironmentName(environment)); + } + }; + + dispatcher.addActionListener( + actions.SET_ENVIRONMENT_INFO, + handleSetEnvironmentInfoData + ); + + return () => { + dispatcher.removeActionListener( + actions.SET_ENVIRONMENT_INFO, + handleSetEnvironmentInfoData + ); + }; }, []); + useEffect(() => { + if (config.backendInfo) { + setIsInitialLoading(false); + } + }, [config.backendInfo]); + + const title = `Dashboard${ + environmentName.length > 0 ? ` - ${environmentName}` : "" + }`; + return ( - Dashboard - {environment} + {title} @@ -53,22 +120,38 @@ export const Dashboard = () => { Dashboard {environment.length > 0 && ( - {environment} + {environmentName} )} - {platform !== "Web" && ( - - Open in browser - - - - - )} + {platform !== "Web" && + config.backendInfo && + !( + [ + DeploymentType.DOCKER_COMPOSE, + DeploymentType.DOCKER_EXTENSION + ] as string[] + ).includes(config.backendInfo.deploymentType) && ( + + Open in browser + + + + + )} - - - + {isInitialLoading ? ( + + + + ) : ( + <> + + {isClientSpansOverallImpactEnabled && ( + + )} + + )} ); diff --git a/src/components/Dashboard/styles.ts b/src/components/Dashboard/styles.ts index dc4c499c6..8b3d27ccf 100644 --- a/src/components/Dashboard/styles.ts +++ b/src/components/Dashboard/styles.ts @@ -26,7 +26,7 @@ export const Header = styled.div` export const IconContainer = styled.div` display: flex; - align-self: center; + align-items: center; `; export const Title = styled.div` @@ -66,23 +66,9 @@ export const ContentContainer = styled.div` gap: 8px; `; -export const DashboardCard = styled.div` +export const LoaderContainer = styled.div` display: flex; - flex-direction: column; - flex-basis: calc(50% - 4px); - border-radius: 4px; - padding: 12px; - gap: 12px; - box-sizing: border-box; - overflow: auto; - height: 231px; - background: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#ebecf0"; - case "dark": - case "dark-jetbrains": - return "#393b40"; - } - }}; + justify-content: center; + margin-top: 50px; + flex-grow: 1; `; diff --git a/src/components/Dashboard/types.ts b/src/components/Dashboard/types.ts new file mode 100644 index 000000000..07e24f4b0 --- /dev/null +++ b/src/components/Dashboard/types.ts @@ -0,0 +1,8 @@ +import { GetEnvironmentResponse } from "../../api/web/services/environments"; + +export interface EnvironmentInfoData { + data: GetEnvironmentResponse | null; + error: { + message: string; + } | null; +} diff --git a/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/ClientSpansPerformanceImpact.stories.tsx b/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/ClientSpansPerformanceImpact.stories.tsx new file mode 100644 index 000000000..acee92c25 --- /dev/null +++ b/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/ClientSpansPerformanceImpact.stories.tsx @@ -0,0 +1,69 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { ClientSpansPerformanceImpact } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Dashboard/widgets/ClientSpansPerformanceImpact", + component: ClientSpansPerformanceImpact, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const WithData: Story = { + args: { + data: { + data: { + totalCount: 27, + entries: [ + { + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /owners/new", + displayName: "GET PetClinic /owners/new", + overallImpact: 1.0 + }, + { + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/SlowEndpoint", + displayName: "GET PetClinic /SampleInsights/SlowEndpoint", + overallImpact: 0.5411061584203873 + }, + { + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /SampleInsights/SpanBottleneck", + displayName: "GET PetClinic /SampleInsights/SpanBottleneck", + overallImpact: 0.015331544891429908 + }, + { + spanCodeObjectId: + "span:io.opentelemetry.okhttp-3.0$_$GET PetClinic /owners/{ownerId}", + displayName: "GET PetClinic /owners/{ownerId}", + overallImpact: 0.01180952993819851 + } + ] + }, + type: "ClientSpanOverallImpact", + error: null + } + } +}; + +export const NoData: Story = { + args: { + data: { + data: { + totalCount: 0, + entries: [] + }, + type: "ClientSpanOverallImpact", + error: null + } + } +}; diff --git a/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/index.tsx b/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/index.tsx new file mode 100644 index 000000000..23406099a --- /dev/null +++ b/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/index.tsx @@ -0,0 +1,55 @@ +import { ImpactScore } from "../../../common/ImpactScore"; +import { Tooltip } from "../../../common/Tooltip"; +import { AlarmClockIcon } from "../../../common/icons/AlarmClockIcon"; +import { ListWidget } from "../../ListWidget"; +import { actions } from "../../actions"; +import { WidgetType } from "../types"; +import * as s from "./styles"; +import { + ClientSpanOverallImpactEntry, + ClientSpansPerformanceImpactProps +} from "./types"; + +const renderClientSpanOverallImpactEntry = ( + item: ClientSpanOverallImpactEntry +) => { + const handleSpanClick = (spanCodeObjectId: string) => { + window.sendMessageToDigma({ + action: actions.GO_TO_SPAN, + payload: { + spanCodeObjectId, + type: WidgetType.CLIENT_SPANS_PERFORMANCE_IMPACT + } + }); + }; + + return ( + + + handleSpanClick(item.spanCodeObjectId)}> + {item.displayName} + + + + + + + ); +}; + +export const ClientSpansPerformanceImpact = ( + props: ClientSpansPerformanceImpactProps +) => ( + + title={"Client Spans Performance Impact"} + type={WidgetType.CLIENT_SPANS_PERFORMANCE_IMPACT} + icon={AlarmClockIcon} + data={props.data} + environment={props.environment} + renderListItem={renderClientSpanOverallImpactEntry} + /> +); diff --git a/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/styles.ts b/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/styles.ts new file mode 100644 index 000000000..3390e1fa6 --- /dev/null +++ b/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/styles.ts @@ -0,0 +1,20 @@ +import styled from "styled-components"; +import { Link } from "../../../common/Link"; + +export const Entry = styled.div` + display: flex; + justify-content: space-between; + gap: 4px; +`; + +export const SpanLink = styled(Link)` + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; + +export const ImpactScoreContainer = styled.div` + display: flex; + width: 70px; + flex-shrink: 0; +`; diff --git a/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/types.ts b/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/types.ts new file mode 100644 index 000000000..c108796f8 --- /dev/null +++ b/src/components/Dashboard/widgets/ClientSpansPerformanceImpact/types.ts @@ -0,0 +1,12 @@ +import { ListWidgetData } from "../../ListWidget/types"; + +export interface ClientSpanOverallImpactEntry { + spanCodeObjectId: string; + displayName: string; + overallImpact: number; +} + +export interface ClientSpansPerformanceImpactProps { + data?: ListWidgetData; + environment: string; +} diff --git a/src/components/Dashboard/SlowQueries/SlowQueries.stories.tsx b/src/components/Dashboard/widgets/SlowQueries/SlowQueries.stories.tsx similarity index 98% rename from src/components/Dashboard/SlowQueries/SlowQueries.stories.tsx rename to src/components/Dashboard/widgets/SlowQueries/SlowQueries.stories.tsx index 33fa77a9c..9bde2f876 100644 --- a/src/components/Dashboard/SlowQueries/SlowQueries.stories.tsx +++ b/src/components/Dashboard/widgets/SlowQueries/SlowQueries.stories.tsx @@ -4,7 +4,7 @@ import { SlowQueries } from "."; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta: Meta = { - title: "Dashboard/SlowQueries", + title: "Dashboard/widgets/SlowQueries", component: SlowQueries, parameters: { // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout diff --git a/src/components/Dashboard/widgets/SlowQueries/index.tsx b/src/components/Dashboard/widgets/SlowQueries/index.tsx new file mode 100644 index 000000000..130a2143a --- /dev/null +++ b/src/components/Dashboard/widgets/SlowQueries/index.tsx @@ -0,0 +1,53 @@ +import { getPercentileKey } from "../../../../utils/getPercentileKey"; +import { Tooltip } from "../../../common/Tooltip"; +import { SnailIcon } from "../../../common/icons/SnailIcon"; +import { ListWidget } from "../../ListWidget"; +import { actions } from "../../actions"; +import { WidgetType } from "../types"; +import * as s from "./styles"; +import { SlowQueriesProps, SlowQueryEntry } from "./types"; + +const renderSlowQueryEntry = ( + item: SlowQueryEntry, + percentileViewMode?: number +) => { + let durationString = ""; + if (percentileViewMode) { + const durationKey = getPercentileKey(percentileViewMode); + const duration = durationKey ? item[durationKey] : undefined; + durationString = duration ? `${duration.value} ${duration.unit}` : ""; + } + + const handleSpanClick = (spanCodeObjectId: string) => { + window.sendMessageToDigma({ + action: actions.GO_TO_SPAN, + payload: { + spanCodeObjectId, + type: WidgetType.SLOW_QUERIES + } + }); + }; + + return ( + + + handleSpanClick(item.spanCodeObjectId)}> + {item.displayName} + + + {durationString && {durationString}} + + ); +}; + +export const SlowQueries = (props: SlowQueriesProps) => ( + + title={"Slow queries"} + type={WidgetType.SLOW_QUERIES} + icon={SnailIcon} + data={props.data} + environment={props.environment} + renderListItem={renderSlowQueryEntry} + showPercentileToggleSwitch={true} + /> +); diff --git a/src/components/Dashboard/widgets/SlowQueries/styles.ts b/src/components/Dashboard/widgets/SlowQueries/styles.ts new file mode 100644 index 000000000..16c3cea2e --- /dev/null +++ b/src/components/Dashboard/widgets/SlowQueries/styles.ts @@ -0,0 +1,29 @@ +import styled from "styled-components"; +import { Link } from "../../../common/Link"; + +export const Entry = styled.div` + display: flex; + justify-content: space-between; + gap: 4px; +`; + +export const SpanLink = styled(Link)` + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; + +export const Duration = styled.span` + flex-shrink: 0; + font-size: 14px; + font-weight: 500; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#818594"; + case "dark": + case "dark-jetbrains": + return "#b4b8bf"; + } + }}; +`; diff --git a/src/components/Dashboard/widgets/SlowQueries/types.ts b/src/components/Dashboard/widgets/SlowQueries/types.ts new file mode 100644 index 000000000..e84e06b17 --- /dev/null +++ b/src/components/Dashboard/widgets/SlowQueries/types.ts @@ -0,0 +1,14 @@ +import { Duration } from "../../../../globals"; +import { ListWidgetData } from "../../ListWidget/types"; + +export interface SlowQueryEntry { + spanCodeObjectId: string; + displayName: string; + p50: Duration | null; + p95: Duration | null; +} + +export interface SlowQueriesProps { + environment: string; + data?: ListWidgetData; +} diff --git a/src/components/Dashboard/widgets/types.ts b/src/components/Dashboard/widgets/types.ts new file mode 100644 index 000000000..ae2341bcc --- /dev/null +++ b/src/components/Dashboard/widgets/types.ts @@ -0,0 +1,4 @@ +export enum WidgetType { + SLOW_QUERIES = "SlowQuery", + CLIENT_SPANS_PERFORMANCE_IMPACT = "ClientSpanOverallImpact" +} diff --git a/src/components/common/App/ConfigContext.ts b/src/components/common/App/ConfigContext.ts index 730a10bf5..335bb9814 100644 --- a/src/components/common/App/ConfigContext.ts +++ b/src/components/common/App/ConfigContext.ts @@ -12,5 +12,6 @@ export const ConfigContext = createContext({ isDockerInstalled: window.isDockerInstalled === true, isDockerComposeInstalled: window.isDockerComposeInstalled === true, userEmail: isString(window.userEmail) ? window.userEmail : "", - environment: isString(window.environment) ? window.environment : "" + environment: isString(window.environment) ? window.environment : "", + backendInfo: undefined }); diff --git a/src/components/common/App/index.tsx b/src/components/common/App/index.tsx index cdfd93410..7d103c8b1 100644 --- a/src/components/common/App/index.tsx +++ b/src/components/common/App/index.tsx @@ -9,10 +9,15 @@ import { isObject } from "../../../typeGuards/isObject"; import { isString } from "../../../typeGuards/isString"; import { ConfigContext } from "./ConfigContext"; import { GlobalStyle } from "./styles"; -import { AppProps, DigmaStatus } from "./types"; +import { AppProps, BackendInfo, DigmaStatus } from "./types"; export const THEMES = ["light", "dark", "dark-jetbrains"]; +export const isBackendInfo = (info: unknown): info is BackendInfo => + isObject(info) && + isString(info.applicationVersion) && + isString(info.deploymentType); + const isMode = (mode: unknown): mode is Mode => isString(mode) && THEMES.includes(mode); @@ -157,6 +162,15 @@ export const App = (props: AppProps) => { } }; + const handleSetBackendInfo = (data: unknown) => { + if (isBackendInfo(data)) { + setConfig((config) => ({ + ...config, + backendInfo: data + })); + } + }; + dispatcher.addActionListener(actions.SET_THEME, handleSetTheme); dispatcher.addActionListener(actions.SET_MAIN_FONT, handleSetMainFont); dispatcher.addActionListener(actions.SET_CODE_FONT, handleSetCodeFont); @@ -194,6 +208,10 @@ export const App = (props: AppProps) => { actions.SET_IS_OBSERVABILITY_ENABLED, handleIsObservabilityEnabled ); + dispatcher.addActionListener( + actions.SET_BACKEND_INFO, + handleSetBackendInfo + ); return () => { dispatcher.removeActionListener(actions.SET_THEME, handleSetTheme); @@ -239,6 +257,10 @@ export const App = (props: AppProps) => { actions.SET_IS_OBSERVABILITY_ENABLED, handleIsObservabilityEnabled ); + dispatcher.removeActionListener( + actions.SET_BACKEND_INFO, + handleSetBackendInfo + ); }; }, []); diff --git a/src/components/common/App/types.ts b/src/components/common/App/types.ts index 60cd8ee3b..757d6e5d5 100644 --- a/src/components/common/App/types.ts +++ b/src/components/common/App/types.ts @@ -18,6 +18,16 @@ export type DigmaStatus = { runningDigmaInstances: InstallationType[]; }; +export interface BackendInfo { + applicationVersion: string; + deploymentType: DeploymentType; +} + +export enum DeploymentType { + DOCKER_COMPOSE = "DockerCompose", + DOCKER_EXTENSION = "DockerExtension" +} + export interface ConfigContextData { digmaApiUrl: string; digmaStatus: DigmaStatus | undefined; @@ -29,4 +39,5 @@ export interface ConfigContextData { isDockerComposeInstalled: boolean; userEmail: string; environment: string; + backendInfo: BackendInfo | undefined; } diff --git a/src/components/common/ImpactScore/index.tsx b/src/components/common/ImpactScore/index.tsx new file mode 100644 index 000000000..84bab561c --- /dev/null +++ b/src/components/common/ImpactScore/index.tsx @@ -0,0 +1,47 @@ +import { Tooltip } from "../Tooltip"; +import * as s from "./styles"; +import { ImpactScoreProps } from "./types"; + +const getImpactScoreLabel = (score: number) => { + if (score < 0) { + return "No data"; + } + + if (score < 0.4) { + return "Low"; + } + + if (score < 0.8) { + return "Medium"; + } + + return "High"; +}; + +const renderIndicator = (score: number) => ( + + + +); + +export const ImpactScore = (props: ImpactScoreProps) => { + let indicatorPosition: "start" | "end" | undefined; + + if (props.score >= 0 && props.showIndicator) { + indicatorPosition = "end"; + + if (props.indicatorPosition) { + indicatorPosition = props.indicatorPosition; + } + } + + return ( + + + {indicatorPosition === "start" && renderIndicator(props.score)} + {getImpactScoreLabel(props.score)} + {indicatorPosition === "end" && renderIndicator(props.score)} + + + ); +}; diff --git a/src/components/common/ImpactScore/styles.ts b/src/components/common/ImpactScore/styles.ts new file mode 100644 index 000000000..40fa7b1a5 --- /dev/null +++ b/src/components/common/ImpactScore/styles.ts @@ -0,0 +1,33 @@ +import styled from "styled-components"; +import { ImpactScoreIndicatorProps } from "./types"; + +export const Container = styled.div` + width: 100%; + display: flex; + gap: 6px; + align-items: center; + font-size: 14px; + font-weight: 500; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#4d668a"; + case "dark": + case "dark-jetbrains": + return "#c6c6c6"; + } + }}; +`; + +export const IndicatorContainer = styled.div` + display: flex; + align-items: center; + height: 100%; +`; + +export const Indicator = styled.div` + border-radius: 50%; + width: 10px; + height: 10px; + background: hsl(14deg 66% ${({ $score }) => 100 - 50 * $score}%); +`; diff --git a/src/components/common/ImpactScore/types.ts b/src/components/common/ImpactScore/types.ts new file mode 100644 index 000000000..10bb12279 --- /dev/null +++ b/src/components/common/ImpactScore/types.ts @@ -0,0 +1,9 @@ +export interface ImpactScoreProps { + score: number; + showIndicator?: boolean; + indicatorPosition?: "start" | "end"; +} + +export interface ImpactScoreIndicatorProps { + $score: number; +}