Compare commits
9 Commits
fix/instal
...
v0.70.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
536656281d | ||
|
|
c6fda99b9b | ||
|
|
bbf64a7b52 | ||
|
|
5783f1555f | ||
|
|
93a8f97355 | ||
|
|
a84e462a65 | ||
|
|
d910d99689 | ||
|
|
e542e96031 | ||
|
|
3849ca1ecc |
@@ -181,7 +181,7 @@ services:
|
||||
- query-service
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:0.70.0
|
||||
image: signoz/query-service:0.70.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --use-logs-new-schema=true
|
||||
@@ -214,7 +214,7 @@ services:
|
||||
retries: 3
|
||||
frontend:
|
||||
!!merge <<: *common
|
||||
image: signoz/frontend:0.70.0
|
||||
image: signoz/frontend:0.70.1
|
||||
depends_on:
|
||||
- alertmanager
|
||||
- query-service
|
||||
@@ -224,7 +224,7 @@ services:
|
||||
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:0.111.24
|
||||
image: signoz/signoz-otel-collector:0.111.25
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -262,7 +262,6 @@ services:
|
||||
networks:
|
||||
signoz-net:
|
||||
name: signoz-net
|
||||
|
||||
volumes:
|
||||
alertmanager:
|
||||
name: signoz-alertmanager
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
- query-service
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:0.70.0
|
||||
image: signoz/query-service:0.70.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --use-logs-new-schema=true
|
||||
@@ -150,7 +150,7 @@ services:
|
||||
retries: 3
|
||||
frontend:
|
||||
!!merge <<: *common
|
||||
image: signoz/frontend:0.70.0
|
||||
image: signoz/frontend:0.70.1
|
||||
depends_on:
|
||||
- alertmanager
|
||||
- query-service
|
||||
@@ -160,7 +160,7 @@ services:
|
||||
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:0.111.24
|
||||
image: signoz/signoz-otel-collector:0.111.25
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -198,7 +198,6 @@ services:
|
||||
networks:
|
||||
signoz-net:
|
||||
name: signoz-net
|
||||
|
||||
volumes:
|
||||
alertmanager:
|
||||
name: signoz-alertmanager
|
||||
|
||||
@@ -188,7 +188,7 @@ services:
|
||||
condition: service_healthy
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.70.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -222,7 +222,7 @@ services:
|
||||
retries: 3
|
||||
frontend:
|
||||
!!merge <<: *common
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.70.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
|
||||
container_name: signoz-frontend
|
||||
depends_on:
|
||||
- alertmanager
|
||||
@@ -234,7 +234,7 @@ services:
|
||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -280,7 +280,6 @@ services:
|
||||
networks:
|
||||
signoz-net:
|
||||
name: signoz-net
|
||||
|
||||
volumes:
|
||||
alertmanager:
|
||||
name: signoz-alertmanager
|
||||
|
||||
@@ -121,7 +121,7 @@ services:
|
||||
condition: service_healthy
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.70.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -157,7 +157,7 @@ services:
|
||||
retries: 3
|
||||
frontend:
|
||||
!!merge <<: *common
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.70.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
|
||||
container_name: signoz-frontend
|
||||
depends_on:
|
||||
- alertmanager
|
||||
@@ -168,7 +168,7 @@ services:
|
||||
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -210,7 +210,6 @@ services:
|
||||
networks:
|
||||
signoz-net:
|
||||
name: signoz-net
|
||||
|
||||
volumes:
|
||||
alertmanager:
|
||||
name: signoz-alertmanager
|
||||
|
||||
@@ -121,7 +121,7 @@ services:
|
||||
condition: service_healthy
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.70.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -155,7 +155,7 @@ services:
|
||||
retries: 3
|
||||
frontend:
|
||||
!!merge <<: *common
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.70.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
|
||||
container_name: signoz-frontend
|
||||
depends_on:
|
||||
- alertmanager
|
||||
@@ -166,7 +166,7 @@ services:
|
||||
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -208,7 +208,6 @@ services:
|
||||
networks:
|
||||
signoz-net:
|
||||
name: signoz-net
|
||||
|
||||
volumes:
|
||||
alertmanager:
|
||||
name: signoz-alertmanager
|
||||
|
||||
@@ -36,6 +36,7 @@ type APIHandlerOptions struct {
|
||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||
Cache cache.Cache
|
||||
Gateway *httputil.ReverseProxy
|
||||
GatewayUrl string
|
||||
// Querier Influx Interval
|
||||
FluxInterval time.Duration
|
||||
UseLogsNewSchema bool
|
||||
@@ -181,6 +182,17 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
|
||||
}
|
||||
|
||||
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *baseapp.AuthMiddleware) {
|
||||
|
||||
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
|
||||
|
||||
router.HandleFunc(
|
||||
"/api/v1/cloud-integrations/{cloudProvider}/accounts/generate-connection-params",
|
||||
am.EditAccess(ah.CloudIntegrationsGenerateConnectionParams),
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
|
||||
version := version.GetVersion()
|
||||
versionResponse := basemodel.GetVersionResponse{
|
||||
|
||||
305
ee/query-service/app/api/cloudIntegrations.go
Normal file
305
ee/query-service/app/api/cloudIntegrations.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/ee/query-service/constants"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CloudIntegrationConnectionParamsResponse struct {
|
||||
IngestionUrl string `json:"ingestion_url,omitempty"`
|
||||
IngestionKey string `json:"ingestion_key,omitempty"`
|
||||
SigNozAPIUrl string `json:"signoz_api_url,omitempty"`
|
||||
}
|
||||
|
||||
func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseWriter, r *http.Request) {
|
||||
cloudProvider := mux.Vars(r)["cloudProvider"]
|
||||
if cloudProvider != "aws" {
|
||||
RespondError(w, basemodel.BadRequest(fmt.Errorf(
|
||||
"cloud provider not supported: %s", cloudProvider,
|
||||
)), nil)
|
||||
return
|
||||
}
|
||||
|
||||
license, err := ah.LM().GetRepo().GetActiveLicense(r.Context())
|
||||
if err != nil {
|
||||
RespondError(w, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't look for active license: %w", err,
|
||||
)), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if license == nil {
|
||||
RespondError(w, basemodel.ForbiddenError(fmt.Errorf(
|
||||
"no active license found",
|
||||
)), nil)
|
||||
return
|
||||
}
|
||||
|
||||
ingestionUrl, signozApiUrl, err := getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
|
||||
if err != nil {
|
||||
RespondError(w, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't deduce ingestion url and signoz api url: %w", err,
|
||||
)), nil)
|
||||
return
|
||||
}
|
||||
|
||||
result := CloudIntegrationConnectionParamsResponse{
|
||||
IngestionUrl: ingestionUrl,
|
||||
SigNozAPIUrl: signozApiUrl,
|
||||
}
|
||||
|
||||
gatewayUrl := ah.opts.GatewayUrl
|
||||
if len(gatewayUrl) > 0 {
|
||||
|
||||
ingestionKey, err := getOrCreateCloudProviderIngestionKey(
|
||||
r.Context(), gatewayUrl, license.Key, cloudProvider,
|
||||
)
|
||||
if err != nil {
|
||||
RespondError(w, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't get or create ingestion key: %w", err,
|
||||
)), nil)
|
||||
return
|
||||
}
|
||||
|
||||
result.IngestionKey = ingestionKey
|
||||
|
||||
} else {
|
||||
zap.L().Info("ingestion key can't be deduced since no gateway url has been configured")
|
||||
}
|
||||
|
||||
ah.Respond(w, result)
|
||||
}
|
||||
|
||||
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||
string, string, *basemodel.ApiError,
|
||||
) {
|
||||
url := fmt.Sprintf(
|
||||
"%s%s",
|
||||
strings.TrimSuffix(constants.ZeusURL, "/"),
|
||||
"/v2/deployments/me",
|
||||
)
|
||||
|
||||
type deploymentResponse struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
Data struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
ClusterInfo struct {
|
||||
Region struct {
|
||||
DNS string `json:"dns"`
|
||||
} `json:"region"`
|
||||
} `json:"cluster"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
resp, apiErr := requestAndParseResponse[deploymentResponse](
|
||||
ctx, url, map[string]string{"X-Signoz-Cloud-Api-Key": licenseKey}, nil,
|
||||
)
|
||||
|
||||
if apiErr != nil {
|
||||
return "", "", basemodel.WrapApiError(
|
||||
apiErr, "couldn't query for deployment info",
|
||||
)
|
||||
}
|
||||
|
||||
if resp.Status != "success" {
|
||||
return "", "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't query for deployment info: status: %s, error: %s",
|
||||
resp.Status, resp.Error,
|
||||
))
|
||||
}
|
||||
|
||||
regionDns := resp.Data.ClusterInfo.Region.DNS
|
||||
deploymentName := resp.Data.Name
|
||||
|
||||
if len(regionDns) < 1 || len(deploymentName) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", "", basemodel.InternalError(fmt.Errorf(
|
||||
"deployment info response not in expected shape. couldn't determine region dns and deployment name",
|
||||
))
|
||||
}
|
||||
|
||||
ingestionUrl := fmt.Sprintf("https://ingest.%s", regionDns)
|
||||
|
||||
signozApiUrl := fmt.Sprintf("https://%s.%s", deploymentName, regionDns)
|
||||
|
||||
return ingestionUrl, signozApiUrl, nil
|
||||
}
|
||||
|
||||
type ingestionKey struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
// other attributes from gateway response not included here since they are not being used.
|
||||
}
|
||||
|
||||
type ingestionKeysSearchResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data []ingestionKey `json:"data"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type createIngestionKeyResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data ingestionKey `json:"data"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func getOrCreateCloudProviderIngestionKey(
|
||||
ctx context.Context, gatewayUrl string, licenseKey string, cloudProvider string,
|
||||
) (string, *basemodel.ApiError) {
|
||||
cloudProviderKeyName := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
|
||||
// see if the key already exists
|
||||
searchResult, apiErr := requestGateway[ingestionKeysSearchResponse](
|
||||
ctx,
|
||||
gatewayUrl,
|
||||
licenseKey,
|
||||
fmt.Sprintf("/v1/workspaces/me/keys/search?name=%s", cloudProviderKeyName),
|
||||
nil,
|
||||
)
|
||||
|
||||
if apiErr != nil {
|
||||
return "", basemodel.WrapApiError(
|
||||
apiErr, "couldn't search for cloudprovider ingestion key",
|
||||
)
|
||||
}
|
||||
|
||||
if searchResult.Status != "success" {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't search for cloudprovider ingestion key: status: %s, error: %s",
|
||||
searchResult.Status, searchResult.Error,
|
||||
))
|
||||
}
|
||||
|
||||
for _, k := range searchResult.Data {
|
||||
if k.Name == cloudProviderKeyName {
|
||||
if len(k.Value) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"ingestion keys search response not as expected",
|
||||
))
|
||||
}
|
||||
|
||||
return k.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
zap.L().Info(
|
||||
"no existing ingestion key found for cloud integration, creating a new one",
|
||||
zap.String("cloudProvider", cloudProvider),
|
||||
)
|
||||
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
|
||||
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
|
||||
map[string]any{
|
||||
"name": cloudProviderKeyName,
|
||||
"tags": []string{"integration", cloudProvider},
|
||||
},
|
||||
)
|
||||
if apiErr != nil {
|
||||
return "", basemodel.WrapApiError(
|
||||
apiErr, "couldn't create cloudprovider ingestion key",
|
||||
)
|
||||
}
|
||||
|
||||
if createKeyResult.Status != "success" {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't create cloudprovider ingestion key: status: %s, error: %s",
|
||||
createKeyResult.Status, createKeyResult.Error,
|
||||
))
|
||||
}
|
||||
|
||||
ingestionKey := createKeyResult.Data.Value
|
||||
if len(ingestionKey) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"ingestion key creation response not as expected",
|
||||
))
|
||||
}
|
||||
|
||||
return ingestionKey, nil
|
||||
}
|
||||
|
||||
func requestGateway[ResponseType any](
|
||||
ctx context.Context, gatewayUrl string, licenseKey string, path string, payload any,
|
||||
) (*ResponseType, *basemodel.ApiError) {
|
||||
|
||||
baseUrl := strings.TrimSuffix(gatewayUrl, "/")
|
||||
reqUrl := fmt.Sprintf("%s%s", baseUrl, path)
|
||||
|
||||
headers := map[string]string{
|
||||
"X-Signoz-Cloud-Api-Key": licenseKey,
|
||||
"X-Consumer-Username": "lid:00000000-0000-0000-0000-000000000000",
|
||||
"X-Consumer-Groups": "ns:default",
|
||||
}
|
||||
|
||||
return requestAndParseResponse[ResponseType](ctx, reqUrl, headers, payload)
|
||||
}
|
||||
|
||||
func requestAndParseResponse[ResponseType any](
|
||||
ctx context.Context, url string, headers map[string]string, payload any,
|
||||
) (*ResponseType, *basemodel.ApiError) {
|
||||
|
||||
reqMethod := http.MethodGet
|
||||
var reqBody io.Reader
|
||||
if payload != nil {
|
||||
reqMethod = http.MethodPost
|
||||
|
||||
bodyJson, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't serialize request payload to JSON: %w", err,
|
||||
))
|
||||
}
|
||||
reqBody = bytes.NewBuffer([]byte(bodyJson))
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, reqMethod, url, reqBody)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't prepare request: %w", err,
|
||||
))
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't make request: %w", err))
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't read response: %w", err))
|
||||
}
|
||||
|
||||
var resp ResponseType
|
||||
|
||||
err = json.Unmarshal(respBody, &resp)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't unmarshal gateway response into %T", resp,
|
||||
))
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
@@ -272,6 +272,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
Cache: c,
|
||||
FluxInterval: fluxInterval,
|
||||
Gateway: gatewayProxy,
|
||||
GatewayUrl: serverOptions.GatewayUrl,
|
||||
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
|
||||
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-lottie": "1.2.10",
|
||||
"react-markdown": "8.0.7",
|
||||
"react-query": "3.39.3",
|
||||
"react-redux": "^7.2.2",
|
||||
@@ -178,6 +179,7 @@
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@types/react-grid-layout": "^1.1.2",
|
||||
"@types/react-helmet-async": "1.0.3",
|
||||
"@types/react-lottie": "1.2.10",
|
||||
"@types/react-redux": "^7.1.11",
|
||||
"@types/react-resizable": "3.0.3",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
@@ -218,6 +220,7 @@
|
||||
"portfinder-sync": "^0.0.2",
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "2.2.1",
|
||||
"prop-types": "15.8.1",
|
||||
"raw-loader": "4.0.2",
|
||||
"react-hooks-testing-library": "0.6.0",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
|
||||
6
frontend/public/Icons/solid-check-circle.svg
Normal file
6
frontend/public/Icons/solid-check-circle.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="57" height="57" viewBox="0 0 57 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="solid-check-circle-2">
|
||||
<path id="Vector" d="M28.2498 51.9638C41.1368 51.9638 51.5832 41.5175 51.5832 28.6305C51.5832 15.7435 41.1368 5.29712 28.2498 5.29712C15.3628 5.29712 4.9165 15.7435 4.9165 28.6305C4.9165 41.5175 15.3628 51.9638 28.2498 51.9638Z" fill="#4E74F8" stroke="#4E74F8" stroke-width="4.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2" d="M16.25 27.6304L24.2115 36.6304L39.25 22.6304" stroke="#121317" stroke-width="4.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 629 B |
BIN
frontend/public/Images/integrations-hero-bg.png
Normal file
BIN
frontend/public/Images/integrations-hero-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
23
frontend/public/Logos/aws-dark.svg
Normal file
23
frontend/public/Logos/aws-dark.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="amazon_web_services_logo.svg" clip-path="url(#clip0_2552_26387)">
|
||||
<g id="Group">
|
||||
<path id="Vector" d="M6.82058 6.05151C6.82058 6.34672 6.85997 6.58289 6.89935 6.76001C6.95843 6.93714 7.03721 7.11426 7.15537 7.33075C7.19476 7.38979 7.21445 7.44883 7.21445 7.50788C7.21445 7.5866 7.17506 7.66532 7.0569 7.74404L6.56456 8.07861C6.48579 8.11797 6.42671 8.15733 6.36762 8.15733C6.28885 8.15733 6.21008 8.11797 6.1313 8.03925C6.01314 7.92117 5.93437 7.80308 5.85559 7.66532C5.77682 7.52756 5.69804 7.38979 5.61927 7.19299C5.00876 7.92117 4.24071 8.27542 3.29542 8.27542C2.62584 8.27542 2.1138 8.07861 1.71993 7.70468C1.32606 7.33075 1.12912 6.81906 1.12912 6.18928C1.12912 5.52014 1.36544 4.96908 1.83809 4.57547C2.31074 4.18186 2.96063 3.96538 3.76807 3.96538C4.04378 3.96538 4.31949 3.98506 4.5952 4.02442C4.8906 4.06378 5.18601 4.12282 5.50111 4.20154V3.63081C5.50111 3.04039 5.38294 2.60742 5.12693 2.37125C4.87091 2.13508 4.45734 2.017 3.84684 2.017C3.57113 2.017 3.29542 2.05636 3.00001 2.1154C2.70461 2.19413 2.4289 2.27285 2.15319 2.39093C2.03503 2.43029 1.93656 2.46965 1.87748 2.48933C1.8184 2.48933 1.77901 2.50901 1.75932 2.50901C1.64115 2.50901 1.60177 2.43029 1.60177 2.27285V1.87924C1.60177 1.76115 1.62146 1.66275 1.66085 1.60371C1.70024 1.54467 1.77901 1.48563 1.87748 1.44626C2.15319 1.3085 2.48798 1.19042 2.86216 1.09201C3.25603 0.993612 3.6499 0.93457 4.08316 0.93457C5.02846 0.93457 5.69804 1.15106 6.15099 1.56435C6.58425 1.99732 6.80088 2.6271 6.80088 3.51272L6.82058 6.05151ZM3.63021 7.25203C3.88623 7.25203 4.16194 7.21267 4.43765 7.11426C4.71336 7.01586 4.96938 6.83874 5.18601 6.60257C5.30417 6.44513 5.40264 6.28768 5.46172 6.09088C5.50111 5.89407 5.54049 5.67758 5.54049 5.40206V5.06749C5.30417 5.00845 5.06785 4.96908 4.81183 4.92972C4.55581 4.89036 4.31949 4.89036 4.06347 4.89036C3.53174 4.89036 3.15756 4.98876 2.88185 5.20525C2.62584 5.42174 2.48798 5.71695 2.48798 6.11056C2.48798 6.48449 2.58645 6.76001 2.78338 6.93714C2.98032 7.17331 3.25603 7.25203 3.63021 7.25203ZM9.95187 8.11797C9.81401 8.11797 9.71554 8.09829 9.65646 8.03925C9.59738 7.99989 9.5383 7.88181 9.49891 7.72436L7.64771 1.62339C7.60832 1.46595 7.56894 1.36754 7.56894 1.3085C7.56894 1.19042 7.62802 1.1117 7.76587 1.1117H8.53392C8.69147 1.1117 8.78994 1.13138 8.84902 1.19042C8.9081 1.22978 8.96718 1.34786 9.00657 1.50531L10.326 6.72065L11.547 1.50531C11.5864 1.34786 11.6258 1.24946 11.7046 1.19042C11.7637 1.15106 11.8818 1.1117 12.0197 1.1117H12.6499C12.8074 1.1117 12.9059 1.13138 12.965 1.19042C13.0241 1.22978 13.0832 1.34786 13.1225 1.50531L14.3632 6.7797L15.7221 1.50531C15.7615 1.34786 15.8206 1.24946 15.8796 1.19042C15.9387 1.15106 16.0372 1.1117 16.1947 1.1117H16.9234C17.0416 1.1117 17.1203 1.17074 17.1203 1.3085C17.1203 1.34786 17.1203 1.38722 17.1007 1.42658C17.1007 1.46595 17.081 1.54467 17.0416 1.62339L15.1313 7.72436C15.0919 7.88181 15.0328 7.98021 14.9737 8.03925C14.9147 8.07861 14.8162 8.11797 14.6783 8.11797H14.0088C13.8512 8.11797 13.7527 8.09829 13.6937 8.03925C13.6346 7.98021 13.5755 7.88181 13.5361 7.72436L12.3151 2.64678L11.0941 7.72436C11.0547 7.88181 11.0153 7.98021 10.9365 8.03925C10.8775 8.09829 10.7593 8.11797 10.6214 8.11797H9.95187ZM20.0941 8.31478C19.6805 8.31478 19.267 8.27542 18.8731 8.17702C18.4792 8.07861 18.1641 7.98021 17.9672 7.86213C17.849 7.7834 17.7505 7.70468 17.7308 7.64564C17.7112 7.5866 17.6718 7.4882 17.6718 7.42915V7.03554C17.6718 6.8781 17.7308 6.79938 17.849 6.79938C17.8884 6.79938 17.9475 6.79938 17.9869 6.81906C18.0263 6.83874 18.105 6.85842 18.1838 6.89778C18.4595 7.01586 18.7352 7.11426 19.0503 7.17331C19.3654 7.23235 19.6805 7.27171 19.9956 7.27171C20.488 7.27171 20.8818 7.19299 21.1378 7.01586C21.4136 6.83874 21.5514 6.58289 21.5514 6.268C21.5514 6.05151 21.4726 5.87439 21.3348 5.71695C21.1969 5.5595 20.9212 5.44142 20.547 5.30365L19.4048 4.9494C18.8337 4.77228 18.4004 4.49675 18.1444 4.1425C17.8884 3.78825 17.7505 3.41432 17.7505 3.00103C17.7505 2.66646 17.8293 2.37125 17.9672 2.13508C18.105 1.89892 18.302 1.66275 18.5383 1.48563C18.7746 1.3085 19.0503 1.17074 19.3654 1.07233C19.6805 0.973931 20.0153 0.93457 20.3501 0.93457C20.5273 0.93457 20.7046 0.93457 20.8818 0.973931C21.0591 0.993612 21.2363 1.03297 21.3939 1.05265C21.5514 1.09201 21.709 1.13138 21.8468 1.17074C21.9847 1.2101 22.1028 1.26914 22.1816 1.3085C22.2998 1.36754 22.3785 1.42658 22.4179 1.50531C22.4573 1.56435 22.4967 1.66275 22.4967 1.76115V2.13508C22.4967 2.29253 22.4376 2.39093 22.3195 2.39093C22.2604 2.39093 22.1619 2.35157 22.0241 2.29253C21.5711 2.09572 21.0788 1.97764 20.5077 1.97764C20.0547 1.97764 19.7002 2.05636 19.4639 2.19413C19.2276 2.33189 19.0897 2.56806 19.0897 2.90263C19.0897 3.11911 19.1685 3.31592 19.326 3.45368C19.4836 3.61113 19.779 3.74889 20.1926 3.88665L21.3151 4.2409C21.8862 4.41803 22.2998 4.67388 22.5361 4.98876C22.7724 5.30365 22.8906 5.67758 22.8906 6.09088C22.8906 6.42545 22.8118 6.74033 22.6936 6.99618C22.5558 7.27171 22.3589 7.50788 22.1225 7.685C21.8862 7.88181 21.5908 8.01957 21.256 8.11797C20.8621 8.27542 20.488 8.31478 20.0941 8.31478Z" fill="white"/>
|
||||
<g id="Group_2">
|
||||
<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M21.5908 12.1525C18.9912 14.0615 15.2101 15.0849 11.9803 15.0849C7.43108 15.0849 3.33481 13.4121 0.242906 10.6174C0.00658259 10.4009 0.223213 10.1057 0.518617 10.2632C3.86653 12.2115 7.9825 13.3727 12.256 13.3727C15.1313 13.3727 18.302 12.7823 21.2166 11.5424C21.6302 11.3653 22.0044 11.8376 21.5908 12.1525Z" fill="#FF9900"/>
|
||||
<path id="Vector_3" fill-rule="evenodd" clip-rule="evenodd" d="M22.6543 10.9324C22.3195 10.4994 20.4683 10.7356 19.6214 10.834C19.3654 10.8734 19.326 10.6372 19.5624 10.4798C21.0394 9.43669 23.4814 9.7319 23.7571 10.0861C24.0328 10.4404 23.6783 12.8808 22.2998 14.0419C22.0831 14.2191 21.8862 14.1207 21.9847 13.8845C22.2998 13.0973 22.989 11.3457 22.6543 10.9324Z" fill="#FF9900"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group_3">
|
||||
<path id="Vector_4" d="M6.82058 6.05151C6.82058 6.34672 6.85997 6.58289 6.89935 6.76001C6.95843 6.93714 7.03721 7.11426 7.15537 7.33075C7.19476 7.38979 7.21445 7.44883 7.21445 7.50788C7.21445 7.5866 7.17506 7.66532 7.0569 7.74404L6.56456 8.07861C6.48579 8.11797 6.42671 8.15733 6.36762 8.15733C6.28885 8.15733 6.21008 8.11797 6.1313 8.03925C6.01314 7.92117 5.93437 7.80308 5.85559 7.66532C5.77682 7.52756 5.69804 7.38979 5.61927 7.19299C5.00876 7.92117 4.24071 8.27542 3.29542 8.27542C2.62584 8.27542 2.1138 8.07861 1.71993 7.70468C1.32606 7.33075 1.12912 6.81906 1.12912 6.18928C1.12912 5.52014 1.36544 4.96908 1.83809 4.57547C2.31074 4.18186 2.96063 3.96538 3.76807 3.96538C4.04378 3.96538 4.31949 3.98506 4.5952 4.02442C4.8906 4.06378 5.18601 4.12282 5.50111 4.20154V3.63081C5.50111 3.04039 5.38294 2.60742 5.12693 2.37125C4.87091 2.13508 4.45734 2.017 3.84684 2.017C3.57113 2.017 3.29542 2.05636 3.00001 2.1154C2.70461 2.19413 2.4289 2.27285 2.15319 2.39093C2.03503 2.43029 1.93656 2.46965 1.87748 2.48933C1.8184 2.48933 1.77901 2.50901 1.75932 2.50901C1.64115 2.50901 1.60177 2.43029 1.60177 2.27285V1.87924C1.60177 1.76115 1.62146 1.66275 1.66085 1.60371C1.70024 1.54467 1.77901 1.48563 1.87748 1.44626C2.15319 1.3085 2.48798 1.19042 2.86216 1.09201C3.25603 0.993612 3.6499 0.93457 4.08316 0.93457C5.02846 0.93457 5.69804 1.15106 6.15099 1.56435C6.58425 1.99732 6.80088 2.6271 6.80088 3.51272L6.82058 6.05151ZM3.63021 7.25203C3.88623 7.25203 4.16194 7.21267 4.43765 7.11426C4.71336 7.01586 4.96938 6.83874 5.18601 6.60257C5.30417 6.44513 5.40264 6.28768 5.46172 6.09088C5.50111 5.89407 5.54049 5.67758 5.54049 5.40206V5.06749C5.30417 5.00845 5.06785 4.96908 4.81183 4.92972C4.55581 4.89036 4.31949 4.89036 4.06347 4.89036C3.53174 4.89036 3.15756 4.98876 2.88185 5.20525C2.62584 5.42174 2.48798 5.71695 2.48798 6.11056C2.48798 6.48449 2.58645 6.76001 2.78338 6.93714C2.98032 7.17331 3.25603 7.25203 3.63021 7.25203ZM9.95187 8.11797C9.81401 8.11797 9.71554 8.09829 9.65646 8.03925C9.59738 7.99989 9.5383 7.88181 9.49891 7.72436L7.64771 1.62339C7.60832 1.46595 7.56894 1.36754 7.56894 1.3085C7.56894 1.19042 7.62802 1.1117 7.76587 1.1117H8.53392C8.69147 1.1117 8.78994 1.13138 8.84902 1.19042C8.9081 1.22978 8.96718 1.34786 9.00657 1.50531L10.326 6.72065L11.547 1.50531C11.5864 1.34786 11.6258 1.24946 11.7046 1.19042C11.7637 1.15106 11.8818 1.1117 12.0197 1.1117H12.6499C12.8074 1.1117 12.9059 1.13138 12.965 1.19042C13.0241 1.22978 13.0832 1.34786 13.1225 1.50531L14.3632 6.7797L15.7221 1.50531C15.7615 1.34786 15.8206 1.24946 15.8796 1.19042C15.9387 1.15106 16.0372 1.1117 16.1947 1.1117H16.9234C17.0416 1.1117 17.1203 1.17074 17.1203 1.3085C17.1203 1.34786 17.1203 1.38722 17.1007 1.42658C17.1007 1.46595 17.081 1.54467 17.0416 1.62339L15.1313 7.72436C15.0919 7.88181 15.0328 7.98021 14.9737 8.03925C14.9147 8.07861 14.8162 8.11797 14.6783 8.11797H14.0088C13.8512 8.11797 13.7527 8.09829 13.6937 8.03925C13.6346 7.98021 13.5755 7.88181 13.5361 7.72436L12.3151 2.64678L11.0941 7.72436C11.0547 7.88181 11.0153 7.98021 10.9365 8.03925C10.8775 8.09829 10.7593 8.11797 10.6214 8.11797H9.95187ZM20.0941 8.31478C19.6805 8.31478 19.267 8.27542 18.8731 8.17702C18.4792 8.07861 18.1641 7.98021 17.9672 7.86213C17.849 7.7834 17.7505 7.70468 17.7308 7.64564C17.7112 7.5866 17.6718 7.4882 17.6718 7.42915V7.03554C17.6718 6.8781 17.7308 6.79938 17.849 6.79938C17.8884 6.79938 17.9475 6.79938 17.9869 6.81906C18.0263 6.83874 18.105 6.85842 18.1838 6.89778C18.4595 7.01586 18.7352 7.11426 19.0503 7.17331C19.3654 7.23235 19.6805 7.27171 19.9956 7.27171C20.488 7.27171 20.8818 7.19299 21.1378 7.01586C21.4136 6.83874 21.5514 6.58289 21.5514 6.268C21.5514 6.05151 21.4726 5.87439 21.3348 5.71695C21.1969 5.5595 20.9212 5.44142 20.547 5.30365L19.4048 4.9494C18.8337 4.77228 18.4004 4.49675 18.1444 4.1425C17.8884 3.78825 17.7505 3.41432 17.7505 3.00103C17.7505 2.66646 17.8293 2.37125 17.9672 2.13508C18.105 1.89892 18.302 1.66275 18.5383 1.48563C18.7746 1.3085 19.0503 1.17074 19.3654 1.07233C19.6805 0.973931 20.0153 0.93457 20.3501 0.93457C20.5273 0.93457 20.7046 0.93457 20.8818 0.973931C21.0591 0.993612 21.2363 1.03297 21.3939 1.05265C21.5514 1.09201 21.709 1.13138 21.8468 1.17074C21.9847 1.2101 22.1028 1.26914 22.1816 1.3085C22.2998 1.36754 22.3785 1.42658 22.4179 1.50531C22.4573 1.56435 22.4967 1.66275 22.4967 1.76115V2.13508C22.4967 2.29253 22.4376 2.39093 22.3195 2.39093C22.2604 2.39093 22.1619 2.35157 22.0241 2.29253C21.5711 2.09572 21.0788 1.97764 20.5077 1.97764C20.0547 1.97764 19.7002 2.05636 19.4639 2.19413C19.2276 2.33189 19.0897 2.56806 19.0897 2.90263C19.0897 3.11911 19.1685 3.31592 19.326 3.45368C19.4836 3.61113 19.779 3.74889 20.1926 3.88665L21.3151 4.2409C21.8862 4.41803 22.2998 4.67388 22.5361 4.98876C22.7724 5.30365 22.8906 5.67758 22.8906 6.09088C22.8906 6.42545 22.8118 6.74033 22.6936 6.99618C22.5558 7.27171 22.3589 7.50788 22.1225 7.685C21.8862 7.88181 21.5908 8.01957 21.256 8.11797C20.8621 8.27542 20.488 8.31478 20.0941 8.31478Z" fill="white"/>
|
||||
<g id="Group_4">
|
||||
<path id="Vector_5" fill-rule="evenodd" clip-rule="evenodd" d="M21.5908 12.1525C18.9912 14.0615 15.2101 15.0849 11.9803 15.0849C7.43108 15.0849 3.33481 13.4121 0.242906 10.6174C0.00658259 10.4009 0.223213 10.1057 0.518617 10.2632C3.86653 12.2115 7.9825 13.3727 12.256 13.3727C15.1313 13.3727 18.302 12.7823 21.2166 11.5424C21.6302 11.3653 22.0044 11.8376 21.5908 12.1525Z" fill="#FF9900"/>
|
||||
<path id="Vector_6" fill-rule="evenodd" clip-rule="evenodd" d="M22.6543 10.9324C22.3195 10.4994 20.4683 10.7356 19.6214 10.834C19.3654 10.8734 19.326 10.6372 19.5624 10.4798C21.0394 9.43669 23.4814 9.7319 23.7571 10.0861C24.0328 10.4404 23.6783 12.8808 22.2998 14.0419C22.0831 14.2191 21.8862 14.1207 21.9847 13.8845C22.2998 13.0973 22.989 11.3457 22.6543 10.9324Z" fill="#FF9900"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2552_26387">
|
||||
<rect width="23.7111" height="14.17" fill="white" transform="translate(0.14444 0.915039)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
9
frontend/public/Logos/aws-light.svg
Normal file
9
frontend/public/Logos/aws-light.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
|
||||
<path fill="#252F3E" d="M4.51 7.687c0 .197.02.357.058.475.042.117.096.245.17.384a.233.233 0 01.037.123c0 .053-.032.107-.1.16l-.336.224a.255.255 0 01-.138.048c-.054 0-.107-.026-.16-.074a1.652 1.652 0 01-.192-.251 4.137 4.137 0 01-.165-.315c-.415.491-.936.737-1.564.737-.447 0-.804-.129-1.064-.385-.261-.256-.394-.598-.394-1.025 0-.454.16-.822.484-1.1.325-.278.756-.416 1.304-.416.18 0 .367.016.564.042.197.027.4.07.612.118v-.39c0-.406-.085-.689-.25-.854-.17-.166-.458-.246-.868-.246-.186 0-.377.022-.574.07a4.23 4.23 0 00-.575.181 1.525 1.525 0 01-.186.07.326.326 0 01-.085.016c-.075 0-.112-.054-.112-.166v-.262c0-.085.01-.15.037-.186a.399.399 0 01.15-.113c.185-.096.409-.176.67-.24.26-.07.537-.101.83-.101.633 0 1.096.144 1.394.432.293.288.442.726.442 1.314v1.73h.01zm-2.161.811c.175 0 .356-.032.548-.096.191-.064.362-.182.505-.342a.848.848 0 00.181-.341c.032-.129.054-.283.054-.465V7.03a4.43 4.43 0 00-.49-.09 3.996 3.996 0 00-.5-.033c-.357 0-.618.07-.793.214-.176.144-.26.347-.26.614 0 .25.063.437.196.566.128.133.314.197.559.197zm4.273.577c-.096 0-.16-.016-.202-.054-.043-.032-.08-.106-.112-.208l-1.25-4.127a.938.938 0 01-.049-.214c0-.085.043-.133.128-.133h.522c.1 0 .17.016.207.053.043.032.075.107.107.208l.894 3.535.83-3.535c.026-.106.058-.176.1-.208a.365.365 0 01.214-.053h.425c.102 0 .17.016.213.053.043.032.08.107.101.208l.841 3.578.92-3.578a.458.458 0 01.107-.208.346.346 0 01.208-.053h.495c.085 0 .133.043.133.133 0 .027-.006.054-.01.086a.76.76 0 01-.038.133l-1.283 4.127c-.032.107-.069.177-.111.209a.34.34 0 01-.203.053h-.457c-.101 0-.17-.016-.213-.053-.043-.038-.08-.107-.101-.214L8.213 5.37l-.82 3.439c-.026.107-.058.176-.1.213-.043.038-.118.054-.213.054h-.458zm6.838.144a3.51 3.51 0 01-.82-.096c-.266-.064-.473-.134-.612-.214-.085-.048-.143-.101-.165-.15a.378.378 0 01-.031-.149v-.272c0-.112.042-.166.122-.166a.3.3 0 01.096.016c.032.011.08.032.133.054.18.08.378.144.585.187.213.042.42.064.633.064.336 0 .596-.059.777-.176a.575.575 0 00.277-.508.52.52 0 00-.144-.373c-.095-.102-.276-.193-.537-.278l-.772-.24c-.388-.123-.676-.305-.851-.545a1.275 1.275 0 01-.266-.774c0-.224.048-.422.143-.593.096-.17.224-.32.384-.438.16-.122.34-.213.553-.277.213-.064.436-.091.67-.091.118 0 .24.005.357.021.122.016.234.038.346.06.106.026.208.052.303.085.096.032.17.064.224.096a.46.46 0 01.16.133.289.289 0 01.047.176v.251c0 .112-.042.171-.122.171a.552.552 0 01-.202-.064 2.427 2.427 0 00-1.022-.208c-.303 0-.543.048-.708.15-.165.1-.25.256-.25.475 0 .149.053.277.16.379.106.101.303.202.585.293l.756.24c.383.123.66.294.825.513.165.219.244.47.244.748 0 .23-.047.437-.138.619a1.436 1.436 0 01-.388.47c-.165.133-.362.23-.591.299-.24.075-.49.112-.761.112z"/>
|
||||
|
||||
<g fill="#F90" fill-rule="evenodd" clip-rule="evenodd">
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
76
frontend/src/api/integrations/aws/index.ts
Normal file
76
frontend/src/api/integrations/aws/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import axios from 'api';
|
||||
import {
|
||||
CloudAccount,
|
||||
Service,
|
||||
ServiceData,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import {
|
||||
AccountConfigPayload,
|
||||
AccountConfigResponse,
|
||||
ConnectionUrlResponse,
|
||||
} from 'types/api/integrations/aws';
|
||||
|
||||
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
|
||||
const response = await axios.get('/cloud-integrations/aws/accounts');
|
||||
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getAwsServices = async (
|
||||
accountId?: string,
|
||||
): Promise<Service[]> => {
|
||||
const params = accountId ? { account_id: accountId } : undefined;
|
||||
const response = await axios.get('/cloud-integrations/aws/services', {
|
||||
params,
|
||||
});
|
||||
|
||||
return response.data.data.services;
|
||||
};
|
||||
|
||||
export const getServiceDetails = async (
|
||||
serviceId: string,
|
||||
accountId?: string,
|
||||
): Promise<ServiceData> => {
|
||||
const params = accountId ? { account_id: accountId } : undefined;
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/aws/services/${serviceId}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const generateConnectionUrl = async (params: {
|
||||
agent_config: { region: string };
|
||||
account_config: { regions: string[] };
|
||||
account_id?: string;
|
||||
}): Promise<ConnectionUrlResponse> => {
|
||||
const response = await axios.post(
|
||||
'/cloud-integrations/aws/accounts/generate-connection-url',
|
||||
params,
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateAccountConfig = async (
|
||||
accountId: string,
|
||||
payload: AccountConfigPayload,
|
||||
): Promise<AccountConfigResponse> => {
|
||||
const response = await axios.post<AccountConfigResponse>(
|
||||
`/cloud-integrations/aws/accounts/${accountId}/config`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateServiceConfig = async (
|
||||
serviceId: string,
|
||||
payload: UpdateServiceConfigPayload,
|
||||
): Promise<UpdateServiceConfigResponse> => {
|
||||
const response = await axios.post<UpdateServiceConfigResponse>(
|
||||
`/cloud-integrations/aws/services/${serviceId}/config`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
15119
frontend/src/assets/Lotties/integrations-success.json
Normal file
15119
frontend/src/assets/Lotties/integrations-success.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,10 @@
|
||||
import './CeleryTaskConfigOptions.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Select, Spin, Tooltip, Typography } from 'antd';
|
||||
import { Select, Spin, Typography } from 'antd';
|
||||
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Check, Share2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
import {
|
||||
getValuesFromQueryParams,
|
||||
@@ -23,11 +19,8 @@ function CeleryTaskConfigOptions(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const [isURLCopied, setIsURLCopied] = useState(false);
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<div className="celery-task-filters">
|
||||
<div className="celery-filters">
|
||||
@@ -66,25 +59,6 @@ function CeleryTaskConfigOptions(): JSX.Element {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip title="Share this" arrow={false}>
|
||||
<Button
|
||||
className="periscope-btn copy-url-btn"
|
||||
onClick={(): void => {
|
||||
handleCopyToClipboard(window.location.href);
|
||||
setIsURLCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsURLCopied(false);
|
||||
}, 1000);
|
||||
}}
|
||||
icon={
|
||||
isURLCopied ? (
|
||||
<Check size={14} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<Share2 size={14} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
export interface Filters {
|
||||
searchText: string;
|
||||
@@ -31,8 +35,18 @@ export function useGetAllFilters(props: Filters): GetAllFiltersResponse {
|
||||
tagType,
|
||||
} = props;
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const { data, isLoading } = useQuery(
|
||||
['attributesValues', attributeKey, searchText],
|
||||
[
|
||||
REACT_QUERY_KEY.GET_ATTRIBUTE_VALUES,
|
||||
attributeKey,
|
||||
searchText,
|
||||
minTime,
|
||||
maxTime,
|
||||
],
|
||||
async () => {
|
||||
const keys = Array.isArray(attributeKey) ? attributeKey : [attributeKey];
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@ import './CeleryTaskGraph.style.scss';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -18,11 +16,10 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
|
||||
import { paths } from '../CeleryUtils';
|
||||
import { useGetGraphCustomSeries } from '../useGetGraphCustomSeries';
|
||||
import {
|
||||
celeryAllStateWidgetData,
|
||||
celeryFailedStateWidgetData,
|
||||
@@ -114,58 +111,26 @@ function CeleryTaskBar({
|
||||
string,
|
||||
];
|
||||
|
||||
onClick?.({
|
||||
entity,
|
||||
value,
|
||||
timeRange: [start, end],
|
||||
widgetData,
|
||||
});
|
||||
if (!isEmpty(entity) || !isEmpty(value)) {
|
||||
onClick?.({
|
||||
entity,
|
||||
value,
|
||||
timeRange: [start, end],
|
||||
widgetData,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getGraphSeries = (color: string, label: string): any => ({
|
||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
||||
isDarkMode,
|
||||
drawStyle: 'bars',
|
||||
paths,
|
||||
lineInterpolation: 'spline',
|
||||
show: true,
|
||||
label,
|
||||
fill: `${color}90`,
|
||||
stroke: color,
|
||||
width: 2,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
size: 5,
|
||||
show: false,
|
||||
stroke: color,
|
||||
colorMapping: {
|
||||
SUCCESS: Color.BG_FOREST_500,
|
||||
FAILURE: Color.BG_CHERRY_500,
|
||||
RETRY: Color.BG_AMBER_400,
|
||||
},
|
||||
});
|
||||
|
||||
const customSeries = (data: QueryData[]): uPlot.Series[] => {
|
||||
const configurations: uPlot.Series[] = [
|
||||
{ label: 'Timestamp', stroke: 'purple' },
|
||||
];
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const { metric = {}, queryName = '', legend = '' } = data[i] || {};
|
||||
const label = getLabelName(metric, queryName || '', legend || '');
|
||||
let color = generateColor(
|
||||
label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
if (label === 'SUCCESS') {
|
||||
color = Color.BG_FOREST_500;
|
||||
}
|
||||
if (label === 'FAILURE') {
|
||||
color = Color.BG_CHERRY_500;
|
||||
}
|
||||
|
||||
if (label === 'RETRY') {
|
||||
color = Color.BG_AMBER_400;
|
||||
}
|
||||
const series = getGraphSeries(color, label);
|
||||
configurations.push(series);
|
||||
}
|
||||
return configurations;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
isDarkMode={isDarkMode}
|
||||
@@ -183,7 +148,7 @@ function CeleryTaskBar({
|
||||
onClickHandler={(...args): void =>
|
||||
onGraphClick(celerySlowestTasksTableWidgetData, ...args)
|
||||
}
|
||||
customSeries={customSeries}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
)}
|
||||
{barState === CeleryTaskState.Failed && (
|
||||
@@ -195,7 +160,7 @@ function CeleryTaskBar({
|
||||
onClickHandler={(...args): void =>
|
||||
onGraphClick(celeryFailedTasksTableWidgetData, ...args)
|
||||
}
|
||||
customSeries={customSeries}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
)}
|
||||
{barState === CeleryTaskState.Retry && (
|
||||
@@ -207,7 +172,7 @@ function CeleryTaskBar({
|
||||
onClickHandler={(...args): void =>
|
||||
onGraphClick(celeryRetryTasksTableWidgetData, ...args)
|
||||
}
|
||||
customSeries={customSeries}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
)}
|
||||
{barState === CeleryTaskState.Successful && (
|
||||
@@ -219,7 +184,7 @@ function CeleryTaskBar({
|
||||
onClickHandler={(...args): void =>
|
||||
onGraphClick(celerySuccessTasksTableWidgetData, ...args)
|
||||
}
|
||||
customSeries={customSeries}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,93 +6,85 @@
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.celery-task-graph-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 60% 40%;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
.celery-task-graph-bar,
|
||||
.celery-task-graph-task-latency {
|
||||
height: 380px !important;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.celery-task-graph {
|
||||
height: 380px !important;
|
||||
.celery-task-graph-grid-content {
|
||||
padding: 6px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: calc(100% - 18px);
|
||||
.ant-card-body {
|
||||
height: calc(100% - 18px);
|
||||
|
||||
.widget-graph-container {
|
||||
&.bar {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
.widget-graph-container {
|
||||
&.bar {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
|
||||
&.graph {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.celery-task-graph-worker-count {
|
||||
.celery-task-graph-worker-count {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(171, 189, 255, 0) 0%,
|
||||
rgba(171, 189, 255, 0) 100%
|
||||
),
|
||||
#0b0c0e;
|
||||
|
||||
.ant-card-body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.worker-count-text-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(171, 189, 255, 0) 0%,
|
||||
rgba(171, 189, 255, 0) 100%
|
||||
),
|
||||
#0b0c0e;
|
||||
|
||||
.ant-card-body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.worker-count-text-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.celery-task-graph-worker-count-text {
|
||||
font-size: 2.5vw;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.worker-count-header {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.celery-task-graph-worker-count-text {
|
||||
font-size: 2.5vw;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.celery-task-graph-bar,
|
||||
.celery-task-graph-task-latency {
|
||||
height: 380px !important;
|
||||
.worker-count-header {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.celery-task-graph-grid-content {
|
||||
padding: 6px;
|
||||
height: 100%;
|
||||
}
|
||||
.celery-task-graph {
|
||||
height: 380px !important;
|
||||
padding: 6px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ant-card-body {
|
||||
height: calc(100% - 18px);
|
||||
.ant-card-body {
|
||||
height: calc(100% - 18px);
|
||||
|
||||
.widget-graph-container {
|
||||
&.bar {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
|
||||
&.graph {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
.widget-graph-container {
|
||||
&.bar {
|
||||
height: calc(100% - 110px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +108,85 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metric-based-graphs,
|
||||
.trace-based-graphs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.metric-page-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
|
||||
.celery-task-graph {
|
||||
height: 280px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trace-based-graphs {
|
||||
.trace-based-graphs-header {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.configure-option-Info {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.configure-option-Info-text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.row-panel {
|
||||
border-radius: 4px;
|
||||
background: rgba(18, 19, 23, 0.4);
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
height: 48px !important;
|
||||
width: 100%;
|
||||
|
||||
.ant-typography {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.row-panel-section {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
|
||||
.row-icon {
|
||||
color: var(--bg-vanilla-400);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.celery-task-states {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import './CeleryTaskGraph.style.scss';
|
||||
|
||||
import { Card, Typography } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
@@ -23,9 +26,11 @@ import CeleryTaskLatencyGraph from './CeleryTaskLatencyGraph';
|
||||
export default function CeleryTaskGraphGrid({
|
||||
onClick,
|
||||
queryEnabled,
|
||||
configureOptionComponent,
|
||||
}: {
|
||||
onClick: (task: CaptureDataProps) => void;
|
||||
queryEnabled: boolean;
|
||||
configureOptionComponent?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
@@ -71,44 +76,123 @@ export default function CeleryTaskGraphGrid({
|
||||
DataTypes.String,
|
||||
'tag',
|
||||
);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [collapsedSections, setCollapsedSections] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({
|
||||
metricBasedGraphs: false,
|
||||
traceBasedGraphs: false,
|
||||
});
|
||||
|
||||
const toggleCollapse = (key: string): void => {
|
||||
setCollapsedSections((prev) => ({
|
||||
...prev,
|
||||
[key]: !prev[key],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="celery-task-graph-grid-container">
|
||||
<div className="celery-task-graph-grid">
|
||||
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
|
||||
<Card className="celery-task-graph-worker-count">
|
||||
<div className="worker-count-header">
|
||||
<Typography.Text className="worker-count-header-text">
|
||||
Worker Count
|
||||
<div className="metric-based-graphs">
|
||||
<CardContainer className="row-card" isDarkMode={isDarkMode}>
|
||||
<div className="row-panel">
|
||||
<div className="row-panel-section">
|
||||
<Typography.Text className="section-title">
|
||||
Flower Metrics
|
||||
</Typography.Text>
|
||||
{collapsedSections.metricBasedGraphs ? (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
onClick={(): void => toggleCollapse('metricBasedGraphs')}
|
||||
className="row-icon"
|
||||
/>
|
||||
) : (
|
||||
<ChevronUp
|
||||
size={14}
|
||||
onClick={(): void => toggleCollapse('metricBasedGraphs')}
|
||||
className="row-icon"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContainer>
|
||||
{!collapsedSections.metricBasedGraphs && (
|
||||
<div className="metric-page-grid">
|
||||
<CeleryTaskGraph
|
||||
key={celeryActiveTasksData.id}
|
||||
widgetData={celeryActiveTasksData}
|
||||
queryEnabled={queryEnabled}
|
||||
/>
|
||||
<Card className="celery-task-graph-worker-count">
|
||||
<div className="worker-count-header">
|
||||
<Typography.Text className="worker-count-header-text">
|
||||
Worker Count
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="worker-count-text-container">
|
||||
<Typography.Text className="celery-task-graph-worker-count-text">
|
||||
{options.filter((option) => option.value).length}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="trace-based-graphs">
|
||||
<div className="trace-based-graphs-header">
|
||||
<CardContainer className="row-card" isDarkMode={isDarkMode}>
|
||||
<div className="row-panel">
|
||||
<div className="row-panel-section">
|
||||
<Typography.Text className="section-title">
|
||||
Span Based Stats
|
||||
</Typography.Text>
|
||||
{collapsedSections.traceBasedGraphs ? (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
onClick={(): void => toggleCollapse('traceBasedGraphs')}
|
||||
className="row-icon"
|
||||
/>
|
||||
) : (
|
||||
<ChevronUp
|
||||
size={14}
|
||||
onClick={(): void => toggleCollapse('traceBasedGraphs')}
|
||||
className="row-icon"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContainer>
|
||||
<div className="configure-option-Info">
|
||||
{configureOptionComponent}
|
||||
<Typography.Text className="configure-option-Info-text">
|
||||
Click on a graph co-ordinate to see more details
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="worker-count-text-container">
|
||||
<Typography.Text className="celery-task-graph-worker-count-text">
|
||||
{options.filter((option) => option.value).length}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="celery-task-graph-grid">
|
||||
<CeleryTaskLatencyGraph onClick={onClick} queryEnabled={queryEnabled} />
|
||||
<CeleryTaskGraph
|
||||
key={celeryActiveTasksData.id}
|
||||
widgetData={celeryActiveTasksData}
|
||||
queryEnabled={queryEnabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="celery-task-graph-grid-bottom">
|
||||
{bottomWidgetData.map((widgetData, index) => (
|
||||
<CeleryTaskGraph
|
||||
key={widgetData.id}
|
||||
widgetData={widgetData}
|
||||
onClick={onClick}
|
||||
queryEnabled={queryEnabled}
|
||||
rightPanelTitle={rightPanelTitle[index]}
|
||||
applyCeleryTaskFilter
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!collapsedSections.traceBasedGraphs && (
|
||||
<>
|
||||
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
|
||||
<CeleryTaskLatencyGraph onClick={onClick} queryEnabled={queryEnabled} />
|
||||
<div className="celery-task-graph-grid-bottom">
|
||||
{bottomWidgetData.map((widgetData, index) => (
|
||||
<CeleryTaskGraph
|
||||
key={widgetData.id}
|
||||
widgetData={widgetData}
|
||||
onClick={onClick}
|
||||
queryEnabled={queryEnabled}
|
||||
rightPanelTitle={rightPanelTitle[index]}
|
||||
applyCeleryTaskFilter
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CeleryTaskGraphGrid.defaultProps = {
|
||||
configureOptionComponent: null,
|
||||
};
|
||||
|
||||
@@ -332,6 +332,7 @@ export const celeryTasksByWorkerWidgetData = (
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
],
|
||||
yAxisUnit: 'cps',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -345,44 +346,103 @@ export const celeryErrorByWorkerWidgetData = (
|
||||
description: 'Represents the number of errors by each worker.',
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count_distinct',
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
dataType: 'string',
|
||||
id: 'span_id--string----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: '',
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: DataSource.TRACES,
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
timeAggregation: 'count_distinct',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
dataType: DataTypes.bool,
|
||||
id: 'has_error--bool----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'has_error',
|
||||
type: '',
|
||||
},
|
||||
op: '=',
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
expression: 'A',
|
||||
disabled: true,
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'celery.hostname--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'celery.hostname',
|
||||
type: 'tag',
|
||||
id: 'celery.hostname--string--tag--false',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: '{{celery.hostname}}',
|
||||
limit: 10,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
{
|
||||
dataSource: 'traces',
|
||||
queryName: 'B',
|
||||
aggregateOperator: 'count_distinct',
|
||||
aggregateAttribute: {
|
||||
dataType: 'string',
|
||||
id: 'span_id--string----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'count_distinct',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'B',
|
||||
disabled: true,
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'celery.hostname',
|
||||
type: 'tag',
|
||||
id: 'celery.hostname--string--tag--false',
|
||||
},
|
||||
],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
{
|
||||
queryName: 'F1',
|
||||
expression: '(A/B)*100',
|
||||
disabled: false,
|
||||
legend: '{{celery.hostname}}',
|
||||
} as any,
|
||||
],
|
||||
yAxisUnit: 'percent',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -392,7 +452,7 @@ export const celeryLatencyByWorkerWidgetData = (
|
||||
): Widgets =>
|
||||
getWidgetQueryBuilder(
|
||||
getWidgetQuery({
|
||||
title: 'Latency by Worker',
|
||||
title: 'Latency by worker',
|
||||
description: 'Represents the latency of tasks by each worker.',
|
||||
queryData: [
|
||||
{
|
||||
@@ -444,7 +504,7 @@ export const celeryActiveTasksWidgetData = (
|
||||
): Widgets =>
|
||||
getWidgetQueryBuilder(
|
||||
getWidgetQuery({
|
||||
title: 'Tasks/ worker (Active tasks)',
|
||||
title: 'Active Tasks by worker',
|
||||
description: 'Represents the number of active tasks.',
|
||||
queryData: [
|
||||
{
|
||||
@@ -487,6 +547,7 @@ export const celeryActiveTasksWidgetData = (
|
||||
timeAggregation: 'latest',
|
||||
},
|
||||
],
|
||||
yAxisUnit: 'cps',
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { drawStyles } from 'lib/uPlotLib/utils/constants';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { paths } from './CeleryUtils';
|
||||
|
||||
interface UseGetGraphCustomSeriesProps {
|
||||
isDarkMode: boolean;
|
||||
drawStyle?: typeof drawStyles[keyof typeof drawStyles];
|
||||
colorMapping?: Record<string, string>;
|
||||
}
|
||||
|
||||
const defaultColorMapping = {
|
||||
SUCCESS: Color.BG_FOREST_500,
|
||||
FAILURE: Color.BG_CHERRY_500,
|
||||
RETRY: Color.BG_AMBER_400,
|
||||
};
|
||||
|
||||
export const useGetGraphCustomSeries = ({
|
||||
isDarkMode,
|
||||
drawStyle = 'bars',
|
||||
colorMapping = defaultColorMapping,
|
||||
}: UseGetGraphCustomSeriesProps): {
|
||||
getCustomSeries: (data: QueryData[]) => uPlot.Series[];
|
||||
} => {
|
||||
const getGraphSeries = (color: string, label: string): any => ({
|
||||
drawStyle,
|
||||
paths,
|
||||
lineInterpolation: 'spline',
|
||||
show: true,
|
||||
label,
|
||||
fill: `${color}90`,
|
||||
stroke: color,
|
||||
width: 2,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
size: 5,
|
||||
show: false,
|
||||
stroke: color,
|
||||
},
|
||||
});
|
||||
|
||||
const getCustomSeries = (data: QueryData[]): uPlot.Series[] => {
|
||||
const configurations: uPlot.Series[] = [
|
||||
{ label: 'Timestamp', stroke: 'purple' },
|
||||
];
|
||||
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const { metric = {}, queryName = '', legend = '' } = data[i] || {};
|
||||
const label = getLabelName(metric, queryName || '', legend || '');
|
||||
|
||||
// Check if label exists in colorMapping
|
||||
const color =
|
||||
colorMapping[label] ||
|
||||
generateColor(
|
||||
label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
|
||||
const series = getGraphSeries(color, label);
|
||||
configurations.push(series);
|
||||
}
|
||||
return configurations;
|
||||
};
|
||||
|
||||
return { getCustomSeries };
|
||||
};
|
||||
117
frontend/src/components/SignozModal/SignozModal.style.scss
Normal file
117
frontend/src/components/SignozModal/SignozModal.style.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
.signoz-modal {
|
||||
.ant-modal-content {
|
||||
padding: 0;
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--bg-ink-400);
|
||||
border-bottom: none;
|
||||
padding: 12px 24px 8px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
top: 15px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
.ant-modal-close-x {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
border-radius: 2px !important;
|
||||
&-selector {
|
||||
background: var(--bg-ink-300) !important;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
border-radius: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-dropdown {
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-ink-300);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.ant-select-item {
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
&-option-selected {
|
||||
background: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.signoz-modal {
|
||||
.ant-modal-content {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-bottom-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
&-selector {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-dropdown {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-select-item {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&-option-selected {
|
||||
background: var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
&-option-active {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
frontend/src/components/SignozModal/SignozModal.tsx
Normal file
25
frontend/src/components/SignozModal/SignozModal.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import './SignozModal.style.scss';
|
||||
|
||||
import { Modal, ModalProps } from 'antd';
|
||||
|
||||
function SignozModal({
|
||||
children,
|
||||
|
||||
rootClassName = '',
|
||||
...rest
|
||||
}: ModalProps): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
width={672}
|
||||
cancelText="Close"
|
||||
rootClassName={`signoz-modal ${rootClassName}`}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignozModal;
|
||||
@@ -32,4 +32,14 @@ export const REACT_QUERY_KEY = {
|
||||
GET_JOB_LIST: 'GET_JOB_LIST',
|
||||
GET_DAEMONSET_LIST: 'GET_DAEMONSET_LIST,',
|
||||
GET_VOLUME_LIST: 'GET_VOLUME_LIST',
|
||||
|
||||
// AWS Integration Query Keys
|
||||
AWS_ACCOUNTS: 'AWS_ACCOUNTS',
|
||||
AWS_SERVICES: 'AWS_SERVICES',
|
||||
AWS_SERVICE_DETAILS: 'AWS_SERVICE_DETAILS',
|
||||
AWS_ACCOUNT_STATUS: 'AWS_ACCOUNT_STATUS',
|
||||
AWS_UPDATE_ACCOUNT_CONFIG: 'AWS_UPDATE_ACCOUNT_CONFIG',
|
||||
AWS_UPDATE_SERVICE_CONFIG: 'AWS_UPDATE_SERVICE_CONFIG',
|
||||
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
|
||||
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
@@ -292,6 +293,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
routeKey === 'MESSAGING_QUEUES_CELERY_TASK' ||
|
||||
routeKey === 'MESSAGING_QUEUES_OVERVIEW';
|
||||
|
||||
const isCloudIntegrationPage = (): boolean =>
|
||||
routeKey === 'INTEGRATIONS' &&
|
||||
new URLSearchParams(window.location.search).get('integration') ===
|
||||
INTEGRATION_TYPES.AWS_INTEGRATION;
|
||||
|
||||
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
||||
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
|
||||
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
|
||||
@@ -426,6 +432,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isAlertHistory() ||
|
||||
isAlertOverview() ||
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import Header from './Header/Header';
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServicesTabs from './ServicesSection/ServicesTabs';
|
||||
|
||||
function CloudIntegrationPage(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<HeroSection />
|
||||
<ServicesTabs />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudIntegrationPage;
|
||||
@@ -0,0 +1,65 @@
|
||||
.cloud-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 18px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
&__navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 0px 6px;
|
||||
}
|
||||
|
||||
&__breadcrumb-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__breadcrumb-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
border-radius: 2px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
width: 113px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.cloud-header {
|
||||
border-bottom: 1px solid var(--bg-slate-300);
|
||||
|
||||
&__breadcrumb-title {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&__help {
|
||||
border-color: var(--bg-slate-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-slate-400);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import './Header.styles.scss';
|
||||
|
||||
import Breadcrumb from 'antd/es/breadcrumb';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Blocks, LifeBuoy } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function Header(): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-header">
|
||||
<div className="cloud-header__navigation">
|
||||
<Breadcrumb
|
||||
className="cloud-header__breadcrumb"
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<Link to={ROUTES.INTEGRATIONS}>
|
||||
<span className="cloud-header__breadcrumb-link">
|
||||
<Blocks size={16} color="var(--bg-vanilla-400)" />
|
||||
<span className="cloud-header__breadcrumb-title">Integrations</span>
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="cloud-header__breadcrumb-title">AWS web services</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="cloud-header__actions">
|
||||
<button className="cloud-header__help" type="button">
|
||||
<LifeBuoy size={12} />
|
||||
Get Help
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,72 @@
|
||||
.hero-section {
|
||||
height: 308px;
|
||||
padding: 26px 16px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-position: right;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
|
||||
&__icon {
|
||||
height: fit-content;
|
||||
background-color: var(--bg-ink-400);
|
||||
padding: 12px;
|
||||
border: 1px solid var(--bg-ink-300);
|
||||
border-radius: 6px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.hero-section {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
&__icon {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&__details {
|
||||
.title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.integrations-select {
|
||||
height: 44px;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
return (
|
||||
<div
|
||||
className="hero-section"
|
||||
style={
|
||||
isDarkMode
|
||||
? {
|
||||
backgroundImage: `url('/Images/integrations-hero-bg.png')`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="hero-section__icon">
|
||||
<img src="/Logos/aws-dark.svg" alt="aws-logo" />
|
||||
</div>
|
||||
<div className="hero-section__details">
|
||||
<div className="title">AWS Web Services</div>
|
||||
<div className="description">
|
||||
One-click setup for AWS monitoring with SigNoz
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -0,0 +1,127 @@
|
||||
.hero-section__actions {
|
||||
margin-top: 12px;
|
||||
|
||||
&-with-account {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
.hero-section__action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hero-section__action-button {
|
||||
font-family: 'Inter';
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
padding: 8px 17px;
|
||||
|
||||
&.primary {
|
||||
background: var(--bg-robin-500);
|
||||
border: none;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-100);
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-400);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-ink-300);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
rgba(18, 19, 23, 0.8) 0%,
|
||||
rgba(18, 19, 23, 0.9) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-slate-400) !important;
|
||||
background: var(--bg-ink-300) !important;
|
||||
padding: 6px 8px !important;
|
||||
}
|
||||
.ant-select-selection-item {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.account-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
&__selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background-color: rgba(192, 193, 195, 0.2); /* #C0C1C3 with 0.2 opacity */
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lightMode {
|
||||
.hero-section__action-button {
|
||||
&.primary {
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-400);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-selector {
|
||||
background: var(--bg-vanilla-100);
|
||||
.ant-select-selector {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
border-color: var(--bg-vanilla-400) !important;
|
||||
}
|
||||
.ant-select-item-option-active {
|
||||
background: var(--bg-vanilla-400) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-vanilla-400) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-option-item {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&__selected {
|
||||
background: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import './AccountActions.style.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Select } from 'antd';
|
||||
import { SelectProps } from 'antd/lib';
|
||||
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import AccountSettingsModal from './AccountSettingsModal';
|
||||
import CloudAccountSetupModal from './CloudAccountSetupModal';
|
||||
|
||||
interface AccountOptionItemProps {
|
||||
label: React.ReactNode;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
function AccountOptionItem({
|
||||
label,
|
||||
isSelected,
|
||||
}: AccountOptionItemProps): JSX.Element {
|
||||
return (
|
||||
<div className="account-option-item">
|
||||
{label}
|
||||
{isSelected && (
|
||||
<div className="account-option-item__selected">
|
||||
<Check size={12} color={Color.BG_VANILLA_100} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOption(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
option: any,
|
||||
activeAccountId: string | undefined,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<AccountOptionItem
|
||||
label={option.label}
|
||||
isSelected={option.value === activeAccountId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getAccountById = (
|
||||
accounts: CloudAccount[],
|
||||
accountId: string,
|
||||
): CloudAccount | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
|
||||
function AccountActions(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const { data: accounts } = useAwsAccounts();
|
||||
|
||||
const initialAccount = useMemo(
|
||||
() =>
|
||||
accounts?.length
|
||||
? getAccountById(accounts, urlQuery.get('accountId') || '') || accounts[0]
|
||||
: null,
|
||||
[accounts, urlQuery],
|
||||
);
|
||||
|
||||
const [activeAccount, setActiveAccount] = useState<CloudAccount | null>(
|
||||
initialAccount,
|
||||
);
|
||||
|
||||
// Update state when initial value changes
|
||||
useEffect(() => {
|
||||
if (initialAccount !== null) {
|
||||
setActiveAccount(initialAccount);
|
||||
urlQuery.set('accountId', initialAccount.cloud_account_id);
|
||||
navigate({ search: urlQuery.toString() });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialAccount]);
|
||||
|
||||
const [isIntegrationModalOpen, setIsIntegrationModalOpen] = useState(false);
|
||||
const [isAccountSettingsModalOpen, setIsAccountSettingsModalOpen] = useState(
|
||||
false,
|
||||
);
|
||||
|
||||
const selectOptions: SelectProps['options'] = useMemo(
|
||||
() =>
|
||||
accounts?.length
|
||||
? accounts.map((account) => ({
|
||||
value: account.cloud_account_id,
|
||||
label: account.cloud_account_id,
|
||||
}))
|
||||
: [],
|
||||
[accounts],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="hero-section__actions">
|
||||
{accounts?.length ? (
|
||||
<div className="hero-section__actions-with-account">
|
||||
<Select
|
||||
value={`Account: ${activeAccount?.cloud_account_id}`}
|
||||
options={selectOptions}
|
||||
rootClassName="cloud-account-selector"
|
||||
placeholder="Select AWS Account"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
optionRender={(option): JSX.Element =>
|
||||
renderOption(option, activeAccount?.cloud_account_id)
|
||||
}
|
||||
onChange={(value): void => {
|
||||
setActiveAccount(getAccountById(accounts, value));
|
||||
urlQuery.set('accountId', value);
|
||||
navigate({ search: urlQuery.toString() });
|
||||
}}
|
||||
/>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Button
|
||||
type="primary"
|
||||
className="hero-section__action-button primary"
|
||||
onClick={(): void => setIsIntegrationModalOpen(true)}
|
||||
>
|
||||
Add New AWS Account
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
className="hero-section__action-button secondary"
|
||||
onClick={(): void => setIsAccountSettingsModalOpen(true)}
|
||||
>
|
||||
Account Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
className="hero-section__action-button primary"
|
||||
onClick={(): void => setIsIntegrationModalOpen(true)}
|
||||
>
|
||||
Integrate Now
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<CloudAccountSetupModal
|
||||
isOpen={isIntegrationModalOpen}
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
|
||||
<AccountSettingsModal
|
||||
isOpen={isAccountSettingsModalOpen}
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount as CloudAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountActions;
|
||||
@@ -0,0 +1,189 @@
|
||||
.account-settings-modal {
|
||||
&__title-account-id {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 17px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
padding: 14px;
|
||||
&-account-info {
|
||||
&-connected-account-details {
|
||||
&-title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
&-account-id {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
&-account-id {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&-regions-switch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
&-title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
&-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
&-label {
|
||||
color: var(--bg-vanilla-400);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.005em;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-regions-select {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
&-close-button,
|
||||
&-save-button {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
&-close-button {
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-400);
|
||||
border: none;
|
||||
}
|
||||
&-save-button {
|
||||
&:disabled {
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
opacity: 0.6;
|
||||
border: none;
|
||||
}
|
||||
border-radius: 2px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
.ant-modal-body {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
.ant-modal-footer {
|
||||
margin: 0;
|
||||
padding: 24px 24px 12px;
|
||||
}
|
||||
|
||||
.integration-detail-content {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.account-settings-modal {
|
||||
&__title-account-id {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
&__body {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&-account-info {
|
||||
&-connected-account-details {
|
||||
&-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
&-account-id {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&-account-id {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-regions-switch {
|
||||
&-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
&-switch {
|
||||
&-label {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
&-close-button,
|
||||
&-save-button {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
&-close-button {
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-400);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&-save-button {
|
||||
// Keep primary button same as dark mode
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
&:disabled {
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import './AccountSettingsModal.style.scss';
|
||||
|
||||
import { Form, Select, Switch } from 'antd';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import {
|
||||
getRegionPreviewText,
|
||||
useAccountSettingsModal,
|
||||
} from 'hooks/integrations/aws/useAccountSettingsModal';
|
||||
import IntergrationsUninstallBar from 'pages/Integrations/IntegrationDetailPage/IntegrationsUninstallBar';
|
||||
import { ConnectionStates } from 'pages/Integrations/IntegrationDetailPage/TestConnection';
|
||||
import { AWS_INTEGRATION } from 'pages/Integrations/IntegrationsList';
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
|
||||
interface AccountSettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
isRegionSelectOpen,
|
||||
isSaveDisabled,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
setIsRegionSelectOpen,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
|
||||
|
||||
const renderRegionSelector = useCallback(() => {
|
||||
if (isRegionSelectOpen) {
|
||||
return (
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="account-settings-modal__body-regions-switch-switch ">
|
||||
<Switch
|
||||
checked={includeAllRegions}
|
||||
onChange={handleIncludeAllRegionsChange}
|
||||
/>
|
||||
<button
|
||||
className="account-settings-modal__body-regions-switch-switch-label"
|
||||
type="button"
|
||||
onClick={(): void => handleIncludeAllRegionsChange(!includeAllRegions)}
|
||||
>
|
||||
Include all regions
|
||||
</button>
|
||||
</div>
|
||||
<Select
|
||||
suffixIcon={null}
|
||||
placeholder="Select Region(s)"
|
||||
className="cloud-account-setup-form__select account-settings-modal__body-regions-select integrations-select"
|
||||
onClick={(): void => setIsRegionSelectOpen(true)}
|
||||
mode="multiple"
|
||||
maxTagCount={3}
|
||||
value={getRegionPreviewText(selectedRegions)}
|
||||
open={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
isRegionSelectOpen,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
setIsRegionSelectOpen,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
]);
|
||||
|
||||
const renderAccountDetails = useCallback(
|
||||
() => (
|
||||
<div className="account-settings-modal__body-account-info">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-title">
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
|
||||
AWS Account:{' '}
|
||||
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
|
||||
{account?.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[account?.id],
|
||||
);
|
||||
|
||||
const modalTitle = (
|
||||
<div className="account-settings-modal__title">
|
||||
Account settings for{' '}
|
||||
<span className="account-settings-modal__title-account-id">
|
||||
{account?.id}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<SignozModal
|
||||
open={isOpen}
|
||||
title={modalTitle}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSubmit}
|
||||
okText="Save"
|
||||
okButtonProps={{
|
||||
disabled: isSaveDisabled,
|
||||
className: 'account-settings-modal__footer-save-button',
|
||||
loading: isLoading,
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
className: 'account-settings-modal__footer-close-button',
|
||||
}}
|
||||
width={672}
|
||||
rootClassName="account-settings-modal"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
}}
|
||||
>
|
||||
<div className="account-settings-modal__body">
|
||||
{renderAccountDetails()}
|
||||
|
||||
<Form.Item
|
||||
name="selectedRegions"
|
||||
rules={[
|
||||
{
|
||||
validator: async (): Promise<void> => {
|
||||
if (selectedRegions.length === 0) {
|
||||
throw new Error('Please select at least one region to monitor');
|
||||
}
|
||||
},
|
||||
message: 'Please select at least one region to monitor',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{renderRegionSelector()}
|
||||
</Form.Item>
|
||||
|
||||
<div className="integration-detail-content">
|
||||
<IntergrationsUninstallBar
|
||||
integrationTitle={AWS_INTEGRATION.title}
|
||||
integrationId={AWS_INTEGRATION.id}
|
||||
onUnInstallSuccess={handleClose}
|
||||
removeIntegrationTitle="Remove"
|
||||
connectionStatus={ConnectionStates.Connected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</SignozModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsModal;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Alert, Spin } from 'antd';
|
||||
import { LoaderCircle, TriangleAlert } from 'lucide-react';
|
||||
|
||||
import { ModalStateEnum } from '../types';
|
||||
|
||||
function AlertMessage({
|
||||
modalState,
|
||||
}: {
|
||||
modalState: ModalStateEnum;
|
||||
}): JSX.Element | null {
|
||||
switch (modalState) {
|
||||
case ModalStateEnum.WAITING:
|
||||
return (
|
||||
<Alert
|
||||
message={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
<Spin
|
||||
indicator={
|
||||
<LoaderCircle
|
||||
size={14}
|
||||
color={Color.BG_AMBER_400}
|
||||
className="anticon anticon-loading anticon-spin ant-spin-dot"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
Waiting for connection, retrying in{' '}
|
||||
<span className="retry-time">10</span> secs...
|
||||
</div>
|
||||
}
|
||||
className="cloud-account-setup-form__alert"
|
||||
type="warning"
|
||||
/>
|
||||
);
|
||||
case ModalStateEnum.ERROR:
|
||||
return (
|
||||
<Alert
|
||||
message={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
<TriangleAlert type="solid" size={15} color={Color.BG_SAKURA_400} />
|
||||
{`We couldn't establish a connection to your AWS account. Please try again`}
|
||||
</div>
|
||||
}
|
||||
type="error"
|
||||
className="cloud-account-setup-form__alert"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AlertMessage;
|
||||
@@ -0,0 +1,221 @@
|
||||
.cloud-account-setup-modal {
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
&__confirm-selection-count {
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
&__close-button {
|
||||
background: var(--bg-slate-400);
|
||||
border-radius: 2px;
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: 'Inter';
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
&,
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 38px;
|
||||
}
|
||||
&__alert {
|
||||
&.ant-alert {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
&.ant-alert-error {
|
||||
color: var(--bg-sakura-400);
|
||||
border: 1px solid rgba(242, 71, 105, 0.1);
|
||||
background: rgba(242, 71, 105, 0.1);
|
||||
}
|
||||
&.ant-alert-warning {
|
||||
color: var(--bg-amber-400);
|
||||
border: 1px solid rgba(255, 205, 86, 0.1);
|
||||
background: rgba(255, 205, 86, 0.1);
|
||||
}
|
||||
&-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.retry-time {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
&__title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
&__description {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
&__select {
|
||||
.ant-select-selection-item {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
&__form-item {
|
||||
margin: 0;
|
||||
}
|
||||
&__include-all-regions-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
margin-bottom: 12px;
|
||||
&-label {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&__note {
|
||||
padding: 12px;
|
||||
color: var(--bg-robin-400);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.06px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(78, 116, 248, 0.1);
|
||||
background: rgba(78, 116, 248, 0.1);
|
||||
}
|
||||
&__submit-button {
|
||||
border-radius: 2px;
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.cloud-account-setup-modal {
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-400);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
&__title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&__select {
|
||||
.ant-select-selection-item {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&__include-all-regions-switch {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&-label {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__note {
|
||||
color: var(--bg-robin-500);
|
||||
border: 1px solid rgba(78, 116, 248, 0.2);
|
||||
background: rgba(78, 116, 248, 0.1);
|
||||
}
|
||||
|
||||
&__submit-button {
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
&__alert {
|
||||
&.ant-alert-error {
|
||||
color: var(--bg-cherry-500);
|
||||
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||
background: rgba(242, 71, 105, 0.1);
|
||||
}
|
||||
|
||||
&.ant-alert-warning {
|
||||
color: var(--bg-amber-500);
|
||||
border: 1px solid rgba(255, 205, 86, 0.2);
|
||||
background: rgba(255, 205, 86, 0.1);
|
||||
}
|
||||
|
||||
&-message {
|
||||
.retry-time {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import './CloudAccountSetupModal.style.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal';
|
||||
import { SquareArrowOutUpRight } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
||||
import {
|
||||
ActiveViewEnum,
|
||||
IntegrationModalProps,
|
||||
ModalStateEnum,
|
||||
} from '../types';
|
||||
import { RegionForm } from './RegionForm';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import { SuccessView } from './SuccessView';
|
||||
|
||||
function CloudAccountSetupModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: IntegrationModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
modalState,
|
||||
setModalState,
|
||||
isLoading,
|
||||
activeView,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
isGeneratingUrl,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleRegionSelect,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
setActiveView,
|
||||
allRegions,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
} = useIntegrationModal({ onClose });
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
if (modalState === ModalStateEnum.SUCCESS) {
|
||||
return <SuccessView />;
|
||||
}
|
||||
|
||||
if (activeView === ActiveViewEnum.SELECT_REGIONS) {
|
||||
return (
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RegionForm
|
||||
form={form}
|
||||
modalState={modalState}
|
||||
setModalState={setModalState}
|
||||
selectedRegions={selectedRegions}
|
||||
includeAllRegions={includeAllRegions}
|
||||
onIncludeAllRegionsChange={handleIncludeAllRegionsChange}
|
||||
onRegionSelect={handleRegionSelect}
|
||||
onSubmit={handleSubmit}
|
||||
accountId={accountId}
|
||||
selectedDeploymentRegion={selectedDeploymentRegion}
|
||||
handleRegionChange={handleRegionChange}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
modalState,
|
||||
activeView,
|
||||
form,
|
||||
setModalState,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleRegionSelect,
|
||||
handleSubmit,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
]);
|
||||
|
||||
const getSelectedRegionsCount = useCallback(
|
||||
(): number =>
|
||||
selectedRegions.includes('all') ? allRegions.length : selectedRegions.length,
|
||||
[selectedRegions, allRegions],
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const handleGoToDashboards = useCallback((): void => {
|
||||
navigate(ROUTES.ALL_DASHBOARD);
|
||||
}, [navigate]);
|
||||
|
||||
const getModalConfig = useCallback(() => {
|
||||
// Handle success state first
|
||||
if (modalState === ModalStateEnum.SUCCESS) {
|
||||
return {
|
||||
title: 'AWS Webservice Integration',
|
||||
okText: (
|
||||
<div className="cloud-account-setup-success-view__footer-button">
|
||||
Go to Dashboards
|
||||
</div>
|
||||
),
|
||||
block: true,
|
||||
onOk: handleGoToDashboards,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle other views
|
||||
const viewConfigs = {
|
||||
[ActiveViewEnum.FORM]: {
|
||||
title: 'Add AWS Account',
|
||||
okText: (
|
||||
<div className="cloud-account-setup-form__submit-button-content">
|
||||
Launch Cloud Formation Template{' '}
|
||||
<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />
|
||||
</div>
|
||||
),
|
||||
onOk: handleSubmit,
|
||||
disabled:
|
||||
isLoading || isGeneratingUrl || modalState === ModalStateEnum.WAITING,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
},
|
||||
[ActiveViewEnum.SELECT_REGIONS]: {
|
||||
title: 'Which regions do you want to monitor?',
|
||||
okText: `Confirm Selection (${getSelectedRegionsCount()})`,
|
||||
onOk: (): void => setActiveView(ActiveViewEnum.FORM),
|
||||
isLoading: isLoading || isGeneratingUrl,
|
||||
cancelButtonProps: { style: { display: 'block' } },
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
return viewConfigs[activeView];
|
||||
}, [
|
||||
modalState,
|
||||
handleSubmit,
|
||||
getSelectedRegionsCount,
|
||||
isLoading,
|
||||
isGeneratingUrl,
|
||||
activeView,
|
||||
handleGoToDashboards,
|
||||
setActiveView,
|
||||
]);
|
||||
|
||||
const modalConfig = getModalConfig();
|
||||
|
||||
return (
|
||||
<SignozModal
|
||||
open={isOpen}
|
||||
className="cloud-account-setup-modal"
|
||||
title={modalConfig.title}
|
||||
onCancel={handleClose}
|
||||
onOk={modalConfig.onOk}
|
||||
okText={modalConfig.okText}
|
||||
okButtonProps={{
|
||||
loading: isLoading,
|
||||
disabled: selectedRegions.length === 0 || modalConfig.disabled,
|
||||
className:
|
||||
activeView === ActiveViewEnum.FORM
|
||||
? 'cloud-account-setup-form__submit-button'
|
||||
: 'account-setup-modal-footer__confirm-button',
|
||||
block: activeView === ActiveViewEnum.FORM,
|
||||
}}
|
||||
cancelButtonProps={modalConfig.cancelButtonProps}
|
||||
width={672}
|
||||
>
|
||||
{renderContent()}
|
||||
</SignozModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudAccountSetupModal;
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Form, Select, Switch } from 'antd';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { Region } from 'utils/regions';
|
||||
|
||||
// Form section components
|
||||
function RegionDeploymentSection({
|
||||
regions,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
isFormDisabled,
|
||||
}: {
|
||||
regions: Region[];
|
||||
selectedDeploymentRegion: string | undefined;
|
||||
handleRegionChange: (value: string) => void;
|
||||
isFormDisabled: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
<div className="cloud-account-setup-form__title">
|
||||
Where should we deploy the SigNoz Cloud stack?
|
||||
</div>
|
||||
<div className="cloud-account-setup-form__description">
|
||||
Choose the AWS region for CloudFormation stack deployment
|
||||
</div>
|
||||
<Form.Item
|
||||
name="region"
|
||||
rules={[{ required: true, message: 'Please select a region' }]}
|
||||
className="cloud-account-setup-form__form-item"
|
||||
>
|
||||
<Select
|
||||
placeholder="e.g. US East (N. Virginia)"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
className="cloud-account-setup-form__select integrations-select"
|
||||
onChange={handleRegionChange}
|
||||
value={selectedDeploymentRegion}
|
||||
disabled={isFormDisabled}
|
||||
>
|
||||
{regions.flatMap((region) =>
|
||||
region.subRegions.map((subRegion) => (
|
||||
<Select.Option key={subRegion.id} value={subRegion.id}>
|
||||
{subRegion.displayName}
|
||||
</Select.Option>
|
||||
)),
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MonitoringRegionsSection({
|
||||
includeAllRegions,
|
||||
selectedRegions,
|
||||
onIncludeAllRegionsChange,
|
||||
getRegionPreviewText,
|
||||
onRegionSelect,
|
||||
isFormDisabled,
|
||||
}: {
|
||||
includeAllRegions: boolean;
|
||||
selectedRegions: string[];
|
||||
onIncludeAllRegionsChange: (checked: boolean) => void;
|
||||
getRegionPreviewText: (regions: string[]) => string[];
|
||||
onRegionSelect: () => void;
|
||||
isFormDisabled: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
<div className="cloud-account-setup-form__title">
|
||||
Which regions do you want to monitor?
|
||||
</div>
|
||||
<div className="cloud-account-setup-form__description">
|
||||
Choose only the regions you want SigNoz to monitor. You can enable all at
|
||||
once, or pick specific ones:
|
||||
</div>
|
||||
<Form.Item
|
||||
name="monitorRegions"
|
||||
rules={[
|
||||
{
|
||||
validator: async (): Promise<void> => {
|
||||
if (selectedRegions.length === 0) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
message: 'Please select at least one region to monitor',
|
||||
},
|
||||
]}
|
||||
className="cloud-account-setup-form__form-item"
|
||||
>
|
||||
<div className="cloud-account-setup-form__include-all-regions-switch">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={includeAllRegions}
|
||||
onChange={onIncludeAllRegionsChange}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
<button
|
||||
className="cloud-account-setup-form__include-all-regions-switch-label"
|
||||
type="button"
|
||||
onClick={(): void =>
|
||||
!isFormDisabled
|
||||
? onIncludeAllRegionsChange(!includeAllRegions)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Include all regions
|
||||
</button>
|
||||
</div>
|
||||
<Select
|
||||
suffixIcon={null}
|
||||
placeholder="Select Region(s)"
|
||||
className="cloud-account-setup-form__select integrations-select"
|
||||
onClick={!isFormDisabled ? onRegionSelect : undefined}
|
||||
mode="multiple"
|
||||
maxTagCount={3}
|
||||
value={getRegionPreviewText(selectedRegions)}
|
||||
open={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ComplianceNote(): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
<div className="cloud-account-setup-form__note">
|
||||
Note: Some organizations may require the CloudFormation stack to be deployed
|
||||
in the same region as their primary infrastructure for compliance or latency
|
||||
reasons.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ComplianceNote, MonitoringRegionsSection, RegionDeploymentSection };
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Form } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useAccountStatus } from 'hooks/integrations/aws/useAccountStatus';
|
||||
import { useRef } from 'react';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/aws';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import { ModalStateEnum, RegionFormProps } from '../types';
|
||||
import AlertMessage from './AlertMessage';
|
||||
import {
|
||||
ComplianceNote,
|
||||
MonitoringRegionsSection,
|
||||
RegionDeploymentSection,
|
||||
} from './IntegrateNowFormSections';
|
||||
|
||||
const allRegions = (): string[] =>
|
||||
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
|
||||
|
||||
const getRegionPreviewText = (regions: string[]): string[] => {
|
||||
if (regions.includes('all')) {
|
||||
return allRegions();
|
||||
}
|
||||
return regions;
|
||||
};
|
||||
|
||||
export function RegionForm({
|
||||
form,
|
||||
modalState,
|
||||
setModalState,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
onIncludeAllRegionsChange,
|
||||
onRegionSelect,
|
||||
onSubmit,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
}: RegionFormProps): JSX.Element {
|
||||
const startTimeRef = useRef(Date.now());
|
||||
const refetchInterval = 10 * 1000;
|
||||
const errorTimeout = 5 * 60 * 1000;
|
||||
|
||||
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (data: AccountStatusResponse) => {
|
||||
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
|
||||
setModalState(ModalStateEnum.SUCCESS);
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
// 5 minutes in milliseconds
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
});
|
||||
|
||||
const isFormDisabled =
|
||||
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="cloud-account-setup-form"
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<AlertMessage modalState={modalState} />
|
||||
|
||||
<div
|
||||
className={cx(`cloud-account-setup-form__content`, {
|
||||
disabled: isFormDisabled,
|
||||
})}
|
||||
>
|
||||
<RegionDeploymentSection
|
||||
regions={regions}
|
||||
handleRegionChange={handleRegionChange}
|
||||
isFormDisabled={isFormDisabled}
|
||||
selectedDeploymentRegion={selectedDeploymentRegion}
|
||||
/>
|
||||
<MonitoringRegionsSection
|
||||
includeAllRegions={includeAllRegions}
|
||||
selectedRegions={selectedRegions}
|
||||
onIncludeAllRegionsChange={onIncludeAllRegionsChange}
|
||||
getRegionPreviewText={getRegionPreviewText}
|
||||
onRegionSelect={onRegionSelect}
|
||||
isFormDisabled={isFormDisabled}
|
||||
/>
|
||||
<ComplianceNote />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.select-all {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.regions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.region-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.region-group label {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import './RegionSelector.style.scss';
|
||||
|
||||
import { Checkbox } from 'antd';
|
||||
import { useRegionSelection } from 'hooks/integrations/aws/useRegionSelection';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
export function RegionSelector({
|
||||
selectedRegions,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
}: {
|
||||
selectedRegions: string[];
|
||||
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
|
||||
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
allRegionIds,
|
||||
handleSelectAll,
|
||||
handleRegionSelect,
|
||||
} = useRegionSelection({
|
||||
selectedRegions,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="region-selector">
|
||||
<div className="select-all">
|
||||
<Checkbox
|
||||
checked={selectedRegions.includes('all')}
|
||||
indeterminate={
|
||||
selectedRegions.length > 20 &&
|
||||
selectedRegions.length < allRegionIds.length
|
||||
}
|
||||
onChange={(e): void => handleSelectAll(e.target.checked)}
|
||||
>
|
||||
Select All Regions
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div className="regions-grid">
|
||||
{regions.map((region) => (
|
||||
<div key={region.id} className="region-group">
|
||||
<h3>{region.name}</h3>
|
||||
{region.subRegions.map((subRegion) => (
|
||||
<Checkbox
|
||||
key={subRegion.id}
|
||||
checked={
|
||||
selectedRegions.includes('all') ||
|
||||
selectedRegions.includes(subRegion.id)
|
||||
}
|
||||
onChange={(): void => handleRegionSelect(subRegion.id)}
|
||||
>
|
||||
{subRegion.name}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
.cloud-account-setup-success-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
text-align: center;
|
||||
padding-top: 34px;
|
||||
p,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
.cloud-account-setup-success-view {
|
||||
&__title {
|
||||
h3 {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
&__description {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__what-next {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
text-align: left;
|
||||
&-title {
|
||||
color: var(--bg-slate-50);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.what-next-items-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
&__item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
&.ant-alert {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.21px;
|
||||
}
|
||||
&.ant-alert-info {
|
||||
border: 1px solid rgba(63, 94, 204, 0.5);
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
|
||||
.what-next-item {
|
||||
color: var(--bg-robin-400);
|
||||
&-bullet-icon {
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
&-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__footer {
|
||||
padding-top: 18px;
|
||||
.ant-btn {
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lottie-container {
|
||||
position: absolute;
|
||||
width: 743.5px;
|
||||
height: 990.342px;
|
||||
top: -100px;
|
||||
left: -36px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.cloud-account-setup-success-view {
|
||||
&__content {
|
||||
.cloud-account-setup-success-view {
|
||||
&__title {
|
||||
h3 {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__what-next {
|
||||
&-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.what-next-items-wrapper {
|
||||
&__item {
|
||||
&.ant-alert-info {
|
||||
border: 1px solid rgba(63, 94, 204, 0.2);
|
||||
background: rgba(78, 116, 248, 0.1);
|
||||
color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
.what-next-item {
|
||||
color: var(--bg-robin-500);
|
||||
|
||||
&-text {
|
||||
color: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
.ant-btn {
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import './SuccessView.style.scss';
|
||||
|
||||
import { Alert } from 'antd';
|
||||
import integrationsSuccess from 'assets/Lotties/integrations-success.json';
|
||||
import { useState } from 'react';
|
||||
import Lottie from 'react-lottie';
|
||||
|
||||
export function SuccessView(): JSX.Element {
|
||||
const [isAnimationComplete, setIsAnimationComplete] = useState(false);
|
||||
|
||||
const defaultOptions = {
|
||||
loop: false,
|
||||
autoplay: true,
|
||||
animationData: integrationsSuccess,
|
||||
rendererSettings: {
|
||||
preserveAspectRatio: 'xMidYMid slice',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isAnimationComplete && (
|
||||
<div className="lottie-container">
|
||||
<Lottie
|
||||
options={defaultOptions}
|
||||
height={990.342}
|
||||
width={743.5}
|
||||
eventListeners={[
|
||||
{
|
||||
eventName: 'complete',
|
||||
callback: (): void => setIsAnimationComplete(true),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="cloud-account-setup-success-view">
|
||||
<div className="cloud-account-setup-success-view__icon">
|
||||
<img src="Icons/solid-check-circle.svg" alt="Success" />
|
||||
</div>
|
||||
<div className="cloud-account-setup-success-view__content">
|
||||
<div className="cloud-account-setup-success-view__title">
|
||||
<h3>🎉 Success! </h3>
|
||||
<h3>Your AWS Web Service integration is all set.</h3>
|
||||
</div>
|
||||
<div className="cloud-account-setup-success-view__description">
|
||||
<p>Your observability journey is off to a great start. </p>
|
||||
<p>Now that your data is flowing, here’s what you can do next:</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cloud-account-setup-success-view__what-next">
|
||||
<h4 className="cloud-account-setup-success-view__what-next-title">
|
||||
WHAT NEXT
|
||||
</h4>
|
||||
<div className="what-next-items-wrapper">
|
||||
{[
|
||||
'Understand your AWS services with SigNoz’s out-of-the-box dashboards',
|
||||
'Set up alerts for real-time monitoring.',
|
||||
'Track logs and traces.',
|
||||
].map((item) => (
|
||||
<Alert
|
||||
key={item}
|
||||
message={
|
||||
<div className="what-next-items-wrapper__item">
|
||||
<div className="what-next-item-bullet-icon">•</div>
|
||||
<div className="what-next-item-text">{item}</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
className="what-next-items-wrapper__item"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { FormInstance } from 'antd';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
export enum ActiveViewEnum {
|
||||
SELECT_REGIONS = 'select-regions',
|
||||
FORM = 'form',
|
||||
}
|
||||
|
||||
export enum ModalStateEnum {
|
||||
FORM = 'form',
|
||||
WAITING = 'waiting',
|
||||
ERROR = 'error',
|
||||
SUCCESS = 'success',
|
||||
}
|
||||
|
||||
export interface RegionFormProps {
|
||||
form: FormInstance;
|
||||
modalState: ModalStateEnum;
|
||||
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
|
||||
selectedRegions: string[];
|
||||
includeAllRegions: boolean;
|
||||
onIncludeAllRegionsChange: (checked: boolean) => void;
|
||||
onRegionSelect: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
accountId?: string;
|
||||
selectedDeploymentRegion: string | undefined;
|
||||
handleRegionChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export interface IntegrationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ServiceData } from './types';
|
||||
|
||||
function DashboardItem({
|
||||
dashboard,
|
||||
}: {
|
||||
dashboard: ServiceData['assets']['dashboards'][number];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-service-dashboard-item">
|
||||
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
|
||||
<div className="cloud-service-dashboard-item__preview">
|
||||
<img
|
||||
src={dashboard.image}
|
||||
alt={dashboard.title}
|
||||
className="cloud-service-dashboard-item__preview-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CloudServiceDashboards({
|
||||
service,
|
||||
}: {
|
||||
service: ServiceData;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{service.assets.dashboards.map((dashboard) => (
|
||||
<DashboardItem key={dashboard.id} dashboard={dashboard} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudServiceDashboards;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Table } from 'antd';
|
||||
|
||||
import { ServiceData } from './types';
|
||||
|
||||
function CloudServiceDataCollected({
|
||||
logsData,
|
||||
metricsData,
|
||||
}: {
|
||||
logsData: ServiceData['data_collected']['logs'];
|
||||
metricsData: ServiceData['data_collected']['metrics'];
|
||||
}): JSX.Element {
|
||||
const logsColumns = [
|
||||
{
|
||||
title: 'NAME',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: 'PATH',
|
||||
dataIndex: 'path',
|
||||
key: 'path',
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
title: 'FACET TYPE',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: '30%',
|
||||
},
|
||||
];
|
||||
|
||||
const metricsColumns = [
|
||||
{
|
||||
title: 'NAME',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
title: 'UNIT',
|
||||
dataIndex: 'unit',
|
||||
key: 'unit',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: 'TYPE',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: '30%',
|
||||
},
|
||||
];
|
||||
|
||||
const tableProps = {
|
||||
pagination: { pageSize: 20, hideOnSinglePage: true },
|
||||
showHeader: true,
|
||||
size: 'middle' as const,
|
||||
bordered: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cloud-service-data-collected">
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Logs</div>
|
||||
<Table
|
||||
columns={logsColumns}
|
||||
dataSource={logsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected__table-logs"
|
||||
/>
|
||||
</div>
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Metrics</div>
|
||||
<Table
|
||||
columns={metricsColumns}
|
||||
dataSource={metricsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected__table-metrics"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudServiceDataCollected;
|
||||
@@ -0,0 +1,89 @@
|
||||
.configure-service-modal {
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
padding: 14px;
|
||||
|
||||
&-regions-switch-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&-label {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
&-switch-description {
|
||||
margin-top: 4px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
&-form-item {
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-modal-body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ant-modal-footer {
|
||||
margin: 0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.configure-service-modal {
|
||||
&__body {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&-regions-switch-switch {
|
||||
&-label {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&-switch-description {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
&.ant-btn-default {
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-400);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-btn-primary {
|
||||
// Keep primary button same as dark mode
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import './ConfigureServiceModal.styles.scss';
|
||||
|
||||
import { Form, Switch } from 'antd';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
ServiceConfig,
|
||||
SupportedSignals,
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useUpdateServiceConfig } from 'hooks/integrations/aws/useUpdateServiceConfig';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
interface IConfigureServiceModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
serviceName: string;
|
||||
serviceId: string;
|
||||
cloudAccountId: string;
|
||||
supportedSignals: SupportedSignals;
|
||||
initialConfig?: ServiceConfig;
|
||||
}
|
||||
|
||||
function ConfigureServiceModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
serviceName,
|
||||
serviceId,
|
||||
cloudAccountId,
|
||||
initialConfig,
|
||||
supportedSignals,
|
||||
}: IConfigureServiceModalProps): JSX.Element {
|
||||
const [form] = Form.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Track current form values
|
||||
const [currentValues, setCurrentValues] = useState({
|
||||
metrics: initialConfig?.metrics?.enabled || false,
|
||||
logs: initialConfig?.logs?.enabled || false,
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateServiceConfig,
|
||||
isLoading: isUpdating,
|
||||
} = useUpdateServiceConfig();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setIsLoading(true);
|
||||
|
||||
updateServiceConfig(
|
||||
{
|
||||
serviceId,
|
||||
payload: {
|
||||
cloud_account_id: cloudAccountId,
|
||||
config: {
|
||||
logs: {
|
||||
enabled: values.logs,
|
||||
},
|
||||
metrics: {
|
||||
enabled: values.metrics,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([
|
||||
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
|
||||
serviceId,
|
||||
]);
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to update service config:', error);
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
updateServiceConfig,
|
||||
serviceId,
|
||||
cloudAccountId,
|
||||
queryClient,
|
||||
onClose,
|
||||
]);
|
||||
|
||||
const isSaveDisabled = useMemo(
|
||||
() => currentValues.metrics === false && currentValues.logs === false,
|
||||
[currentValues],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
form.resetFields();
|
||||
onClose();
|
||||
}, [form, onClose]);
|
||||
|
||||
return (
|
||||
<SignozModal
|
||||
title={
|
||||
<div className="account-settings-modal__title">Configure {serviceName}</div>
|
||||
}
|
||||
centered
|
||||
open={isOpen}
|
||||
okText="Save"
|
||||
okButtonProps={{
|
||||
disabled: isSaveDisabled,
|
||||
className: 'account-settings-modal__footer-save-button',
|
||||
loading: isLoading || isUpdating,
|
||||
}}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSubmit}
|
||||
cancelText="Close"
|
||||
cancelButtonProps={{
|
||||
className: 'account-settings-modal__footer-close-button',
|
||||
}}
|
||||
width={672}
|
||||
rootClassName=" configure-service-modal"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
metrics: initialConfig?.metrics?.enabled || false,
|
||||
logs: initialConfig?.logs?.enabled || false,
|
||||
}}
|
||||
>
|
||||
<div className=" configure-service-modal__body">
|
||||
{supportedSignals.metrics && (
|
||||
<Form.Item
|
||||
name="metrics"
|
||||
valuePropName="checked"
|
||||
className="configure-service-modal__body-form-item"
|
||||
>
|
||||
<div className="configure-service-modal__body-regions-switch-switch">
|
||||
<Switch
|
||||
checked={currentValues.metrics}
|
||||
onChange={(checked): void => {
|
||||
setCurrentValues((prev) => ({ ...prev, metrics: checked }));
|
||||
form.setFieldsValue({ metrics: checked });
|
||||
}}
|
||||
/>
|
||||
<span className="configure-service-modal__body-regions-switch-switch-label">
|
||||
Metric Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="configure-service-modal__body-switch-description">
|
||||
Metric Collection is enabled for this AWS account. We recommend keeping
|
||||
this enabled, but you can disable metric collection if you do not want
|
||||
to monitor your AWS infrastructure.
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{supportedSignals.logs && (
|
||||
<Form.Item
|
||||
name="logs"
|
||||
valuePropName="checked"
|
||||
className="configure-service-modal__body-form-item"
|
||||
>
|
||||
<div className="configure-service-modal__body-regions-switch-switch">
|
||||
<Switch
|
||||
checked={currentValues.logs}
|
||||
onChange={(checked): void => {
|
||||
setCurrentValues((prev) => ({ ...prev, logs: checked }));
|
||||
form.setFieldsValue({ logs: checked });
|
||||
}}
|
||||
/>
|
||||
<span className="configure-service-modal__body-regions-switch-switch-label">
|
||||
Log Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="configure-service-modal__body-switch-description">
|
||||
To ingest logs from your AWS services, you must complete several steps
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</SignozModal>
|
||||
);
|
||||
}
|
||||
|
||||
ConfigureServiceModal.defaultProps = {
|
||||
initialConfig: {
|
||||
metrics: { enabled: false },
|
||||
logs: { enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
export default ConfigureServiceModal;
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, TabsProps } from 'antd';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import Spinner from 'components/Spinner';
|
||||
import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
|
||||
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
|
||||
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useServiceDetails } from 'hooks/integrations/aws/useServiceDetails';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Wrench } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import ConfigureServiceModal from './ConfigureServiceModal';
|
||||
|
||||
const getStatus = (
|
||||
logsLastReceivedTimestamp: number | undefined,
|
||||
metricsLastReceivedTimestamp: number | undefined,
|
||||
): { text: string; className: string } => {
|
||||
if (!logsLastReceivedTimestamp && !metricsLastReceivedTimestamp) {
|
||||
return { text: 'No Data Yet', className: 'service-status--no-data' };
|
||||
}
|
||||
|
||||
const latestTimestamp = Math.max(
|
||||
logsLastReceivedTimestamp || 0,
|
||||
metricsLastReceivedTimestamp || 0,
|
||||
);
|
||||
|
||||
const isStale = dayjs().diff(dayjs(latestTimestamp), 'minute') > 30;
|
||||
|
||||
if (isStale) {
|
||||
return { text: 'Stale Data', className: 'service-status--stale-data' };
|
||||
}
|
||||
|
||||
return { text: 'Connected', className: 'service-status--connected' };
|
||||
};
|
||||
|
||||
function ServiceStatus({
|
||||
serviceStatus,
|
||||
}: {
|
||||
serviceStatus: IServiceStatus | null;
|
||||
}): JSX.Element {
|
||||
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
|
||||
const metricsLastReceivedTimestamp =
|
||||
serviceStatus?.metrics?.last_received_ts_ms;
|
||||
|
||||
const { text, className } = getStatus(
|
||||
logsLastReceivedTimestamp,
|
||||
metricsLastReceivedTimestamp,
|
||||
);
|
||||
|
||||
return <div className={`service-status ${className}`}>{text}</div>;
|
||||
}
|
||||
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const accountId = urlQuery.get('accountId');
|
||||
const serviceId = urlQuery.get('service');
|
||||
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
|
||||
false,
|
||||
);
|
||||
|
||||
const { data: serviceDetailsData, isLoading } = useServiceDetails(
|
||||
serviceId || '',
|
||||
accountId || undefined,
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size="large" height="50vh" />;
|
||||
}
|
||||
|
||||
if (!serviceDetailsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'dashboards',
|
||||
label: `Dashboards (${serviceDetailsData?.assets.dashboards.length})`,
|
||||
children: <CloudServiceDashboards service={serviceDetailsData} />,
|
||||
},
|
||||
{
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
children: (
|
||||
<CloudServiceDataCollected
|
||||
logsData={serviceDetailsData?.data_collected.logs || []}
|
||||
metricsData={serviceDetailsData?.data_collected.metrics || []}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="service-details">
|
||||
<div className="service-details__title-bar">
|
||||
<div className="service-details__details-title">Details</div>
|
||||
<div className="service-details__right-actions">
|
||||
{serviceDetailsData?.status && (
|
||||
<ServiceStatus serviceStatus={serviceDetailsData.status} />
|
||||
)}
|
||||
{!!accountId && (
|
||||
<Button
|
||||
className="configure-button"
|
||||
onClick={(): void => setIsConfigureServiceModalOpen(true)}
|
||||
>
|
||||
<Wrench size={12} color={Color.BG_VANILLA_400} />
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="service-details__overview">
|
||||
<MarkdownRenderer
|
||||
variables={{}}
|
||||
markdownContent={serviceDetailsData?.overview}
|
||||
/>
|
||||
</div>
|
||||
<div className="service-details__tabs">
|
||||
<Tabs items={tabItems} />
|
||||
</div>
|
||||
<ConfigureServiceModal
|
||||
isOpen={isConfigureServiceModalOpen}
|
||||
onClose={(): void => setIsConfigureServiceModalOpen(false)}
|
||||
serviceName={serviceDetailsData.title}
|
||||
serviceId={serviceId || ''}
|
||||
cloudAccountId={accountId || ''}
|
||||
initialConfig={serviceDetailsData.config}
|
||||
supportedSignals={serviceDetailsData.supported_signals || {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceDetails;
|
||||
@@ -0,0 +1,39 @@
|
||||
import cx from 'classnames';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
|
||||
import { Service } from './types';
|
||||
|
||||
function ServiceItem({
|
||||
service,
|
||||
onClick,
|
||||
isActive,
|
||||
}: {
|
||||
service: Service;
|
||||
onClick: (serviceName: string) => void;
|
||||
isActive?: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
className={cx('service-item', { active: isActive })}
|
||||
onClick={(): void => onClick(service.id)}
|
||||
type="button"
|
||||
>
|
||||
<div className="service-item__icon-wrapper">
|
||||
<img
|
||||
src={service.icon}
|
||||
alt={service.title}
|
||||
className="service-item__icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="service-item__title">
|
||||
<LineClampedText text={service.title} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ServiceItem.defaultProps = {
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
export default ServiceItem;
|
||||
@@ -0,0 +1,52 @@
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
||||
import ServiceItem from './ServiceItem';
|
||||
|
||||
interface ServicesListProps {
|
||||
accountId: string;
|
||||
filter: 'all_services' | 'enabled' | 'available';
|
||||
}
|
||||
|
||||
function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const { data: services = [], isLoading } = useGetAccountServices(accountId);
|
||||
const activeService = urlQuery.get('service');
|
||||
|
||||
const handleServiceClick = (serviceId: string): void => {
|
||||
urlQuery.set('service', serviceId);
|
||||
navigate({ search: urlQuery.toString() });
|
||||
};
|
||||
|
||||
const filteredServices = useMemo(() => {
|
||||
if (filter === 'all_services') return services;
|
||||
|
||||
return services.filter((service) => {
|
||||
const isEnabled =
|
||||
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
|
||||
return filter === 'enabled' ? isEnabled : !isEnabled;
|
||||
});
|
||||
}, [services, filter]);
|
||||
|
||||
if (isLoading) return <Spinner size="large" height="25vh" />;
|
||||
if (!services) return <div>No services found</div>;
|
||||
|
||||
return (
|
||||
<div className="services-list">
|
||||
{filteredServices.map((service) => (
|
||||
<ServiceItem
|
||||
key={service.id}
|
||||
service={service}
|
||||
onClick={handleServiceClick}
|
||||
isActive={service.id === activeService}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesList;
|
||||
@@ -0,0 +1,431 @@
|
||||
.services-tabs {
|
||||
.ant-tabs-tab {
|
||||
font-family: 'Inter';
|
||||
padding: 16px 4px 14px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
font-weight: 400;
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
.ant-tabs-tab-btn {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
.services-section {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
&__sidebar {
|
||||
width: 16%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 84%;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
.services-filter {
|
||||
padding: 16px 0;
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
color: var(--bg-vanilla-400) !important;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--bg-ink-100);
|
||||
}
|
||||
&__icon-wrapper {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 4px;
|
||||
.service-item__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
&__title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.service-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
&__title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
.service-details__details-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
text-align: left;
|
||||
}
|
||||
.service-details__right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.service-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.54px;
|
||||
border-radius: 2px;
|
||||
line-height: normal;
|
||||
&--connected {
|
||||
border: 1px solid rgba(37, 225, 146, 0.1);
|
||||
background: rgba(37, 225, 146, 0.1);
|
||||
color: var(--bg-forest-400);
|
||||
}
|
||||
&--stale-data {
|
||||
background: rgba(255, 215, 120, 0.1);
|
||||
border: 1px solid rgba(255, 215, 120, 0.1);
|
||||
color: var(--bg-amber-400);
|
||||
}
|
||||
&--no-data {
|
||||
border: 1px solid rgba(234, 109, 113, 0.1);
|
||||
background: rgba(234, 109, 113, 0.1);
|
||||
color: var(--bg-cherry-400);
|
||||
}
|
||||
}
|
||||
.configure-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--bg-vanilla-400);
|
||||
background: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__overview {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
width: 800px;
|
||||
}
|
||||
&__tabs {
|
||||
.ant-tabs {
|
||||
&-ink-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
&-nav {
|
||||
padding: 8px 0 18px;
|
||||
&-wrap {
|
||||
padding: 0;
|
||||
}
|
||||
&::before {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
&-tab {
|
||||
margin-left: 0 !important;
|
||||
padding: 0;
|
||||
|
||||
&-btn {
|
||||
padding: 10px 24px;
|
||||
color: var(--text-vanilla-400) !important;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
&[aria-selected='true'] {
|
||||
color: var(--text-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
&-active {
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
&-extra-content {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
&-nav-list {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-400);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cloud-service {
|
||||
&-dashboard-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
&__title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 111.111% */
|
||||
letter-spacing: -0.09px;
|
||||
}
|
||||
&__preview-image {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&-data-collected {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
&__table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.ant-table {
|
||||
background: transparent;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
.ant-table-thead {
|
||||
> tr > th {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
> tr {
|
||||
&:nth-child(odd),
|
||||
&:hover > td {
|
||||
background: rgba(255, 255, 255, 0.01) !important;
|
||||
}
|
||||
|
||||
> td {
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
border: none;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__table-heading {
|
||||
color: var(--bg-slate-50);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.services-tabs {
|
||||
.ant-tabs-tab {
|
||||
&.ant-tabs-tab-active {
|
||||
.ant-tabs-tab-btn {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.services-filter {
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--bg-ink-400) !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.service-item {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.service-details {
|
||||
&__title-bar {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.service-details__details-title {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.configure-button {
|
||||
color: var(--bg-ink-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-400);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.service-status {
|
||||
&--connected {
|
||||
border: 1px solid rgba(37, 225, 146, 0.2);
|
||||
background: rgba(37, 225, 146, 0.1);
|
||||
color: var(--bg-forest-500);
|
||||
}
|
||||
|
||||
&--stale-data {
|
||||
border: 1px solid rgba(255, 215, 120, 0.2);
|
||||
background: rgba(255, 215, 120, 0.1);
|
||||
color: var(--bg-amber-500);
|
||||
}
|
||||
|
||||
&--no-data {
|
||||
border: 1px solid rgba(234, 109, 113, 0.2);
|
||||
background: rgba(234, 109, 113, 0.1);
|
||||
color: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__overview {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
.ant-tabs {
|
||||
&-tab {
|
||||
&-btn {
|
||||
color: var(--bg-ink-400) !important;
|
||||
|
||||
&[aria-selected='true'] {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-active {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
&-nav-list {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-service {
|
||||
&-dashboard-item {
|
||||
&__title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&-data-collected {
|
||||
&__table {
|
||||
.ant-table {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
.ant-table-thead {
|
||||
> tr > th {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
> tr {
|
||||
&:nth-child(odd),
|
||||
&:hover > td {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
|
||||
> td {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table-heading {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { SelectProps, TabsProps } from 'antd';
|
||||
import { Select, Tabs } from 'antd';
|
||||
import { getAwsServices } from 'api/integrations/aws';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import ServiceDetails from './ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
export enum ServiceFilterType {
|
||||
ALL_SERVICES = 'all_services',
|
||||
ENABLED = 'enabled',
|
||||
AVAILABLE = 'available',
|
||||
}
|
||||
|
||||
interface ServicesFilterProps {
|
||||
accountId: string;
|
||||
onFilterChange: (value: ServiceFilterType) => void;
|
||||
}
|
||||
|
||||
function ServicesFilter({
|
||||
accountId,
|
||||
onFilterChange,
|
||||
}: ServicesFilterProps): JSX.Element | null {
|
||||
const { data: services, isLoading } = useQuery(
|
||||
[REACT_QUERY_KEY.AWS_SERVICES, accountId],
|
||||
() => getAwsServices(accountId),
|
||||
);
|
||||
|
||||
const { enabledCount, availableCount } = useMemo(() => {
|
||||
if (!services) return { enabledCount: 0, availableCount: 0 };
|
||||
|
||||
return services.reduce(
|
||||
(acc, service) => {
|
||||
const isEnabled =
|
||||
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
|
||||
return {
|
||||
enabledCount: acc.enabledCount + (isEnabled ? 1 : 0),
|
||||
availableCount: acc.availableCount + (isEnabled ? 0 : 1),
|
||||
};
|
||||
},
|
||||
{ enabledCount: 0, availableCount: 0 },
|
||||
);
|
||||
}, [services]);
|
||||
|
||||
const selectOptions: SelectProps['options'] = useMemo(
|
||||
() => [
|
||||
{ value: 'all_services', label: `All Services (${services?.length || 0})` },
|
||||
{ value: 'enabled', label: `Enabled (${enabledCount})` },
|
||||
{ value: 'available', label: `Available (${availableCount})` },
|
||||
],
|
||||
[services, enabledCount, availableCount],
|
||||
);
|
||||
|
||||
if (isLoading) return null;
|
||||
if (!services?.length) return null;
|
||||
|
||||
return (
|
||||
<div className="services-filter">
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
defaultValue={ServiceFilterType.ALL_SERVICES}
|
||||
options={selectOptions}
|
||||
className="services-sidebar__select"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ServicesSection(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const accountId = urlQuery.get('accountId') || '';
|
||||
|
||||
const [activeFilter, setActiveFilter] = useState<
|
||||
'all_services' | 'enabled' | 'available'
|
||||
>('all_services');
|
||||
|
||||
return (
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesFilter accountId={accountId} onFilterChange={setActiveFilter} />
|
||||
<ServicesList accountId={accountId} filter={activeFilter} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'services',
|
||||
label: 'Services For Integration',
|
||||
children: <ServicesSection />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<Tabs defaultActiveKey="services" items={tabItems} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -0,0 +1,135 @@
|
||||
interface Service {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
config: ServiceConfig;
|
||||
}
|
||||
|
||||
interface Dashboard {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
name: string;
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
name: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface ConfigStatus {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface DataStatus {
|
||||
last_received_ts_ms: number;
|
||||
last_received_from: string;
|
||||
}
|
||||
|
||||
interface ServiceConfig {
|
||||
logs: ConfigStatus;
|
||||
metrics: ConfigStatus;
|
||||
}
|
||||
|
||||
interface IServiceStatus {
|
||||
logs: DataStatus | null;
|
||||
metrics: DataStatus | null;
|
||||
}
|
||||
|
||||
interface SupportedSignals {
|
||||
metrics: boolean;
|
||||
logs: boolean;
|
||||
}
|
||||
|
||||
interface ServiceData {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
overview: string;
|
||||
supported_signals: SupportedSignals;
|
||||
assets: {
|
||||
dashboards: Dashboard[];
|
||||
};
|
||||
data_collected: {
|
||||
logs?: LogField[];
|
||||
metrics: Metric[];
|
||||
};
|
||||
config?: ServiceConfig;
|
||||
status?: IServiceStatus;
|
||||
}
|
||||
|
||||
interface ServiceDetailsResponse {
|
||||
status: 'success';
|
||||
data: ServiceData;
|
||||
}
|
||||
|
||||
interface CloudAccountConfig {
|
||||
regions: string[];
|
||||
}
|
||||
|
||||
interface IntegrationStatus {
|
||||
last_heartbeat_ts_ms: number;
|
||||
}
|
||||
|
||||
interface AccountStatus {
|
||||
integration: IntegrationStatus;
|
||||
}
|
||||
|
||||
interface CloudAccount {
|
||||
id: string;
|
||||
cloud_account_id: string;
|
||||
config: CloudAccountConfig;
|
||||
status: AccountStatus;
|
||||
}
|
||||
|
||||
interface CloudAccountsData {
|
||||
accounts: CloudAccount[];
|
||||
}
|
||||
|
||||
interface UpdateServiceConfigPayload {
|
||||
cloud_account_id: string;
|
||||
config: {
|
||||
logs: {
|
||||
enabled: boolean;
|
||||
};
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateServiceConfigResponse {
|
||||
status: string;
|
||||
data: {
|
||||
id: string;
|
||||
config: {
|
||||
logs: {
|
||||
enabled: boolean;
|
||||
};
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type {
|
||||
CloudAccount,
|
||||
CloudAccountsData,
|
||||
IServiceStatus,
|
||||
Service,
|
||||
ServiceConfig,
|
||||
ServiceData,
|
||||
ServiceDetailsResponse,
|
||||
SupportedSignals,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
};
|
||||
@@ -25,7 +25,10 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultTraceSelectedColumns,
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -408,6 +411,14 @@ function ExplorerOptions({
|
||||
const handleClearSelect = (): void => {
|
||||
removeCurrentViewFromLocalStorage();
|
||||
|
||||
handleOptionsChange({
|
||||
...options,
|
||||
selectColumns:
|
||||
sourcepage === DataSource.TRACES
|
||||
? defaultTraceSelectedColumns
|
||||
: defaultLogsSelectedColumns,
|
||||
});
|
||||
|
||||
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
|
||||
};
|
||||
|
||||
|
||||
@@ -201,6 +201,11 @@ function K8sClustersList({
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const nestedClustersData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sClustersList(
|
||||
query as K8sClustersListPayload,
|
||||
{
|
||||
@@ -283,12 +288,21 @@ function K8sClustersList({
|
||||
|
||||
const selectedClusterData = useMemo(() => {
|
||||
if (!selectedClusterName) return null;
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the cluster from the formatted grouped by clusters data
|
||||
return (
|
||||
nestedClustersData.find(
|
||||
(cluster) => cluster.meta.k8s_cluster_name === selectedClusterName,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the cluster from the clusters data
|
||||
return (
|
||||
clustersData.find(
|
||||
(cluster) => cluster.meta.k8s_cluster_name === selectedClusterName,
|
||||
) || null
|
||||
);
|
||||
}, [selectedClusterName, clustersData]);
|
||||
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
|
||||
|
||||
const handleRowClick = (record: K8sClustersRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -341,6 +355,10 @@ function K8sClustersList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setselectedClusterName(record.clusterUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -220,6 +220,11 @@ function K8sDaemonSetsList({
|
||||
[daemonSetsData, groupBy],
|
||||
);
|
||||
|
||||
const nestedDaemonSetsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const columns = useMemo(() => getK8sDaemonSetsListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sDaemonSetsRowData): void => {
|
||||
@@ -285,13 +290,26 @@ function K8sDaemonSetsList({
|
||||
}, []);
|
||||
|
||||
const selectedDaemonSetData = useMemo(() => {
|
||||
if (!selectedDaemonSetUID) return null;
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the daemonset from the formatted grouped by data
|
||||
return (
|
||||
nestedDaemonSetsData.find(
|
||||
(daemonSet) => daemonSet.daemonSetName === selectedDaemonSetUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the daemonset from the daemonsets data
|
||||
return (
|
||||
daemonSetsData.find(
|
||||
(daemonSet) => daemonSet.daemonSetName === selectedDaemonSetUID,
|
||||
) || null
|
||||
);
|
||||
}, [selectedDaemonSetUID, daemonSetsData]);
|
||||
}, [
|
||||
selectedDaemonSetUID,
|
||||
groupBy.length,
|
||||
daemonSetsData,
|
||||
nestedDaemonSetsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -344,6 +362,10 @@ function K8sDaemonSetsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setselectedDaemonSetUID(record.daemonsetUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -220,6 +220,11 @@ function K8sDeploymentsList({
|
||||
[deploymentsData, groupBy],
|
||||
);
|
||||
|
||||
const nestedDeploymentsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const columns = useMemo(() => getK8sDeploymentsListColumns(groupBy), [
|
||||
groupBy,
|
||||
]);
|
||||
@@ -288,12 +293,26 @@ function K8sDeploymentsList({
|
||||
|
||||
const selectedDeploymentData = useMemo(() => {
|
||||
if (!selectedDeploymentUID) return null;
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the deployment from the formatted grouped by deployments data
|
||||
return (
|
||||
nestedDeploymentsData.find(
|
||||
(deployment) => deployment.deploymentName === selectedDeploymentUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the deployment from the deployments data
|
||||
return (
|
||||
deploymentsData.find(
|
||||
(deployment) => deployment.deploymentName === selectedDeploymentUID,
|
||||
) || null
|
||||
);
|
||||
}, [selectedDeploymentUID, deploymentsData]);
|
||||
}, [
|
||||
selectedDeploymentUID,
|
||||
groupBy.length,
|
||||
deploymentsData,
|
||||
nestedDeploymentsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (record: K8sDeploymentsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -346,6 +365,10 @@ function K8sDeploymentsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setselectedDeploymentUID(record.deploymentUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -884,3 +884,7 @@
|
||||
.ant-table-content {
|
||||
margin-bottom: 60px !important;
|
||||
}
|
||||
|
||||
.expanded-clickable-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -195,6 +195,11 @@ function K8sJobsList({
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const nestedJobsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sJobsList(
|
||||
query as K8sJobsListPayload,
|
||||
{
|
||||
@@ -274,9 +279,13 @@ function K8sJobsList({
|
||||
}, []);
|
||||
|
||||
const selectedJobData = useMemo(() => {
|
||||
if (!selectedJobUID) return null;
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the job from the formatted grouped by data
|
||||
return nestedJobsData.find((job) => job.jobName === selectedJobUID) || null;
|
||||
}
|
||||
// If not grouped by, return the job from the jobs data
|
||||
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
|
||||
}, [selectedJobUID, jobsData]);
|
||||
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
|
||||
|
||||
const handleRowClick = (record: K8sJobsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -329,6 +338,10 @@ function K8sJobsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setselectedJobUID(record.jobUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -219,6 +219,11 @@ function K8sNamespacesList({
|
||||
[namespacesData, groupBy],
|
||||
);
|
||||
|
||||
const nestedNamespacesData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const columns = useMemo(() => getK8sNamespacesListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sNamespacesRowData): void => {
|
||||
@@ -285,12 +290,26 @@ function K8sNamespacesList({
|
||||
|
||||
const selectedNamespaceData = useMemo(() => {
|
||||
if (!selectedNamespaceUID) return null;
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the namespace from the formatted grouped by namespaces data
|
||||
return (
|
||||
nestedNamespacesData.find(
|
||||
(namespace) => namespace.namespaceName === selectedNamespaceUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the node from the nodes data
|
||||
return (
|
||||
namespacesData.find(
|
||||
(namespace) => namespace.namespaceName === selectedNamespaceUID,
|
||||
) || null
|
||||
);
|
||||
}, [selectedNamespaceUID, namespacesData]);
|
||||
}, [
|
||||
selectedNamespaceUID,
|
||||
groupBy.length,
|
||||
namespacesData,
|
||||
nestedNamespacesData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (record: K8sNamespacesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -343,6 +362,10 @@ function K8sNamespacesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setselectedNamespaceUID(record.namespaceUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -188,6 +188,11 @@ function K8sNodesList({
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const nestedNodesData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const formattedGroupedByNodesData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
@@ -274,8 +279,15 @@ function K8sNodesList({
|
||||
|
||||
const selectedNodeData = useMemo(() => {
|
||||
if (!selectedNodeUID) return null;
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the node from the formatted grouped by nodes data
|
||||
return (
|
||||
nestedNodesData.find((node) => node.nodeUID === selectedNodeUID) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the node from the nodes data
|
||||
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
|
||||
}, [selectedNodeUID, nodesData]);
|
||||
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
|
||||
|
||||
const handleRowClick = (record: K8sNodesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -329,6 +341,10 @@ function K8sNodesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setselectedNodeUID(record.nodeUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -227,6 +227,11 @@ function K8sPodsList({
|
||||
const podsData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const nestedPodsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const formattedPodsData = useMemo(
|
||||
() => formatDataForTable(podsData, groupBy),
|
||||
[podsData, groupBy],
|
||||
@@ -313,8 +318,13 @@ function K8sPodsList({
|
||||
|
||||
const selectedPodData = useMemo(() => {
|
||||
if (!selectedPodUID) return null;
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the pod from the formatted grouped by pods data
|
||||
return nestedPodsData.find((pod) => pod.podUID === selectedPodUID) || null;
|
||||
}
|
||||
// If not grouped by, return the node from the nodes data
|
||||
return podsData.find((pod) => pod.podUID === selectedPodUID) || null;
|
||||
}, [selectedPodUID, podsData]);
|
||||
}, [selectedPodUID, groupBy.length, podsData, nestedPodsData]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sPodsRowData): void => {
|
||||
setSelectedRowData(record);
|
||||
@@ -425,6 +435,10 @@ function K8sPodsList({
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setSelectedPodUID(record.podUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -202,6 +202,11 @@ function K8sStatefulSetsList({
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const nestedStatefulSetsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sStatefulSetsList(
|
||||
query as K8sStatefulSetsListPayload,
|
||||
{
|
||||
@@ -288,12 +293,24 @@ function K8sStatefulSetsList({
|
||||
|
||||
const selectedStatefulSetData = useMemo(() => {
|
||||
if (!selectedStatefulSetUID) return null;
|
||||
if (groupBy.length > 0) {
|
||||
return (
|
||||
nestedStatefulSetsData.find(
|
||||
(statefulSet) => statefulSet.statefulSetName === selectedStatefulSetUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
return (
|
||||
statefulSetsData.find(
|
||||
(statefulSet) => statefulSet.statefulSetName === selectedStatefulSetUID,
|
||||
) || null
|
||||
);
|
||||
}, [selectedStatefulSetUID, statefulSetsData]);
|
||||
}, [
|
||||
selectedStatefulSetUID,
|
||||
groupBy.length,
|
||||
statefulSetsData,
|
||||
nestedStatefulSetsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -346,6 +363,10 @@ function K8sStatefulSetsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setselectedStatefulSetUID(record.statefulsetUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -198,6 +198,11 @@ function K8sVolumesList({
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const nestedVolumesData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) return [];
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sVolumesList(
|
||||
query as K8sVolumesListPayload,
|
||||
{
|
||||
@@ -278,12 +283,19 @@ function K8sVolumesList({
|
||||
|
||||
const selectedVolumeData = useMemo(() => {
|
||||
if (!selectedVolumeUID) return null;
|
||||
if (groupBy.length > 0) {
|
||||
return (
|
||||
nestedVolumesData.find(
|
||||
(volume) => volume.persistentVolumeClaimName === selectedVolumeUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
return (
|
||||
volumesData.find(
|
||||
(volume) => volume.persistentVolumeClaimName === selectedVolumeUID,
|
||||
) || null
|
||||
);
|
||||
}, [selectedVolumeUID, volumesData]);
|
||||
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
|
||||
|
||||
const handleRowClick = (record: K8sVolumesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -336,6 +348,10 @@ function K8sVolumesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => setselectedVolumeUID(record.volumeUID),
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
|
||||
@@ -13,6 +13,7 @@ import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
|
||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||
import { Heading } from 'container/LogsTable/styles';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
@@ -53,7 +54,10 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
[logs, activeLogId],
|
||||
);
|
||||
|
||||
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
||||
const selectedFields = convertKeysToColumnFields([
|
||||
...defaultLogsSelectedColumns,
|
||||
...options.selectColumns,
|
||||
]);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
|
||||
@@ -5,7 +5,9 @@ import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ShowButton from 'container/LogsContextList/ShowButton';
|
||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -109,6 +111,7 @@ function ContextLogRenderer({
|
||||
data={logTorender}
|
||||
linesPerRow={1}
|
||||
fontSize={options.fontSize}
|
||||
selectedFields={convertKeysToColumnFields(defaultLogsSelectedColumns)}
|
||||
/>
|
||||
),
|
||||
[log.id, options.fontSize],
|
||||
|
||||
@@ -45,7 +45,7 @@ function EventsTable(props: IEventsTableProps): JSX.Element {
|
||||
)}
|
||||
{events
|
||||
.filter((eve) =>
|
||||
eve.name.toLowerCase().includes(fieldSearchInput.toLowerCase()),
|
||||
eve.name?.toLowerCase().includes(fieldSearchInput.toLowerCase()),
|
||||
)
|
||||
.map((event) => (
|
||||
<div
|
||||
@@ -76,7 +76,7 @@ function EventsTable(props: IEventsTableProps): JSX.Element {
|
||||
<div className="timestamp-container">
|
||||
<Typography.Text className="attribute-value">
|
||||
{getYAxisFormattedValue(
|
||||
`${event.timeUnixNano / 1e6 - startTime}`,
|
||||
`${(event.timeUnixNano || 0) / 1e6 - startTime}`,
|
||||
'ms',
|
||||
)}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -241,18 +241,19 @@
|
||||
height: 54px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-left: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
.span-line {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
height: 12px;
|
||||
top: 35%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.span-line-text {
|
||||
position: absolute;
|
||||
top: 65%;
|
||||
position: relative;
|
||||
top: 40%;
|
||||
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||
slashed-zero;
|
||||
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
|
||||
|
||||
@@ -188,14 +188,13 @@ function SpanDuration({
|
||||
left: `${leftOffset}%`,
|
||||
width: `${width}%`,
|
||||
backgroundColor: color,
|
||||
marginLeft: '15px',
|
||||
}}
|
||||
/>
|
||||
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
|
||||
<Typography.Text
|
||||
className="span-line-text"
|
||||
ellipsis
|
||||
style={{ left: `${leftOffset}%`, color, marginLeft: '15px' }}
|
||||
style={{ left: `${leftOffset}%`, color }}
|
||||
>{`${toFixed(time, 2)} ${timeUnitName}`}</Typography.Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
139
frontend/src/hooks/integrations/aws/useAccountSettingsModal.ts
Normal file
139
frontend/src/hooks/integrations/aws/useAccountSettingsModal.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Form } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useUpdateAccountConfig } from 'hooks/integrations/aws/useUpdateAccountConfig';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { AccountConfigPayload } from 'types/api/integrations/aws';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
interface UseAccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
interface UseAccountSettingsModal {
|
||||
form: FormInstance;
|
||||
isLoading: boolean;
|
||||
selectedRegions: string[];
|
||||
includeAllRegions: boolean;
|
||||
isRegionSelectOpen: boolean;
|
||||
isSaveDisabled: boolean;
|
||||
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
|
||||
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
|
||||
setIsRegionSelectOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleIncludeAllRegionsChange: (checked: boolean) => void;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
const allRegions = (): string[] =>
|
||||
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
|
||||
|
||||
const getRegionPreviewText = (regions: string[] | undefined): string[] => {
|
||||
if (!regions) return [];
|
||||
if (regions.includes('all')) {
|
||||
return allRegions();
|
||||
}
|
||||
return regions;
|
||||
};
|
||||
|
||||
export function useAccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: UseAccountSettingsModalProps): UseAccountSettingsModal {
|
||||
const [form] = Form.useForm();
|
||||
const { mutate: updateConfig, isLoading } = useUpdateAccountConfig();
|
||||
const accountRegions = useMemo(() => account?.config?.regions || [], [
|
||||
account?.config?.regions,
|
||||
]);
|
||||
const [isInitialRegionsSet, setIsInitialRegionsSet] = useState(false);
|
||||
|
||||
const [selectedRegions, setSelectedRegions] = useState<string[]>([]);
|
||||
const [includeAllRegions, setIncludeAllRegions] = useState(false);
|
||||
const [isRegionSelectOpen, setIsRegionSelectOpen] = useState(false);
|
||||
|
||||
// Initialize regions from account when modal opens
|
||||
useEffect(() => {
|
||||
if (accountRegions.length > 0 && !isInitialRegionsSet) {
|
||||
setSelectedRegions(accountRegions);
|
||||
setIsInitialRegionsSet(true);
|
||||
setIncludeAllRegions(accountRegions.includes('all'));
|
||||
}
|
||||
}, [accountRegions, isInitialRegionsSet]);
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
const payload: AccountConfigPayload = {
|
||||
config: {
|
||||
regions: selectedRegions,
|
||||
},
|
||||
};
|
||||
|
||||
updateConfig(
|
||||
{ accountId: account?.id, payload },
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
setActiveAccount(response.data);
|
||||
onClose();
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
selectedRegions,
|
||||
updateConfig,
|
||||
account?.id,
|
||||
setActiveAccount,
|
||||
onClose,
|
||||
]);
|
||||
|
||||
const isSaveDisabled = useMemo(
|
||||
() => isEqual(selectedRegions.sort(), accountRegions.sort()),
|
||||
[selectedRegions, accountRegions],
|
||||
);
|
||||
|
||||
const handleIncludeAllRegionsChange = useCallback((checked: boolean): void => {
|
||||
setIncludeAllRegions(checked);
|
||||
if (checked) {
|
||||
setSelectedRegions(['all']);
|
||||
} else {
|
||||
setSelectedRegions([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsRegionSelectOpen(false);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
isRegionSelectOpen,
|
||||
isSaveDisabled,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
setIsRegionSelectOpen,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
};
|
||||
}
|
||||
|
||||
export { getRegionPreviewText };
|
||||
21
frontend/src/hooks/integrations/aws/useAccountStatus.ts
Normal file
21
frontend/src/hooks/integrations/aws/useAccountStatus.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from 'api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/aws';
|
||||
|
||||
export function useAccountStatus(
|
||||
accountId: string | undefined,
|
||||
options: UseQueryOptions<AccountStatusResponse, AxiosError>,
|
||||
): UseQueryResult<AccountStatusResponse, AxiosError> {
|
||||
return useQuery<AccountStatusResponse, AxiosError>({
|
||||
queryKey: [REACT_QUERY_KEY.AWS_ACCOUNT_STATUS, accountId],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<AccountStatusResponse>(
|
||||
`/cloud-integrations/aws/accounts/${accountId}/status`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
8
frontend/src/hooks/integrations/aws/useAwsAccounts.ts
Normal file
8
frontend/src/hooks/integrations/aws/useAwsAccounts.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { getAwsAccounts } from 'api/integrations/aws';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
|
||||
export function useAwsAccounts(): UseQueryResult<CloudAccount[]> {
|
||||
return useQuery(REACT_QUERY_KEY.AWS_ACCOUNTS, getAwsAccounts);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { generateConnectionUrl } from 'api/integrations/aws';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import {
|
||||
ConnectionUrlResponse,
|
||||
GenerateConnectionUrlPayload,
|
||||
} from 'types/api/integrations/aws';
|
||||
|
||||
export function useGenerateConnectionUrl(): UseMutationResult<
|
||||
ConnectionUrlResponse,
|
||||
AxiosError,
|
||||
GenerateConnectionUrlPayload
|
||||
> {
|
||||
return useMutation<
|
||||
ConnectionUrlResponse,
|
||||
AxiosError,
|
||||
GenerateConnectionUrlPayload
|
||||
>({
|
||||
mutationFn: generateConnectionUrl,
|
||||
});
|
||||
}
|
||||
12
frontend/src/hooks/integrations/aws/useGetAccountServices.ts
Normal file
12
frontend/src/hooks/integrations/aws/useGetAccountServices.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getAwsServices } from 'api/integrations/aws';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { Service } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
|
||||
export function useGetAccountServices(
|
||||
accountId?: string,
|
||||
): UseQueryResult<Service[]> {
|
||||
return useQuery([REACT_QUERY_KEY.AWS_SERVICES, accountId], () =>
|
||||
getAwsServices(accountId),
|
||||
);
|
||||
}
|
||||
164
frontend/src/hooks/integrations/aws/useIntegrationModal.ts
Normal file
164
frontend/src/hooks/integrations/aws/useIntegrationModal.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Form } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import {
|
||||
ActiveViewEnum,
|
||||
ModalStateEnum,
|
||||
} from 'container/CloudIntegrationPage/HeroSection/types';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
ConnectionUrlResponse,
|
||||
GenerateConnectionUrlPayload,
|
||||
} from 'types/api/integrations/aws';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import { useGenerateConnectionUrl } from './useGenerateConnectionUrl';
|
||||
|
||||
interface UseIntegrationModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface UseIntegrationModal {
|
||||
form: FormInstance;
|
||||
modalState: ModalStateEnum;
|
||||
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
|
||||
isLoading: boolean;
|
||||
activeView: ActiveViewEnum;
|
||||
selectedRegions: string[];
|
||||
includeAllRegions: boolean;
|
||||
isGeneratingUrl: boolean;
|
||||
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
|
||||
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
|
||||
handleIncludeAllRegionsChange: (checked: boolean) => void;
|
||||
handleRegionSelect: () => void;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
setActiveView: (view: ActiveViewEnum) => void;
|
||||
allRegions: string[];
|
||||
accountId?: string;
|
||||
selectedDeploymentRegion: string | undefined;
|
||||
handleRegionChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function useIntegrationModal({
|
||||
onClose,
|
||||
}: UseIntegrationModalProps): UseIntegrationModal {
|
||||
const [form] = Form.useForm();
|
||||
const [modalState, setModalState] = useState<ModalStateEnum>(
|
||||
ModalStateEnum.FORM,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [accountId, setAccountId] = useState<string | undefined>(undefined);
|
||||
const [activeView, setActiveView] = useState<ActiveViewEnum>(
|
||||
ActiveViewEnum.FORM,
|
||||
);
|
||||
const [selectedRegions, setSelectedRegions] = useState<string[]>([]);
|
||||
const [includeAllRegions, setIncludeAllRegions] = useState(false);
|
||||
const [selectedDeploymentRegion, setSelectedDeploymentRegion] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const allRegions = useMemo(
|
||||
() => regions.flatMap((r) => r.subRegions.map((sr) => sr.name)),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({ region: selectedDeploymentRegion });
|
||||
}, [selectedDeploymentRegion, form]);
|
||||
|
||||
const handleRegionChange = (value: string): void => {
|
||||
setSelectedDeploymentRegion(value);
|
||||
};
|
||||
|
||||
const handleIncludeAllRegionsChange = useCallback((checked: boolean): void => {
|
||||
setIncludeAllRegions(checked);
|
||||
if (checked) {
|
||||
setSelectedRegions(['all']);
|
||||
} else {
|
||||
setSelectedRegions([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRegionSelect = useCallback((): void => {
|
||||
setActiveView(ActiveViewEnum.SELECT_REGIONS);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setActiveView(ActiveViewEnum.FORM);
|
||||
setSelectedRegions([]);
|
||||
setIncludeAllRegions(false);
|
||||
setModalState(ModalStateEnum.FORM);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const {
|
||||
mutate: generateUrl,
|
||||
isLoading: isGeneratingUrl,
|
||||
} = useGenerateConnectionUrl();
|
||||
|
||||
const handleGenerateUrl = useCallback(
|
||||
(payload: GenerateConnectionUrlPayload): void => {
|
||||
generateUrl(payload, {
|
||||
onSuccess: (data: ConnectionUrlResponse) => {
|
||||
window.open(data.connection_url, '_blank');
|
||||
setModalState(ModalStateEnum.WAITING);
|
||||
setAccountId(data.account_id);
|
||||
},
|
||||
onError: () => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
});
|
||||
},
|
||||
[generateUrl],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const values = await form.validateFields();
|
||||
|
||||
const payload: GenerateConnectionUrlPayload = {
|
||||
agent_config: {
|
||||
region: values.region,
|
||||
},
|
||||
account_config: {
|
||||
regions: includeAllRegions ? ['all'] : selectedRegions,
|
||||
},
|
||||
};
|
||||
|
||||
handleGenerateUrl(payload);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [form, includeAllRegions, selectedRegions, handleGenerateUrl]);
|
||||
|
||||
return {
|
||||
form,
|
||||
modalState,
|
||||
isLoading,
|
||||
activeView,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
isGeneratingUrl,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleRegionSelect,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
setActiveView,
|
||||
allRegions,
|
||||
accountId,
|
||||
setModalState,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
};
|
||||
}
|
||||
58
frontend/src/hooks/integrations/aws/useRegionSelection.ts
Normal file
58
frontend/src/hooks/integrations/aws/useRegionSelection.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
interface UseRegionSelectionProps {
|
||||
selectedRegions: string[];
|
||||
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
|
||||
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
interface UseRegionSelection {
|
||||
allRegionIds: string[];
|
||||
handleSelectAll: (checked: boolean) => void;
|
||||
handleRegionSelect: (regionId: string) => void;
|
||||
}
|
||||
|
||||
export function useRegionSelection({
|
||||
selectedRegions,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
}: UseRegionSelectionProps): UseRegionSelection {
|
||||
const allRegionIds = useMemo(
|
||||
() => regions.flatMap((r) => r.subRegions.map((sr) => sr.id)),
|
||||
[],
|
||||
);
|
||||
const handleSelectAll = (checked: boolean): void => {
|
||||
setIncludeAllRegions(checked);
|
||||
setSelectedRegions(checked ? ['all'] : []);
|
||||
};
|
||||
|
||||
const handleRegionSelect = (regionId: string): void => {
|
||||
if (selectedRegions.includes('all')) {
|
||||
const filteredRegionIds = allRegionIds.filter((id) => id !== regionId);
|
||||
|
||||
setSelectedRegions(filteredRegionIds);
|
||||
setIncludeAllRegions(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedRegions((prev) => {
|
||||
const newSelection = prev.includes(regionId)
|
||||
? prev.filter((id) => id !== regionId)
|
||||
: [...prev, regionId];
|
||||
|
||||
if (newSelection.length === allRegionIds.length) {
|
||||
setIncludeAllRegions(true);
|
||||
return ['all'];
|
||||
}
|
||||
|
||||
return newSelection;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
allRegionIds,
|
||||
handleSelectAll,
|
||||
handleRegionSelect,
|
||||
};
|
||||
}
|
||||
17
frontend/src/hooks/integrations/aws/useServiceDetails.ts
Normal file
17
frontend/src/hooks/integrations/aws/useServiceDetails.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getServiceDetails } from 'api/integrations/aws';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { ServiceData } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
|
||||
export function useServiceDetails(
|
||||
serviceId: string,
|
||||
accountId?: string,
|
||||
): UseQueryResult<ServiceData> {
|
||||
return useQuery(
|
||||
[REACT_QUERY_KEY.AWS_SERVICE_DETAILS, serviceId, accountId],
|
||||
() => getServiceDetails(serviceId, accountId),
|
||||
{
|
||||
enabled: !!serviceId,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { updateAccountConfig } from 'api/integrations/aws';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import {
|
||||
AccountConfigPayload,
|
||||
AccountConfigResponse,
|
||||
} from 'types/api/integrations/aws';
|
||||
|
||||
interface UpdateConfigVariables {
|
||||
accountId: string;
|
||||
payload: AccountConfigPayload;
|
||||
}
|
||||
|
||||
export function useUpdateAccountConfig(): UseMutationResult<
|
||||
AccountConfigResponse,
|
||||
Error,
|
||||
UpdateConfigVariables
|
||||
> {
|
||||
return useMutation<AccountConfigResponse, Error, UpdateConfigVariables>({
|
||||
mutationFn: ({ accountId, payload }) =>
|
||||
updateAccountConfig(accountId, payload),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { updateServiceConfig } from 'api/integrations/aws';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
|
||||
interface UpdateServiceConfigPayload {
|
||||
cloud_account_id: string;
|
||||
config: {
|
||||
logs: {
|
||||
enabled: boolean;
|
||||
};
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateConfigResponse {
|
||||
status: string;
|
||||
data: {
|
||||
id: string;
|
||||
config: {
|
||||
logs: {
|
||||
enabled: boolean;
|
||||
};
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateConfigVariables {
|
||||
serviceId: string;
|
||||
payload: UpdateServiceConfigPayload;
|
||||
}
|
||||
|
||||
export function useUpdateServiceConfig(): UseMutationResult<
|
||||
UpdateConfigResponse,
|
||||
Error,
|
||||
UpdateConfigVariables
|
||||
> {
|
||||
return useMutation<UpdateConfigResponse, Error, UpdateConfigVariables>({
|
||||
mutationFn: ({ serviceId, payload }) =>
|
||||
updateServiceConfig(serviceId, payload),
|
||||
});
|
||||
}
|
||||
@@ -106,6 +106,9 @@ export const useThemeConfig = (): ThemeConfig => {
|
||||
Input: {
|
||||
colorBorder: isDarkMode ? '#1D212D' : '#E9E9E9',
|
||||
},
|
||||
Breadcrumb: {
|
||||
separatorMargin: 4,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Card } from 'antd';
|
||||
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
|
||||
import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
@@ -109,6 +111,18 @@ export default function OverviewRightPanelGraph({
|
||||
},
|
||||
[navigateToTraces, filters, selectedTimeStamp],
|
||||
);
|
||||
|
||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
||||
isDarkMode: false,
|
||||
drawStyle: 'bars',
|
||||
colorMapping: {
|
||||
False: Color.BG_CHERRY_500,
|
||||
True: Color.BG_FOREST_400,
|
||||
None: Color.BG_SLATE_200,
|
||||
'Request Rate': Color.BG_ROBIN_500,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="overview-right-panel-graph-card">
|
||||
<div className="request-rate-card">
|
||||
@@ -125,6 +139,7 @@ export default function OverviewRightPanelGraph({
|
||||
headerMenuList={[...ViewMenuAction]}
|
||||
onDragSelect={onDragSelect}
|
||||
onClickHandler={handleGraphClick('Celery_request_rate')}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
</div>
|
||||
<div className="error-rate-card">
|
||||
@@ -141,6 +156,7 @@ export default function OverviewRightPanelGraph({
|
||||
headerMenuList={[...ViewMenuAction]}
|
||||
onDragSelect={onDragSelect}
|
||||
onClickHandler={handleGraphClick('Celery_error_rate')}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
</div>
|
||||
<div className="avg-latency-card">
|
||||
|
||||
@@ -195,6 +195,7 @@ export const celeryOverviewRequestRateGraphData = (
|
||||
},
|
||||
],
|
||||
panelTypes: PANEL_TYPES.BAR,
|
||||
yAxisUnit: 'reqps',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -206,22 +207,24 @@ export const celeryOverviewErrorRateGraphData = (
|
||||
): Widgets =>
|
||||
getWidgetQueryBuilder(
|
||||
getWidgetQuery({
|
||||
title: 'Error',
|
||||
title: 'Error %',
|
||||
description: 'Represents Error in the service',
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count_distinct',
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.EMPTY,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
dataType: DataTypes.String,
|
||||
id: 'span_id--string----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: '',
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: DataSource.TRACES,
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
timeAggregation: 'count_distinct',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [
|
||||
...(filters ?? []),
|
||||
@@ -241,31 +244,31 @@ export const celeryOverviewErrorRateGraphData = (
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: groupByFilter ? [groupByFilter] : [],
|
||||
expression: 'A',
|
||||
disabled: true,
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
having: [],
|
||||
legend: 'True',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
groupBy: groupByFilter ? [groupByFilter] : [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'B',
|
||||
aggregateOperator: 'count_distinct',
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.EMPTY,
|
||||
id: '------false',
|
||||
isColumn: false,
|
||||
dataType: DataTypes.String,
|
||||
id: 'span_id--string----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: '',
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: DataSource.TRACES,
|
||||
disabled: false,
|
||||
expression: 'B',
|
||||
timeAggregation: 'count_distinct',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [
|
||||
...(filters ?? []),
|
||||
@@ -285,20 +288,60 @@ export const celeryOverviewErrorRateGraphData = (
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: groupByFilter ? [groupByFilter] : [],
|
||||
expression: 'B',
|
||||
disabled: true,
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
having: [],
|
||||
legend: 'False',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
groupBy: groupByFilter ? [groupByFilter] : [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'C',
|
||||
aggregateOperator: 'count_distinct',
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'span_id--string----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'count_distinct',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: filters ?? [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'C',
|
||||
disabled: true,
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: groupByFilter ? [groupByFilter] : [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
{
|
||||
queryName: 'F1',
|
||||
expression: '(A/C)*100',
|
||||
disabled: false,
|
||||
legend: 'True',
|
||||
} as any,
|
||||
{
|
||||
queryName: 'F2',
|
||||
expression: '(B/C)*100',
|
||||
disabled: false,
|
||||
legend: 'False',
|
||||
} as any,
|
||||
],
|
||||
panelTypes: PANEL_TYPES.BAR,
|
||||
yAxisUnit: 'percent',
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
line-height: 32px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.celery-content-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import './CeleryTask.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import CeleryTaskConfigOptions from 'components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions';
|
||||
import CeleryTaskDetail, {
|
||||
CaptureDataProps,
|
||||
} from 'components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail';
|
||||
import CeleryTaskGraphGrid from 'components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphGrid';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { Check, Share2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
export default function CeleryTask(): JSX.Element {
|
||||
const [task, setTask] = useState<CaptureDataProps | null>(null);
|
||||
@@ -15,15 +19,43 @@ export default function CeleryTask(): JSX.Element {
|
||||
setTask(captureData);
|
||||
};
|
||||
|
||||
const [isURLCopied, setIsURLCopied] = useState(false);
|
||||
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<div className="celery-task-container">
|
||||
<div className="celery-content">
|
||||
<div className="celery-content-header">
|
||||
<p className="celery-content-header-title">Celery Task</p>
|
||||
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
|
||||
<p className="celery-content-header-title">Celery</p>
|
||||
<div className="celery-content-header-right">
|
||||
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
|
||||
<Tooltip title="Share this" arrow={false}>
|
||||
<Button
|
||||
className="periscope-btn copy-url-btn"
|
||||
onClick={(): void => {
|
||||
handleCopyToClipboard(window.location.href);
|
||||
setIsURLCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsURLCopied(false);
|
||||
}, 1000);
|
||||
}}
|
||||
icon={
|
||||
isURLCopied ? (
|
||||
<Check size={14} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<Share2 size={14} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<CeleryTaskConfigOptions />
|
||||
<CeleryTaskGraphGrid onClick={onTaskClick} queryEnabled={!task} />
|
||||
<CeleryTaskGraphGrid
|
||||
onClick={onTaskClick}
|
||||
queryEnabled={!task}
|
||||
configureOptionComponent={<CeleryTaskConfigOptions />}
|
||||
/>
|
||||
</div>
|
||||
{!!task && (
|
||||
<CeleryTaskDetail
|
||||
|
||||
@@ -23,7 +23,7 @@ interface IntegrationDetailHeaderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
refetchIntegrationDetails: () => void;
|
||||
onUnInstallSuccess: () => void;
|
||||
connectionState: ConnectionStates;
|
||||
connectionData: IntegrationConnectionStatus;
|
||||
setActiveDetailTab: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
@@ -39,7 +39,7 @@ function IntegrationDetailHeader(
|
||||
description,
|
||||
connectionState,
|
||||
connectionData,
|
||||
refetchIntegrationDetails,
|
||||
onUnInstallSuccess,
|
||||
setActiveDetailTab,
|
||||
} = props;
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -62,7 +62,7 @@ function IntegrationDetailHeader(
|
||||
installIntegration,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchIntegrationDetails();
|
||||
onUnInstallSuccess();
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
|
||||
@@ -300,13 +300,13 @@
|
||||
|
||||
.uninstall-integration-btn {
|
||||
border-radius: 2px;
|
||||
background: var(--Accent---Secondary-Cherry, #da5565);
|
||||
border-color: unset !important;
|
||||
background: var(--bg-cherry-500);
|
||||
border: none !important;
|
||||
padding: 9px 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-100);
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
@@ -317,7 +317,7 @@
|
||||
|
||||
.uninstall-integration-btn:hover {
|
||||
&.ant-btn-default {
|
||||
color: var(--bg-ink-300) !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
logs: null,
|
||||
metrics: null,
|
||||
})}
|
||||
refetchIntegrationDetails={refetch}
|
||||
onUnInstallSuccess={refetch}
|
||||
setActiveDetailTab={setActiveDetailTab}
|
||||
/>
|
||||
<IntegrationDetailContent
|
||||
@@ -140,7 +140,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
<IntergrationsUninstallBar
|
||||
integrationTitle={defaultTo(integrationData?.title, '')}
|
||||
integrationId={selectedIntegration}
|
||||
refetchIntegrationDetails={refetch}
|
||||
onUnInstallSuccess={refetch}
|
||||
connectionStatus={connectionStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -15,8 +15,9 @@ import { ConnectionStates } from './TestConnection';
|
||||
interface IntergrationsUninstallBarProps {
|
||||
integrationTitle: string;
|
||||
integrationId: string;
|
||||
refetchIntegrationDetails: () => void;
|
||||
onUnInstallSuccess: () => void;
|
||||
connectionStatus: ConnectionStates;
|
||||
removeIntegrationTitle?: string;
|
||||
}
|
||||
function IntergrationsUninstallBar(
|
||||
props: IntergrationsUninstallBarProps,
|
||||
@@ -24,8 +25,9 @@ function IntergrationsUninstallBar(
|
||||
const {
|
||||
integrationTitle,
|
||||
integrationId,
|
||||
refetchIntegrationDetails,
|
||||
onUnInstallSuccess,
|
||||
connectionStatus,
|
||||
removeIntegrationTitle = 'Remove from SigNoz',
|
||||
} = props;
|
||||
const { notifications } = useNotifications();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -35,7 +37,7 @@ function IntergrationsUninstallBar(
|
||||
isLoading: isUninstallLoading,
|
||||
} = useMutation(unInstallIntegration, {
|
||||
onSuccess: () => {
|
||||
refetchIntegrationDetails();
|
||||
onUnInstallSuccess?.();
|
||||
setIsModalOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
@@ -79,7 +81,7 @@ function IntergrationsUninstallBar(
|
||||
icon={<X size={14} />}
|
||||
onClick={(): void => showModal()}
|
||||
>
|
||||
Remove from SigNoz
|
||||
{removeIntegrationTitle}
|
||||
</Button>
|
||||
<Modal
|
||||
className="remove-integration-modal"
|
||||
@@ -103,4 +105,8 @@ function IntergrationsUninstallBar(
|
||||
);
|
||||
}
|
||||
|
||||
IntergrationsUninstallBar.defaultProps = {
|
||||
removeIntegrationTitle: 'Remove from SigNoz',
|
||||
};
|
||||
|
||||
export default IntergrationsUninstallBar;
|
||||
|
||||
@@ -133,6 +133,19 @@
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
&__new-tag {
|
||||
color: var(--bg-forest-400);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.54px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(37, 225, 146, 0.1);
|
||||
background: rgba(37, 225, 146, 0.1);
|
||||
padding: 2px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
|
||||
@@ -7,9 +7,18 @@ import { Button, List, Typography } from 'antd';
|
||||
import { useGetAllIntegrations } from 'hooks/Integrations/useGetAllIntegrations';
|
||||
import { MoveUpRight, RotateCw } from 'lucide-react';
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { IntegrationsProps } from 'types/api/integrations/types';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import { handleContactSupport } from './utils';
|
||||
import { handleContactSupport, INTEGRATION_TYPES } from './utils';
|
||||
|
||||
export const AWS_INTEGRATION = {
|
||||
id: INTEGRATION_TYPES.AWS_INTEGRATION,
|
||||
title: 'AWS Web Services',
|
||||
description: 'One-click setup for AWS monitoring with SigNoz',
|
||||
icon: `Logos/aws-dark.svg`,
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
interface IntegrationsListProps {
|
||||
setSelectedIntegration: (id: string) => void;
|
||||
@@ -30,12 +39,24 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
|
||||
} = useGetAllIntegrations();
|
||||
|
||||
const filteredDataList = useMemo(() => {
|
||||
let integrationsList: IntegrationsProps[] = [];
|
||||
|
||||
// Temporarily hide AWS Integration from the list, uncomment when the BE changes are finalized
|
||||
// if (AWS_INTEGRATION.title.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
// integrationsList.push(AWS_INTEGRATION);
|
||||
// }
|
||||
|
||||
// Add other integrations
|
||||
if (data?.data.data.integrations) {
|
||||
return data?.data.data.integrations.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
integrationsList = [
|
||||
...integrationsList,
|
||||
...data.data.data.integrations.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
|
||||
return integrationsList;
|
||||
}, [data?.data.data.integrations, searchTerm]);
|
||||
|
||||
const loading = isLoading || isFetching || isRefetching;
|
||||
@@ -93,7 +114,10 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
|
||||
<img src={item.icon} alt={item.title} className="list-item-image" />
|
||||
</div>
|
||||
<div className="list-item-details">
|
||||
<Typography.Text className="heading">{item.title}</Typography.Text>
|
||||
<Typography.Text className="heading">
|
||||
{item.title}
|
||||
{item.is_new && <div className="heading__new-tag">NEW</div>}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="description">
|
||||
{item.description}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -20,3 +20,7 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
|
||||
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
|
||||
'Integrations Detail Page: Navigated to configure an integration',
|
||||
};
|
||||
|
||||
export const INTEGRATION_TYPES = {
|
||||
AWS_INTEGRATION: 'aws-integration',
|
||||
};
|
||||
|
||||
@@ -2,18 +2,27 @@ import './IntegrationsModulePage.styles.scss';
|
||||
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import CloudIntegrationPage from 'container/CloudIntegrationPage/CloudIntegrationPage';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
|
||||
import { useLocation } from 'react-use';
|
||||
|
||||
import { installedIntegrations } from './constants';
|
||||
|
||||
function IntegrationsModulePage(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const urlQuery = useUrlQuery();
|
||||
const selectedIntegration = urlQuery.get('integration');
|
||||
|
||||
const routes: TabRoutes[] = [installedIntegrations];
|
||||
return (
|
||||
<div className="integrations-module-container">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
{selectedIntegration === INTEGRATION_TYPES.AWS_INTEGRATION ? (
|
||||
<CloudIntegrationPage />
|
||||
) : (
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { isEqual, isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { WrapperStyled } from './styles';
|
||||
@@ -86,25 +87,80 @@ function LogsExplorer(): JSX.Element {
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
|
||||
const migrateOptionsQuery = (query: OptionsQuery): OptionsQuery => {
|
||||
// If version is missing AND timestamp/body are not in selectColumns, this is an old URL
|
||||
if (
|
||||
!query.version &&
|
||||
!query.selectColumns.some((col) => col.key === 'timestamp') &&
|
||||
!query.selectColumns.some((col) => col.key === 'body')
|
||||
) {
|
||||
// Get and parse stored columns from localStorage
|
||||
const logListOptionsFromLocalStorage = useMemo(() => {
|
||||
const data = getLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const hasValidColumns = Array.isArray(parsed?.selectColumns);
|
||||
|
||||
return hasValidColumns ? parsed.selectColumns : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check if the columns have the required columns (timestamp, body)
|
||||
const hasRequiredColumns = useCallback(
|
||||
(columns?: Array<{ key: string }> | null): boolean => {
|
||||
if (!columns?.length) return false;
|
||||
|
||||
const hasTimestamp = columns.some((col) => col.key === 'timestamp');
|
||||
const hasBody = columns.some((col) => col.key === 'body');
|
||||
|
||||
return hasTimestamp && hasBody;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Merge the columns with the required columns (timestamp, body) if missing
|
||||
const mergeWithRequiredColumns = useCallback(
|
||||
(columns: BaseAutocompleteData[]): BaseAutocompleteData[] => [
|
||||
// Add required columns (timestamp, body) if missing
|
||||
...(!hasRequiredColumns(columns) ? defaultLogsSelectedColumns : []),
|
||||
...columns,
|
||||
],
|
||||
[hasRequiredColumns],
|
||||
);
|
||||
|
||||
// Migrate the options query to the new format
|
||||
const migrateOptionsQuery = useCallback(
|
||||
(query: OptionsQuery): OptionsQuery => {
|
||||
// Skip if already migrated
|
||||
if (query.version) return query;
|
||||
|
||||
// Case 1: query has columns
|
||||
if (query.selectColumns.length > 0) {
|
||||
return {
|
||||
...query,
|
||||
version: 1,
|
||||
selectColumns: mergeWithRequiredColumns(query.selectColumns),
|
||||
};
|
||||
}
|
||||
|
||||
// Case 2: No query columns in but we have localStorage columns
|
||||
if (logListOptionsFromLocalStorage?.selectColumns?.length > 0) {
|
||||
return {
|
||||
...query,
|
||||
version: 1,
|
||||
selectColumns: mergeWithRequiredColumns(
|
||||
logListOptionsFromLocalStorage.selectColumns,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Case 3: No columns anywhere, use defaults
|
||||
return {
|
||||
...query,
|
||||
version: 1,
|
||||
selectColumns: [
|
||||
// Add default timestamp and body columns
|
||||
...defaultLogsSelectedColumns,
|
||||
...query.selectColumns,
|
||||
],
|
||||
selectColumns: defaultLogsSelectedColumns,
|
||||
};
|
||||
}
|
||||
return query;
|
||||
};
|
||||
},
|
||||
[mergeWithRequiredColumns, logListOptionsFromLocalStorage?.selectColumns],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const migratedQuery = migrateOptionsQuery(optionsQueryData);
|
||||
@@ -112,7 +168,7 @@ function LogsExplorer(): JSX.Element {
|
||||
if (!isEqual(migratedQuery, optionsQueryData)) {
|
||||
redirectWithOptionsData(migratedQuery);
|
||||
}
|
||||
}, [optionsQueryData, redirectWithOptionsData]);
|
||||
}, [migrateOptionsQuery, optionsQueryData, redirectWithOptionsData]);
|
||||
|
||||
const isMultipleQueries = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -38,7 +38,7 @@ export const Celery: TabRoutes = {
|
||||
Component: CeleryTask,
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Rows3 size={16} /> Celery Task
|
||||
<Rows3 size={16} /> Celery
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
|
||||
|
||||
@@ -68,11 +68,11 @@ function TraceDetailsV2(): JSX.Element {
|
||||
() =>
|
||||
!isFetchingTraceData &&
|
||||
!errorFetchingTraceData &&
|
||||
defaultTo(traceData?.payload?.spans.length, 0) === 0,
|
||||
defaultTo(traceData?.payload?.spans?.length, 0) === 0,
|
||||
[
|
||||
errorFetchingTraceData,
|
||||
isFetchingTraceData,
|
||||
traceData?.payload?.spans.length,
|
||||
traceData?.payload?.spans?.length,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
39
frontend/src/types/api/integrations/aws.ts
Normal file
39
frontend/src/types/api/integrations/aws.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
|
||||
export interface GenerateConnectionUrlPayload {
|
||||
agent_config: {
|
||||
region: string;
|
||||
};
|
||||
account_config: {
|
||||
regions: string[];
|
||||
};
|
||||
account_id?: string;
|
||||
}
|
||||
|
||||
export interface ConnectionUrlResponse {
|
||||
connection_url: string;
|
||||
account_id: string;
|
||||
}
|
||||
|
||||
export interface AccountStatusResponse {
|
||||
status: 'success';
|
||||
data: {
|
||||
id: string;
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: number | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface AccountConfigPayload {
|
||||
config: {
|
||||
regions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AccountConfigResponse {
|
||||
status: string;
|
||||
data: CloudAccount;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
interface IntegrationsProps {
|
||||
export interface IntegrationsProps {
|
||||
author: {
|
||||
email: string;
|
||||
homepage: string;
|
||||
@@ -8,6 +8,7 @@ interface IntegrationsProps {
|
||||
id: string;
|
||||
icon: string;
|
||||
is_installed: boolean;
|
||||
is_new?: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
|
||||
152
frontend/src/utils/regions.ts
Normal file
152
frontend/src/utils/regions.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
export interface Region {
|
||||
id: string;
|
||||
name: string;
|
||||
subRegions: SubRegion[];
|
||||
}
|
||||
export interface SubRegion {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
const regions: Region[] = [
|
||||
{
|
||||
id: 'north-america',
|
||||
name: 'North America',
|
||||
subRegions: [
|
||||
{ id: 'us-east-1', name: 'us-east-1', displayName: 'US East (N. Virginia)' },
|
||||
{ id: 'us-east-2', name: 'us-east-2', displayName: 'US East (Ohio)' },
|
||||
{
|
||||
id: 'us-west-1',
|
||||
name: 'us-west-1',
|
||||
displayName: 'US West (N. California)',
|
||||
},
|
||||
{ id: 'us-west-2', name: 'us-west-2', displayName: 'US West (Oregon)' },
|
||||
{
|
||||
id: 'ca-central-1',
|
||||
name: 'ca-central-1',
|
||||
displayName: 'Canada (Central)',
|
||||
},
|
||||
{ id: 'ca-west-1', name: 'ca-west-1', displayName: 'Canada (West)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'africa',
|
||||
name: 'Africa',
|
||||
subRegions: [
|
||||
{ id: 'af-south-1', name: 'af-south-1', displayName: 'Africa (Cape Town)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'asia-pacific',
|
||||
name: 'Asia Pacific',
|
||||
subRegions: [
|
||||
{
|
||||
id: 'ap-east-1',
|
||||
name: 'ap-east-1',
|
||||
displayName: 'Asia Pacific (Hong Kong)',
|
||||
},
|
||||
{
|
||||
id: 'ap-northeast-1',
|
||||
name: 'ap-northeast-1',
|
||||
displayName: 'Asia Pacific (Tokyo)',
|
||||
},
|
||||
{
|
||||
id: 'ap-northeast-2',
|
||||
name: 'ap-northeast-2',
|
||||
displayName: 'Asia Pacific (Seoul)',
|
||||
},
|
||||
{
|
||||
id: 'ap-northeast-3',
|
||||
name: 'ap-northeast-3',
|
||||
displayName: 'Asia Pacific (Osaka)',
|
||||
},
|
||||
{
|
||||
id: 'ap-south-1',
|
||||
name: 'ap-south-1',
|
||||
displayName: 'Asia Pacific (Mumbai)',
|
||||
},
|
||||
{
|
||||
id: 'ap-south-2',
|
||||
name: 'ap-south-2',
|
||||
displayName: 'Asia Pacific (Hyderabad)',
|
||||
},
|
||||
{
|
||||
id: 'ap-southeast-1',
|
||||
name: 'ap-southeast-1',
|
||||
displayName: 'Asia Pacific (Singapore)',
|
||||
},
|
||||
{
|
||||
id: 'ap-southeast-2',
|
||||
name: 'ap-southeast-2',
|
||||
displayName: 'Asia Pacific (Sydney)',
|
||||
},
|
||||
{
|
||||
id: 'ap-southeast-3',
|
||||
name: 'ap-southeast-3',
|
||||
displayName: 'Asia Pacific (Jakarta)',
|
||||
},
|
||||
{
|
||||
id: 'ap-southeast-4',
|
||||
name: 'ap-southeast-4',
|
||||
displayName: 'Asia Pacific (Melbourne)',
|
||||
},
|
||||
{
|
||||
id: 'ap-southeast-5',
|
||||
name: 'ap-southeast-5',
|
||||
displayName: 'Asia Pacific (Auckland)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'europe',
|
||||
name: 'Europe',
|
||||
subRegions: [
|
||||
{
|
||||
id: 'eu-central-1',
|
||||
name: 'eu-central-1',
|
||||
displayName: 'Europe (Frankfurt)',
|
||||
},
|
||||
{ id: 'eu-central-2', name: 'eu-central-2', displayName: 'Europe (Zurich)' },
|
||||
{ id: 'eu-north-1', name: 'eu-north-1', displayName: 'Europe (Stockholm)' },
|
||||
{ id: 'eu-south-1', name: 'eu-south-1', displayName: 'Europe (Milan)' },
|
||||
{ id: 'eu-south-2', name: 'eu-south-2', displayName: 'Europe (Spain)' },
|
||||
{ id: 'eu-west-1', name: 'eu-west-1', displayName: 'Europe (Ireland)' },
|
||||
{ id: 'eu-west-2', name: 'eu-west-2', displayName: 'Europe (London)' },
|
||||
{ id: 'eu-west-3', name: 'eu-west-3', displayName: 'Europe (Paris)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'middle-east',
|
||||
name: 'Middle East',
|
||||
subRegions: [
|
||||
{
|
||||
id: 'il-central-1',
|
||||
name: 'il-central-1',
|
||||
displayName: 'Israel (Tel Aviv)',
|
||||
},
|
||||
{
|
||||
id: 'me-central-1',
|
||||
name: 'me-central-1',
|
||||
displayName: 'Middle East (UAE)',
|
||||
},
|
||||
{
|
||||
id: 'me-south-1',
|
||||
name: 'me-south-1',
|
||||
displayName: 'Middle East (Bahrain)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'south-america',
|
||||
name: 'South America',
|
||||
subRegions: [
|
||||
{
|
||||
id: 'sa-east-1',
|
||||
name: 'sa-east-1',
|
||||
displayName: 'South America (São Paulo)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export { regions };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user