mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-05 01:40:33 +01:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1224222d5b | ||
|
|
c8e7abaa85 | ||
|
|
0b7cd4c1a7 | ||
|
|
62c033ccf8 | ||
|
|
e637487984 | ||
|
|
8fc43a00f8 | ||
|
|
031d62ca44 | ||
|
|
8c4c357351 | ||
|
|
d8d8191a32 | ||
|
|
a876c0a744 | ||
|
|
c36f913a90 | ||
|
|
ed597f00c0 | ||
|
|
4957d3ae93 | ||
|
|
8835e3493d | ||
|
|
027a1631ef | ||
|
|
d7a6607a25 | ||
|
|
7a58bc58c9 | ||
|
|
88be23c3e3 | ||
|
|
8f095dfbc9 | ||
|
|
72207691a3 | ||
|
|
8998ca652e | ||
|
|
f4ae5f19ff | ||
|
|
d1ea608671 | ||
|
|
ac7ecac2c1 | ||
|
|
64071165c4 | ||
|
|
9c25a33cd9 | ||
|
|
3100d602c4 | ||
|
|
d80908a1fc | ||
|
|
bc17a10550 | ||
|
|
694c185373 | ||
|
|
02f3dfefb9 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -54,6 +54,7 @@ ee/query-service/tests/test-deploy/data/
|
||||
bin/
|
||||
.local/
|
||||
*/query-service/queries.active
|
||||
ee/query-service/db
|
||||
|
||||
# e2e
|
||||
|
||||
|
||||
@@ -77,4 +77,4 @@ Need assistance? Join our Slack community:
|
||||
## Where do I go from here?
|
||||
|
||||
- Set up your [development environment](docs/contributing/development.md)
|
||||
- Deploy and observe [SigNoz in action with OpenTelemetry Demo Application](docs/otel-demo/otel-demo-docs.md)
|
||||
- Deploy and observe [SigNoz in action with OpenTelemetry Demo Application](docs/otel-demo-docs.md)
|
||||
|
||||
@@ -313,6 +313,9 @@ func (p *BaseSeasonalProvider) getScore(
|
||||
series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries *v3.Series, value float64, idx int,
|
||||
) float64 {
|
||||
expectedValue := p.getExpectedValue(series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries, idx)
|
||||
if expectedValue < 0 {
|
||||
expectedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
|
||||
}
|
||||
return (value - expectedValue) / p.getStdDev(weekSeries)
|
||||
}
|
||||
|
||||
|
||||
@@ -385,7 +385,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
||||
apiHandler.RegisterThirdPartyApiRoutes(r, am)
|
||||
apiHandler.MetricExplorerRoutes(r, am)
|
||||
apiHandler.RegisterTraceFunnelsRoutes(r, am)
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/app"
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
|
||||
@@ -24,7 +26,6 @@ import (
|
||||
|
||||
func initZapLog() *zap.Logger {
|
||||
config := zap.NewProductionConfig()
|
||||
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
config.EncoderConfig.TimeKey = "timestamp"
|
||||
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
logger, _ := config.Build()
|
||||
@@ -95,12 +96,17 @@ func main() {
|
||||
|
||||
version.Info.PrettyPrint(config.Version)
|
||||
|
||||
sqlStoreFactories := signoz.NewSQLStoreProviderFactories()
|
||||
if err := sqlStoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
|
||||
zap.L().Fatal("Failed to add postgressqlstore factory", zap.Error(err))
|
||||
}
|
||||
|
||||
signoz, err := signoz.New(
|
||||
context.Background(),
|
||||
config,
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(),
|
||||
signoz.NewSQLStoreProviderFactories(),
|
||||
sqlStoreFactories,
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -157,6 +157,13 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.TraceFunnels,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ProPlan = basemodel.FeatureSet{
|
||||
@@ -279,6 +286,13 @@ var ProPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.TraceFunnels,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var EnterprisePlan = basemodel.FeatureSet{
|
||||
@@ -415,4 +429,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.TraceFunnels,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
211
ee/sqlstore/postgressqlstore/dialect.go
Normal file
211
ee/sqlstore/postgressqlstore/dialect.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package postgressqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type dialect struct {
|
||||
}
|
||||
|
||||
func (dialect *dialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB, table string, column string) error {
|
||||
columnType, err := dialect.GetColumnType(ctx, bun, table, column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// bigint for postgres and INTEGER for sqlite
|
||||
if columnType != "bigint" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the columns is integer then do this
|
||||
if _, err := bun.
|
||||
ExecContext(ctx, `ALTER TABLE `+table+` RENAME COLUMN `+column+` TO `+column+`_old`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add new timestamp column
|
||||
if _, err := bun.
|
||||
NewAddColumn().
|
||||
Table(table).
|
||||
ColumnExpr(column + " TIMESTAMP").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := bun.
|
||||
NewUpdate().
|
||||
Table(table).
|
||||
Set(column + " = to_timestamp(cast(" + column + "_old as INTEGER))").
|
||||
Where("1=1").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// drop old column
|
||||
if _, err := bun.
|
||||
NewDropColumn().
|
||||
Table(table).
|
||||
Column(column + "_old").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error {
|
||||
columnType, err := dialect.GetColumnType(ctx, bun, table, column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if columnType != "bigint" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := bun.
|
||||
ExecContext(ctx, `ALTER TABLE `+table+` RENAME COLUMN `+column+` TO `+column+`_old`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add new boolean column
|
||||
if _, err := bun.
|
||||
NewAddColumn().
|
||||
Table(table).
|
||||
ColumnExpr(column + " BOOLEAN").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy data from old column to new column, converting from int to boolean
|
||||
if _, err := bun.NewUpdate().
|
||||
Table(table).
|
||||
Set(column + " = CASE WHEN " + column + "_old = 1 THEN true ELSE false END").
|
||||
Where("1=1").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// drop old column
|
||||
if _, err := bun.NewDropColumn().Table(table).Column(column + "_old").Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) GetColumnType(ctx context.Context, bun bun.IDB, table string, column string) (string, error) {
|
||||
var columnType string
|
||||
|
||||
err := bun.NewSelect().
|
||||
ColumnExpr("data_type").
|
||||
TableExpr("information_schema.columns").
|
||||
Where("table_name = ?", table).
|
||||
Where("column_name = ?", column).
|
||||
Scan(ctx, &columnType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return columnType, nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) ColumnExists(ctx context.Context, bun bun.IDB, table string, column string) (bool, error) {
|
||||
var count int
|
||||
err := bun.NewSelect().
|
||||
ColumnExpr("COUNT(*)").
|
||||
TableExpr("information_schema.columns").
|
||||
Where("table_name = ?", table).
|
||||
Where("column_name = ?", column).
|
||||
Scan(ctx, &count)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table string, oldColumnName string, newColumnName string) (bool, error) {
|
||||
oldColumnExists, err := dialect.ColumnExists(ctx, bun, table, oldColumnName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newColumnExists, err := dialect.ColumnExists(ctx, bun, table, newColumnName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !oldColumnExists && newColumnExists {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
ExecContext(ctx, "ALTER TABLE "+table+" RENAME COLUMN "+oldColumnName+" TO "+newColumnName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) TableExists(ctx context.Context, bun bun.IDB, table interface{}) (bool, error) {
|
||||
|
||||
count := 0
|
||||
err := bun.
|
||||
NewSelect().
|
||||
ColumnExpr("count(*)").
|
||||
Table("pg_catalog.pg_tables").
|
||||
Where("tablename = ?", bun.Dialect().Tables().Get(reflect.TypeOf(table)).Name).
|
||||
Scan(ctx, &count)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error {
|
||||
exists, err := dialect.TableExists(ctx, bun, newModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
NewCreateTable().
|
||||
IfNotExists().
|
||||
Model(newModel).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cb(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
NewDropTable().
|
||||
IfExists().
|
||||
Model(oldModel).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -18,7 +18,7 @@ type provider struct {
|
||||
sqldb *sql.DB
|
||||
bundb *sqlstore.BunDB
|
||||
sqlxdb *sqlx.DB
|
||||
dialect *PGDialect
|
||||
dialect *dialect
|
||||
}
|
||||
|
||||
func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] {
|
||||
@@ -60,7 +60,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
||||
sqldb: sqldb,
|
||||
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
|
||||
sqlxdb: sqlx.NewDb(sqldb, "postgres"),
|
||||
dialect: &PGDialect{},
|
||||
dialect: new(dialect),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "0.379.0",
|
||||
"lucide-react": "0.427.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"motion": "12.4.13",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
|
||||
@@ -60,10 +60,14 @@
|
||||
"INTEGRATIONS": "SigNoz | Integrations",
|
||||
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
||||
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
|
||||
"MESSAGING_QUEUES_OVERVIEW": "SigNoz | Messaging Queues",
|
||||
"MESSAGING_QUEUES_KAFKA": "SigNoz | Messaging Queues | Kafka",
|
||||
"MESSAGING_QUEUES_KAFKA_DETAIL": "SigNoz | Messaging Queues | Kafka",
|
||||
"MESSAGING_QUEUES_CELERY_TASK": "SigNoz | Messaging Queues | Celery",
|
||||
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
||||
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
||||
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
|
||||
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
|
||||
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer"
|
||||
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
||||
"API_MONITORING": "SigNoz | API Monitoring"
|
||||
}
|
||||
|
||||
@@ -295,3 +295,7 @@ export const MetricsExplorer = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
|
||||
);
|
||||
|
||||
export const ApiMonitoring = Loadable(
|
||||
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
APIKeys,
|
||||
ApiMonitoring,
|
||||
BillingPage,
|
||||
CreateAlertChannelAlerts,
|
||||
CreateNewAlerts,
|
||||
@@ -497,6 +498,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'METRICS_EXPLORER_VIEWS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.API_MONITORING,
|
||||
exact: true,
|
||||
component: ApiMonitoring,
|
||||
key: 'API_MONITORING',
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORT_ROUTE: AppRoutes = {
|
||||
|
||||
@@ -521,7 +521,7 @@ export default function CeleryOverviewTable({
|
||||
locale={{
|
||||
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
showSorterTooltip
|
||||
onDragColumn={handleDragColumn}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
|
||||
@@ -199,12 +199,12 @@ function ExplorerCard({
|
||||
value={viewName || undefined}
|
||||
>
|
||||
{viewsData?.data.data.map((view) => (
|
||||
<Select.Option key={view.uuid} value={view.name}>
|
||||
<Select.Option key={view.id} value={view.name}>
|
||||
<MenuItemGenerator
|
||||
viewName={view.name}
|
||||
viewKey={viewKey}
|
||||
createdBy={view.createdBy}
|
||||
uuid={view.uuid}
|
||||
uuid={view.id}
|
||||
refetchAllView={refetchAllView}
|
||||
viewData={viewsData.data.data}
|
||||
sourcePage={sourcepage}
|
||||
|
||||
@@ -53,17 +53,12 @@ function MenuItemGenerator({
|
||||
({ key }: { key: string }): void => {
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(key, viewData);
|
||||
if (!currentViewDetails) return;
|
||||
const {
|
||||
query,
|
||||
name,
|
||||
uuid,
|
||||
panelType: currentPanelType,
|
||||
} = currentViewDetails;
|
||||
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
|
||||
|
||||
handleExplorerTabChange(currentPanelType, {
|
||||
query,
|
||||
name,
|
||||
uuid,
|
||||
id,
|
||||
});
|
||||
},
|
||||
[viewData, handleExplorerTabChange],
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const viewMockData: ViewProps[] = [
|
||||
{
|
||||
uuid: 'view1',
|
||||
id: 'view1',
|
||||
name: 'View 1',
|
||||
createdBy: 'User 1',
|
||||
category: 'category 1',
|
||||
@@ -17,7 +17,7 @@ export const viewMockData: ViewProps[] = [
|
||||
updatedBy: 'User 1',
|
||||
},
|
||||
{
|
||||
uuid: 'view2',
|
||||
id: 'view2',
|
||||
name: 'View 2',
|
||||
createdBy: 'User 2',
|
||||
category: 'category 2',
|
||||
|
||||
@@ -25,9 +25,9 @@ describe('MenuItemGenerator', () => {
|
||||
<MockQueryClientProvider>
|
||||
<MenuItemGenerator
|
||||
viewName={viewMockData[0].name}
|
||||
viewKey={viewMockData[0].uuid}
|
||||
viewKey={viewMockData[0].id}
|
||||
createdBy={viewMockData[0].createdBy}
|
||||
uuid={viewMockData[0].uuid}
|
||||
uuid={viewMockData[0].id}
|
||||
refetchAllView={jest.fn()}
|
||||
viewData={viewMockData}
|
||||
sourcePage={DataSource.TRACES}
|
||||
@@ -43,9 +43,9 @@ describe('MenuItemGenerator', () => {
|
||||
<MockQueryClientProvider>
|
||||
<MenuItemGenerator
|
||||
viewName={viewMockData[0].name}
|
||||
viewKey={viewMockData[0].uuid}
|
||||
viewKey={viewMockData[0].id}
|
||||
createdBy={viewMockData[0].createdBy}
|
||||
uuid={viewMockData[0].uuid}
|
||||
uuid={viewMockData[0].id}
|
||||
refetchAllView={jest.fn()}
|
||||
viewData={viewMockData}
|
||||
sourcePage={DataSource.TRACES}
|
||||
|
||||
@@ -26,7 +26,7 @@ export type GetViewDetailsUsingViewKey = (
|
||||
| {
|
||||
query: Query;
|
||||
name: string;
|
||||
uuid: string;
|
||||
id: string;
|
||||
panelType: PANEL_TYPES;
|
||||
extraData?: string;
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
|
||||
viewKey,
|
||||
data,
|
||||
) => {
|
||||
const selectedView = data?.find((view) => view.uuid === viewKey);
|
||||
const selectedView = data?.find((view) => view.id === viewKey);
|
||||
if (selectedView) {
|
||||
const { compositeQuery, name, uuid, extraData } = selectedView;
|
||||
const { compositeQuery, name, id, extraData } = selectedView;
|
||||
const query = mapQueryDataFromApi(compositeQuery);
|
||||
return { query, name, uuid, panelType: compositeQuery.panelType, extraData };
|
||||
return { query, name, id, panelType: compositeQuery.panelType, extraData };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ function CopyClipboardHOC({
|
||||
|
||||
notifications.success({
|
||||
message: notificationMessage,
|
||||
key: notificationMessage,
|
||||
});
|
||||
}
|
||||
}, [value, notifications, entityKey]);
|
||||
|
||||
@@ -63,30 +63,31 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="quick-filters">
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<Typography.Text className="text">Filters for</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</section>
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING &&
|
||||
source !== QuickFiltersSource.API_MONITORING && (
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<Typography.Text className="text">Filters for</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</section>
|
||||
|
||||
<section className="right-actions">
|
||||
<Tooltip title="Reset All">
|
||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||
</Tooltip>
|
||||
<div className="divider-filter" />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
<section className="right-actions">
|
||||
<Tooltip title="Reset All">
|
||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||
</Tooltip>
|
||||
<div className="divider-filter" />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
)}
|
||||
|
||||
<section className="filters">
|
||||
{config.map((filter) => {
|
||||
|
||||
@@ -39,4 +39,5 @@ export enum QuickFiltersSource {
|
||||
LOGS_EXPLORER = 'logs-explorer',
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './ResizeTable.styles.scss';
|
||||
|
||||
import { SyntheticEvent, useMemo } from 'react';
|
||||
import { Resizable, ResizeCallbackData } from 'react-resizable';
|
||||
|
||||
@@ -10,8 +12,8 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
|
||||
const handle = useMemo(
|
||||
() => (
|
||||
<SpanStyle
|
||||
className="react-resizable-handle"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
className="resize-handle"
|
||||
/>
|
||||
),
|
||||
[],
|
||||
@@ -19,7 +21,7 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
|
||||
|
||||
if (!width) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <th {...restProps} />;
|
||||
return <th {...restProps} className="resizable-header" />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -29,9 +31,10 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
|
||||
handle={handle}
|
||||
onResize={onResize}
|
||||
draggableOpts={enableUserSelectHack}
|
||||
minConstraints={[150, 0]}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<th {...restProps} />
|
||||
<th {...restProps} className="resizable-header" />
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
53
frontend/src/components/ResizeTable/ResizeTable.styles.scss
Normal file
53
frontend/src/components/ResizeTable/ResizeTable.styles.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
.resizable-header {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
position: relative;
|
||||
|
||||
.ant-table-column-title {
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.resize-main-table {
|
||||
.ant-table-body {
|
||||
.ant-table-tbody {
|
||||
.ant-table-row {
|
||||
.ant-table-cell {
|
||||
.ant-typography {
|
||||
white-space: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-table,
|
||||
.traces-table {
|
||||
.resize-table {
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
inset-inline-end: -5px;
|
||||
width: 10px;
|
||||
cursor: col-resize;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1px;
|
||||
height: 1.6em;
|
||||
background-color: var(--bg-slate-200);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,63 @@
|
||||
|
||||
import { Table } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import cx from 'classnames';
|
||||
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
||||
import { set } from 'lodash-es';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { debounce, set } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import ReactDragListView from 'react-drag-listview';
|
||||
import { ResizeCallbackData } from 'react-resizable';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import ResizableHeader from './ResizableHeader';
|
||||
import { DragSpanStyle } from './styles';
|
||||
import { ResizeTableProps } from './types';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ResizeTable({
|
||||
columns,
|
||||
onDragColumn,
|
||||
pagination,
|
||||
widgetId,
|
||||
shouldPersistColumnWidths = false,
|
||||
...restProps
|
||||
}: ResizeTableProps): JSX.Element {
|
||||
const [columnsData, setColumns] = useState<ColumnsType>([]);
|
||||
const { setColumnWidths, selectedDashboard } = useDashboard();
|
||||
|
||||
const columnWidths = shouldPersistColumnWidths
|
||||
? (selectedDashboard?.data?.widgets?.find(
|
||||
(widget) => widget.id === widgetId,
|
||||
) as Widgets)?.columnWidths
|
||||
: undefined;
|
||||
|
||||
const updateAllColumnWidths = useRef(
|
||||
debounce((widthsConfig: Record<string, number>) => {
|
||||
if (!widgetId || !shouldPersistColumnWidths) return;
|
||||
setColumnWidths?.((prev) => ({
|
||||
...prev,
|
||||
[widgetId]: widthsConfig,
|
||||
}));
|
||||
}, 1000),
|
||||
).current;
|
||||
|
||||
const handleResize = useCallback(
|
||||
(index: number) => (
|
||||
_e: SyntheticEvent<Element>,
|
||||
e: SyntheticEvent<Element>,
|
||||
{ size }: ResizeCallbackData,
|
||||
): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const newColumns = [...columnsData];
|
||||
newColumns[index] = {
|
||||
...newColumns[index],
|
||||
@@ -65,6 +93,7 @@ function ResizeTable({
|
||||
...restProps,
|
||||
components: { header: { cell: ResizableHeader } },
|
||||
columns: mergedColumns,
|
||||
className: cx('resize-main-table', restProps.className),
|
||||
};
|
||||
|
||||
set(
|
||||
@@ -78,9 +107,39 @@ function ResizeTable({
|
||||
|
||||
useEffect(() => {
|
||||
if (columns) {
|
||||
setColumns(columns);
|
||||
// Apply stored column widths from widget configuration
|
||||
const columnsWithStoredWidths = columns.map((col) => {
|
||||
const dataIndex = (col as RowData).dataIndex as string;
|
||||
if (dataIndex && columnWidths && columnWidths[dataIndex]) {
|
||||
return {
|
||||
...col,
|
||||
width: columnWidths[dataIndex], // Apply stored width
|
||||
};
|
||||
}
|
||||
return col;
|
||||
});
|
||||
|
||||
setColumns(columnsWithStoredWidths);
|
||||
}
|
||||
}, [columns]);
|
||||
}, [columns, columnWidths]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldPersistColumnWidths) return;
|
||||
// Collect all column widths in a single object
|
||||
const newColumnWidths: Record<string, number> = {};
|
||||
|
||||
mergedColumns.forEach((col) => {
|
||||
if (col.width && (col as RowData).dataIndex) {
|
||||
const dataIndex = (col as RowData).dataIndex as string;
|
||||
newColumnWidths[dataIndex] = col.width as number;
|
||||
}
|
||||
});
|
||||
|
||||
// Only update if there are actual widths to set
|
||||
if (Object.keys(newColumnWidths).length > 0) {
|
||||
updateAllColumnWidths(newColumnWidths);
|
||||
}
|
||||
}, [mergedColumns, updateAllColumnWidths, shouldPersistColumnWidths]);
|
||||
|
||||
return onDragColumn ? (
|
||||
<ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}>
|
||||
|
||||
@@ -8,6 +8,8 @@ export const SpanStyle = styled.span`
|
||||
width: 0.625rem;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
export const DragSpanStyle = styled.span`
|
||||
|
||||
@@ -9,6 +9,8 @@ import { TableDataSource } from './contants';
|
||||
|
||||
export interface ResizeTableProps extends TableProps<any> {
|
||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||
widgetId?: string;
|
||||
shouldPersistColumnWidths?: boolean;
|
||||
}
|
||||
export interface DynamicColumnTableProps extends TableProps<any> {
|
||||
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
||||
|
||||
@@ -25,4 +25,6 @@ export enum FeatureKeys {
|
||||
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
|
||||
AWS_INTEGRATION = 'AWS_INTEGRATION',
|
||||
ONBOARDING_V3 = 'ONBOARDING_V3',
|
||||
THIRD_PARTY_API = 'THIRD_PARTY_API',
|
||||
TRACE_FUNNELS = 'TRACE_FUNNELS',
|
||||
}
|
||||
|
||||
@@ -51,6 +51,21 @@ export const REACT_QUERY_KEY = {
|
||||
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
|
||||
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
|
||||
GET_RELATED_METRICS: 'GET_RELATED_METRICS',
|
||||
|
||||
// API Monitoring Query Keys
|
||||
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',
|
||||
GET_ENDPOINTS_LIST_BY_DOMAIN: 'GET_ENDPOINTS_LIST_BY_DOMAIN',
|
||||
GET_NESTED_ENDPOINTS_LIST: 'GET_NESTED_ENDPOINTS_LIST',
|
||||
GET_ENDPOINT_METRICS_DATA: 'GET_ENDPOINT_METRICS_DATA',
|
||||
GET_ENDPOINT_STATUS_CODE_DATA: 'GET_ENDPOINT_STATUS_CODE_DATA',
|
||||
GET_ENDPOINT_RATE_OVER_TIME_DATA: 'GET_ENDPOINT_RATE_OVER_TIME_DATA',
|
||||
GET_ENDPOINT_LATENCY_OVER_TIME_DATA: 'GET_ENDPOINT_LATENCY_OVER_TIME_DATA',
|
||||
GET_ENDPOINT_DROPDOWN_DATA: 'GET_ENDPOINT_DROPDOWN_DATA',
|
||||
GET_ENDPOINT_DEPENDENT_SERVICES_DATA: 'GET_ENDPOINT_DEPENDENT_SERVICES_DATA',
|
||||
GET_ENDPOINT_STATUS_CODE_BAR_CHARTS_DATA:
|
||||
'GET_ENDPOINT_STATUS_CODE_BAR_CHARTS_DATA',
|
||||
GET_ENDPOINT_STATUS_CODE_LATENCY_BAR_CHARTS_DATA:
|
||||
'GET_ENDPOINT_STATUS_CODE_LATENCY_BAR_CHARTS_DATA',
|
||||
GET_FUNNELS_LIST: 'GET_FUNNELS_LIST',
|
||||
GET_FUNNEL_DETAILS: 'GET_FUNNEL_DETAILS',
|
||||
} as const;
|
||||
|
||||
@@ -71,6 +71,7 @@ const ROUTES = {
|
||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||
API_MONITORING: '/api-monitoring/explorer',
|
||||
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
||||
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
||||
HOME_PAGE: '/',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
@@ -20,6 +21,13 @@ jest.mock('hooks/useNotifications', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Alert Channels Settings List page', () => {
|
||||
beforeEach(() => {
|
||||
render(<AlertChannels />);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
@@ -25,6 +26,13 @@ jest.mock('hooks/useComponentPermission', () => ({
|
||||
default: jest.fn().mockImplementation(() => [false]),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Alert Channels Settings List page (Normal User)', () => {
|
||||
beforeEach(() => {
|
||||
render(<AlertChannels />);
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Select, Spin, Table, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
EndPointsTableRowData,
|
||||
formatEndPointsDataForTable,
|
||||
getEndPointsColumnsConfig,
|
||||
getEndPointsQueryPayload,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import ErrorState from './components/ErrorState';
|
||||
import ExpandedRow from './components/ExpandedRow';
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
|
||||
function AllEndPoints({
|
||||
domainName,
|
||||
setSelectedEndPointName,
|
||||
setSelectedView,
|
||||
groupBy,
|
||||
setGroupBy,
|
||||
}: {
|
||||
domainName: string;
|
||||
setSelectedEndPointName: (name: string) => void;
|
||||
setSelectedView: (tab: VIEWS) => void;
|
||||
groupBy: IBuilderQuery['groupBy'];
|
||||
setGroupBy: (groupBy: IBuilderQuery['groupBy']) => void;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateAttribute: '',
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
});
|
||||
|
||||
const [groupByOptions, setGroupByOptions] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const groupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(key) => key.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
groupBy.push(key);
|
||||
}
|
||||
}
|
||||
setGroupBy(groupBy);
|
||||
},
|
||||
[groupByFiltersData, setGroupBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getEndPointsQueryPayload(
|
||||
groupBy,
|
||||
domainName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
),
|
||||
[groupBy, domainName, minTime, maxTime],
|
||||
);
|
||||
|
||||
// Since only one query here
|
||||
const endPointsDataQueries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_ENDPOINTS_LIST_BY_DOMAIN,
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
groupBy,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
|
||||
})),
|
||||
);
|
||||
|
||||
const endPointsDataQuery = endPointsDataQueries[0];
|
||||
const {
|
||||
data: allEndPointsData,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
refetch,
|
||||
} = endPointsDataQuery;
|
||||
|
||||
const endPointsColumnsConfig = useMemo(
|
||||
() => getEndPointsColumnsConfig(groupBy.length > 0, expandedRowKeys),
|
||||
[groupBy.length, expandedRowKeys],
|
||||
);
|
||||
|
||||
const expandedRowRender = (record: EndPointsTableRowData): JSX.Element => (
|
||||
<ExpandedRow
|
||||
domainName={domainName}
|
||||
selectedRowData={record}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
setSelectedView={setSelectedView}
|
||||
/>
|
||||
);
|
||||
|
||||
const handleGroupByRowClick = (record: EndPointsTableRowData): void => {
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys((expandedRowKeys) => [...expandedRowKeys, record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowClick = (record: EndPointsTableRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedEndPointName(record.endpointName); // this will open up the endpoint details tab
|
||||
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
|
||||
logEvent('API Monitoring: Endpoint name row clicked', {});
|
||||
} else {
|
||||
handleGroupByRowClick(record); // this will prepare the nested query payload
|
||||
}
|
||||
};
|
||||
|
||||
const formattedEndPointsData = useMemo(
|
||||
() =>
|
||||
formatEndPointsDataForTable(
|
||||
allEndPointsData?.payload?.data?.result[0]?.table?.rows,
|
||||
groupBy,
|
||||
),
|
||||
[groupBy, allEndPointsData],
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="all-endpoints-error-state-wrapper">
|
||||
<ErrorState refetch={refetch} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="all-endpoints-container">
|
||||
<div className="group-by-container">
|
||||
<div className="group-by-label"> Group by </div>
|
||||
<Select
|
||||
className="group-by-select"
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>{' '}
|
||||
</div>
|
||||
<div className="endpoints-table-container">
|
||||
<div className="endpoints-table-header">Endpoint overview</div>
|
||||
<Table
|
||||
columns={endPointsColumnsConfig}
|
||||
loading={{
|
||||
spinning: isLoading || isRefetching,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
dataSource={isLoading || isRefetching ? [] : formattedEndPointsData}
|
||||
locale={{
|
||||
emptyText:
|
||||
isLoading || isRefetching ? null : (
|
||||
<div className="no-filtered-endpoints-message-container">
|
||||
<div className="no-filtered-endpoints-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-endpoints-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: groupBy.length > 0 ? expandedRowRender : undefined,
|
||||
expandedRowKeys,
|
||||
expandIconColumnIndex: -1,
|
||||
}}
|
||||
rowClassName={(_, index): string =>
|
||||
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AllEndPoints;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
import './DomainDetails.styles.scss';
|
||||
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Typography } from 'antd';
|
||||
import { RadioChangeEvent } from 'antd/lib';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { ArrowDown, ArrowUp, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import AllEndPoints from './AllEndPoints';
|
||||
import DomainMetrics from './components/DomainMetrics';
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
import EndPointDetailsWrapper from './EndPointDetailsWrapper';
|
||||
|
||||
function DomainDetails({
|
||||
domainData,
|
||||
handleClose,
|
||||
selectedDomainIndex,
|
||||
setSelectedDomainIndex,
|
||||
domainListLength,
|
||||
domainListFilters,
|
||||
}: {
|
||||
domainData: any;
|
||||
handleClose: () => void;
|
||||
selectedDomainIndex: number;
|
||||
setSelectedDomainIndex: (index: number) => void;
|
||||
domainListLength: number;
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.ALL_ENDPOINTS);
|
||||
const [selectedEndPointName, setSelectedEndPointName] = useState<string>('');
|
||||
const [endPointsGroupBy, setEndPointsGroupBy] = useState<
|
||||
IBuilderQuery['groupBy']
|
||||
>([]);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="60%"
|
||||
title={
|
||||
<div className="domain-details-drawer-header">
|
||||
<div className="domain-details-drawer-header-title">
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{domainData.domainName}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button.Group className="domain-details-drawer-header-ctas">
|
||||
<Button
|
||||
className="domain-navigate-cta"
|
||||
onClick={(): void => {
|
||||
setSelectedDomainIndex(selectedDomainIndex - 1);
|
||||
setSelectedEndPointName('');
|
||||
setEndPointsGroupBy([]);
|
||||
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
|
||||
}}
|
||||
icon={<ArrowUp size={16} />}
|
||||
disabled={selectedDomainIndex === 0}
|
||||
title="Previous domain"
|
||||
/>
|
||||
<Button
|
||||
className="domain-navigate-cta"
|
||||
onClick={(): void => {
|
||||
setSelectedDomainIndex(selectedDomainIndex + 1);
|
||||
setSelectedEndPointName('');
|
||||
setEndPointsGroupBy([]);
|
||||
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
|
||||
}}
|
||||
icon={<ArrowDown size={16} />}
|
||||
disabled={selectedDomainIndex === domainListLength - 1}
|
||||
title="Next domain"
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!domainData}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="domain-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{domainData && (
|
||||
<>
|
||||
<DomainMetrics domainData={domainData} />
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.ALL_ENDPOINTS}
|
||||
>
|
||||
<div className="view-title">All Endpoints</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.ENDPOINT_DETAILS
|
||||
? 'tab selected_view'
|
||||
: 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.ENDPOINT_DETAILS}
|
||||
>
|
||||
<div className="view-title">Endpoint Details</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
|
||||
<AllEndPoints
|
||||
domainName={domainData.domainName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
setSelectedView={setSelectedView}
|
||||
groupBy={endPointsGroupBy}
|
||||
setGroupBy={setEndPointsGroupBy}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.ENDPOINT_DETAILS && (
|
||||
<EndPointDetailsWrapper
|
||||
domainName={domainData.domainName}
|
||||
endPointName={selectedEndPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
domainListFilters={domainListFilters}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default DomainDetails;
|
||||
@@ -0,0 +1,199 @@
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import {
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
|
||||
extractPortAndEndpoint,
|
||||
getEndPointDetailsQueryPayload,
|
||||
getLatencyOverTimeWidgetData,
|
||||
getRateOverTimeWidgetData,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import DependentServices from './components/DependentServices';
|
||||
import EndPointMetrics from './components/EndPointMetrics';
|
||||
import EndPointsDropDown from './components/EndPointsDropDown';
|
||||
import MetricOverTimeGraph from './components/MetricOverTimeGraph';
|
||||
import StatusCodeBarCharts from './components/StatusCodeBarCharts';
|
||||
import StatusCodeTable from './components/StatusCodeTable';
|
||||
|
||||
function EndPointDetails({
|
||||
domainName,
|
||||
endPointName,
|
||||
setSelectedEndPointName,
|
||||
domainListFilters,
|
||||
}: {
|
||||
domainName: string;
|
||||
endPointName: string;
|
||||
setSelectedEndPointName: (value: string) => void;
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
|
||||
op: 'AND',
|
||||
items: [],
|
||||
});
|
||||
|
||||
// Manually update the query to include the filters
|
||||
// Because using the hook is causing the global domain
|
||||
// query to be updated and causing main domain list to
|
||||
// refetch with the filters of endpoints
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
filters,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[filters, currentQuery],
|
||||
);
|
||||
|
||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const isServicesFilterApplied = useMemo(
|
||||
() => filters.items.some((item) => item.key?.key === 'service.name'),
|
||||
[filters],
|
||||
);
|
||||
|
||||
const endPointDetailsQueryPayload = useMemo(
|
||||
() =>
|
||||
getEndPointDetailsQueryPayload(
|
||||
domainName,
|
||||
endPointName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
filters,
|
||||
),
|
||||
[domainName, endPointName, filters, minTime, maxTime],
|
||||
);
|
||||
|
||||
const endPointDetailsDataQueries = useQueries(
|
||||
endPointDetailsQueryPayload.map((payload, index) => ({
|
||||
queryKey: [
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY[index],
|
||||
payload,
|
||||
filters.items,
|
||||
ENTITY_VERSION_V4,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
})),
|
||||
);
|
||||
|
||||
const [
|
||||
endPointMetricsDataQuery,
|
||||
endPointStatusCodeDataQuery,
|
||||
endPointDropDownDataQuery,
|
||||
endPointDependentServicesDataQuery,
|
||||
endPointStatusCodeBarChartsDataQuery,
|
||||
endPointStatusCodeLatencyBarChartsDataQuery,
|
||||
] = useMemo(
|
||||
() => [
|
||||
endPointDetailsDataQueries[0],
|
||||
endPointDetailsDataQueries[1],
|
||||
endPointDetailsDataQueries[2],
|
||||
endPointDetailsDataQueries[3],
|
||||
endPointDetailsDataQueries[4],
|
||||
endPointDetailsDataQueries[5],
|
||||
],
|
||||
[endPointDetailsDataQueries],
|
||||
);
|
||||
|
||||
const { endpoint, port } = useMemo(
|
||||
() => extractPortAndEndpoint(endPointName),
|
||||
[endPointName],
|
||||
);
|
||||
|
||||
const [rateOverTimeWidget, latencyOverTimeWidget] = useMemo(
|
||||
() => [
|
||||
getRateOverTimeWidgetData(domainName, endPointName, {
|
||||
items: [...domainListFilters.items, ...filters.items],
|
||||
op: filters.op,
|
||||
}),
|
||||
getLatencyOverTimeWidgetData(domainName, endPointName, {
|
||||
items: [...domainListFilters.items, ...filters.items],
|
||||
op: filters.op,
|
||||
}),
|
||||
],
|
||||
[domainName, endPointName, filters, domainListFilters],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="endpoint-details-container">
|
||||
<div className="endpoint-details-filters-container">
|
||||
<div className="endpoint-details-filters-container-dropdown">
|
||||
<EndPointsDropDown
|
||||
selectedEndPointName={endPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
endPointDropDownDataQuery={endPointDropDownDataQuery}
|
||||
parentContainerDiv=".endpoint-details-filters-container"
|
||||
dropdownStyle={{ width: 'calc(100% - 36px)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="endpoint-details-filters-container-search">
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={(searchFilters): void => {
|
||||
setFilters(searchFilters);
|
||||
}}
|
||||
placeholder="Search for filters..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="endpoint-meta-data">
|
||||
<div className="endpoint-meta-data-pill">
|
||||
<div className="endpoint-meta-data-label">Endpoint</div>
|
||||
<div className="endpoint-meta-data-value">{endpoint || '-'}</div>
|
||||
</div>
|
||||
<div className="endpoint-meta-data-pill">
|
||||
<div className="endpoint-meta-data-label">Port</div>
|
||||
<div className="endpoint-meta-data-value">{port || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<EndPointMetrics endPointMetricsDataQuery={endPointMetricsDataQuery} />
|
||||
{!isServicesFilterApplied && (
|
||||
<DependentServices
|
||||
dependentServicesQuery={endPointDependentServicesDataQuery}
|
||||
/>
|
||||
)}
|
||||
<StatusCodeBarCharts
|
||||
endPointStatusCodeBarChartsDataQuery={endPointStatusCodeBarChartsDataQuery}
|
||||
endPointStatusCodeLatencyBarChartsDataQuery={
|
||||
endPointStatusCodeLatencyBarChartsDataQuery
|
||||
}
|
||||
domainName={domainName}
|
||||
endPointName={endPointName}
|
||||
domainListFilters={domainListFilters}
|
||||
filters={filters}
|
||||
/>
|
||||
<StatusCodeTable endPointStatusCodeDataQuery={endPointStatusCodeDataQuery} />
|
||||
<MetricOverTimeGraph widget={rateOverTimeWidget} />
|
||||
<MetricOverTimeGraph widget={latencyOverTimeWidget} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointDetails;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { getEndPointZeroStateQueryPayload } from 'container/ApiMonitoring/utils';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import EndPointDetailsZeroState from './components/EndPointDetailsZeroState';
|
||||
import EndPointDetails from './EndPointDetails';
|
||||
|
||||
function EndPointDetailsWrapper({
|
||||
domainName,
|
||||
endPointName,
|
||||
setSelectedEndPointName,
|
||||
domainListFilters,
|
||||
}: {
|
||||
domainName: string;
|
||||
endPointName: string;
|
||||
setSelectedEndPointName: (value: string) => void;
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const endPointZeroStateQueryPayload = useMemo(
|
||||
() =>
|
||||
getEndPointZeroStateQueryPayload(
|
||||
domainName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
),
|
||||
[domainName, minTime, maxTime],
|
||||
);
|
||||
|
||||
const endPointZeroStateDataQueries = useQueries(
|
||||
endPointZeroStateQueryPayload.map((payload) => ({
|
||||
queryKey: [
|
||||
// Since only one query here
|
||||
REACT_QUERY_KEY.GET_ENDPOINT_DROPDOWN_DATA,
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
})),
|
||||
);
|
||||
|
||||
const [endPointZeroStateDataQuery] = useMemo(
|
||||
() => [endPointZeroStateDataQueries[0]],
|
||||
[endPointZeroStateDataQueries],
|
||||
);
|
||||
|
||||
if (endPointName === '') {
|
||||
return (
|
||||
<EndPointDetailsZeroState
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
endPointDropDownDataQuery={endPointZeroStateDataQuery}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EndPointDetails
|
||||
domainName={domainName}
|
||||
endPointName={endPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
domainListFilters={domainListFilters}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointDetailsWrapper;
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Typography } from 'antd';
|
||||
import Skeleton from 'antd/lib/skeleton';
|
||||
import { getFormattedDependentServicesData } from 'container/ApiMonitoring/utils';
|
||||
import { UnfoldVertical } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
|
||||
interface DependentServicesProps {
|
||||
dependentServicesQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
}
|
||||
|
||||
function DependentServices({
|
||||
dependentServicesQuery,
|
||||
}: DependentServicesProps): JSX.Element {
|
||||
const {
|
||||
data,
|
||||
refetch,
|
||||
isError,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
} = dependentServicesQuery;
|
||||
|
||||
const [currentRenderCount, setCurrentRenderCount] = useState(0);
|
||||
|
||||
const dependentServicesData = useMemo(() => {
|
||||
const formattedDependentServicesData = getFormattedDependentServicesData(
|
||||
data?.payload?.data?.result[0].table.rows,
|
||||
);
|
||||
setCurrentRenderCount(Math.min(formattedDependentServicesData.length, 5));
|
||||
return formattedDependentServicesData;
|
||||
}, [data]);
|
||||
|
||||
const renderItems = useMemo(
|
||||
() => dependentServicesData.slice(0, currentRenderCount),
|
||||
[currentRenderCount, dependentServicesData],
|
||||
);
|
||||
|
||||
if (isLoading || isRefetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <ErrorState refetch={refetch} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="top-services-content">
|
||||
<div className="top-services-title">
|
||||
<span className="title-wrapper">Dependent Services</span>
|
||||
</div>
|
||||
<div className="dependent-services-container">
|
||||
{renderItems.length === 0 ? (
|
||||
<div className="no-dependent-services-message-container">
|
||||
<div className="no-dependent-services-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-dependent-services-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
renderItems.map((item) => (
|
||||
<div className="top-services-item" key={item.key}>
|
||||
<div className="top-services-item-progress">
|
||||
<div className="top-services-item-key">{item.serviceName}</div>
|
||||
<div className="top-services-item-count">{item.count}</div>
|
||||
<div
|
||||
className="top-services-item-progress-bar"
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="top-services-item-percentage">
|
||||
{item.percentage.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{currentRenderCount < dependentServicesData.length && (
|
||||
<div
|
||||
className="top-services-load-more"
|
||||
onClick={(): void => setCurrentRenderCount(dependentServicesData.length)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
setCurrentRenderCount(dependentServicesData.length);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<UnfoldVertical size={14} />
|
||||
Show more...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DependentServices;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Tooltip, Typography } from 'antd';
|
||||
import { getLastUsedRelativeTime } from 'container/ApiMonitoring/utils';
|
||||
|
||||
function DomainMetrics({ domainData }: { domainData: any }): JSX.Element {
|
||||
return (
|
||||
<div className="domain-detail-drawer__endpoint">
|
||||
<div className="domain-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
EXTERNAL API
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
AVERAGE LATENCY
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
ERROR RATE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
LAST USED
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
<Tooltip title={domainData.endpointCount}>
|
||||
<span className="round-metric-tag">{domainData.endpointCount}</span>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
<Tooltip title={domainData.latency}>
|
||||
<span className="round-metric-tag">
|
||||
{(domainData.latency / 1000).toFixed(3)}s
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="domain-details-metadata-value error-rate">
|
||||
<Tooltip title={domainData.errorRate}>
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number((domainData.errorRate * 100).toFixed(1))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
(domainData.errorRate * 100).toFixed(1),
|
||||
);
|
||||
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
<Tooltip title={domainData.lastUsed}>
|
||||
{getLastUsedRelativeTime(domainData.lastUsed)}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DomainMetrics;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
import EndPointsDropDown from './EndPointsDropDown';
|
||||
|
||||
function EndPointDetailsZeroState({
|
||||
setSelectedEndPointName,
|
||||
endPointDropDownDataQuery,
|
||||
}: {
|
||||
setSelectedEndPointName: (endPointName: string) => void;
|
||||
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>>;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="end-point-details-zero-state-wrapper">
|
||||
<div className="end-point-details-zero-state-content">
|
||||
<img
|
||||
src="/Icons/no-data.svg"
|
||||
alt="no-data"
|
||||
width={32}
|
||||
height={32}
|
||||
className="end-point-details-zero-state-icon"
|
||||
/>
|
||||
<div className="end-point-details-zero-state-content-wrapper">
|
||||
<div className="end-point-details-zero-state-text-content">
|
||||
<div className="title">No endpoint selected yet</div>
|
||||
<div className="description">Select an endpoint to see the details</div>
|
||||
</div>
|
||||
<EndPointsDropDown
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
endPointDropDownDataQuery={endPointDropDownDataQuery}
|
||||
parentContainerDiv=".end-point-details-zero-state-wrapper"
|
||||
dropdownStyle={{ width: '60%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointDetailsZeroState;
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { getFormattedEndPointMetricsData } from 'container/ApiMonitoring/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
|
||||
function EndPointMetrics({
|
||||
endPointMetricsDataQuery,
|
||||
}: {
|
||||
endPointMetricsDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
data,
|
||||
refetch,
|
||||
} = endPointMetricsDataQuery;
|
||||
|
||||
const metricsData = useMemo(() => {
|
||||
if (isLoading || isRefetching || isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getFormattedEndPointMetricsData(
|
||||
data?.payload?.data?.result[0].table.rows,
|
||||
);
|
||||
}, [data?.payload?.data?.result, isLoading, isRefetching, isError]);
|
||||
|
||||
if (isError) {
|
||||
return <ErrorState refetch={refetch} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="domain-detail-drawer__endpoint">
|
||||
<div className="domain-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
Rate
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
AVERAGE LATENCY
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
ERROR RATE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
LAST USED
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={metricsData?.rate}>
|
||||
<span className="round-metric-tag">{metricsData?.rate} ops/sec</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={metricsData?.latency}>
|
||||
<span className="round-metric-tag">{metricsData?.latency}ms</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="domain-details-metadata-value error-rate">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={metricsData?.errorRate}>
|
||||
<Progress
|
||||
percent={Number((metricsData?.errorRate ?? 0 * 100).toFixed(1))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
(metricsData?.errorRate ?? 0 * 100).toFixed(1),
|
||||
);
|
||||
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={metricsData?.lastUsed}>{metricsData?.lastUsed}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointMetrics;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Select } from 'antd';
|
||||
import { getFormattedEndPointDropDownData } from 'container/ApiMonitoring/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
interface EndPointsDropDownProps {
|
||||
selectedEndPointName?: string;
|
||||
setSelectedEndPointName: (value: string) => void;
|
||||
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
parentContainerDiv?: string;
|
||||
dropdownStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
selectedEndPointName: '',
|
||||
parentContainerDiv: '',
|
||||
dropdownStyle: {},
|
||||
};
|
||||
|
||||
function EndPointsDropDown({
|
||||
selectedEndPointName,
|
||||
setSelectedEndPointName,
|
||||
endPointDropDownDataQuery,
|
||||
parentContainerDiv,
|
||||
dropdownStyle,
|
||||
}: EndPointsDropDownProps): JSX.Element {
|
||||
const { data, isLoading, isFetching } = endPointDropDownDataQuery;
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
setSelectedEndPointName(value);
|
||||
};
|
||||
|
||||
const formattedData = useMemo(
|
||||
() =>
|
||||
getFormattedEndPointDropDownData(data?.payload.data.result[0].table.rows),
|
||||
[data?.payload.data.result],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={selectedEndPointName || undefined}
|
||||
placeholder="Select endpoint"
|
||||
loading={isLoading || isFetching}
|
||||
style={{ width: '100%' }}
|
||||
onChange={handleChange}
|
||||
options={formattedData}
|
||||
getPopupContainer={
|
||||
parentContainerDiv
|
||||
? (): HTMLElement =>
|
||||
document.querySelector(parentContainerDiv) as HTMLElement
|
||||
: (triggerNode): HTMLElement => triggerNode.parentNode as HTMLElement
|
||||
}
|
||||
dropdownStyle={dropdownStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
EndPointsDropDown.defaultProps = defaultProps;
|
||||
|
||||
export default EndPointsDropDown;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import { RotateCw } from 'lucide-react';
|
||||
|
||||
function ErrorState({ refetch }: { refetch: () => void }): JSX.Element {
|
||||
return (
|
||||
<div className="error-state-container">
|
||||
<div className="error-state-content-wrapper">
|
||||
<div className="error-state-content">
|
||||
<div className="icon">
|
||||
<img src="/Icons/awwSnap.svg" alt="awwSnap" width={32} height={32} />
|
||||
</div>
|
||||
<div className="error-state-text">
|
||||
<Typography.Text>Uh-oh :/ We ran into an error.</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
Please refresh this panel.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="refresh-cta"
|
||||
onClick={(): void => refetch()}
|
||||
icon={<RotateCw size={16} />}
|
||||
>
|
||||
Refresh this panel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorState;
|
||||
@@ -0,0 +1,129 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Table } from 'antd';
|
||||
import { ColumnType } from 'antd/lib/table';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
createFiltersForSelectedRowData,
|
||||
EndPointsTableRowData,
|
||||
formatEndPointsDataForTable,
|
||||
getEndPointsColumnsConfig,
|
||||
getEndPointsQueryPayload,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { VIEW_TYPES, VIEWS } from '../constants';
|
||||
|
||||
function ExpandedRow({
|
||||
domainName,
|
||||
selectedRowData,
|
||||
setSelectedEndPointName,
|
||||
setSelectedView,
|
||||
}: {
|
||||
domainName: string;
|
||||
selectedRowData: EndPointsTableRowData;
|
||||
setSelectedEndPointName: (name: string) => void;
|
||||
setSelectedView: (view: VIEWS) => void;
|
||||
}): JSX.Element {
|
||||
const nestedColumns = useMemo(() => getEndPointsColumnsConfig(false, []), []);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const groupedByRowDataQueryPayload = useMemo(() => {
|
||||
if (!selectedRowData) return null;
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||
|
||||
const baseQueryPayload = getEndPointsQueryPayload(
|
||||
[],
|
||||
domainName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
);
|
||||
|
||||
return baseQueryPayload.map((currentQueryPayload) => ({
|
||||
...currentQueryPayload,
|
||||
query: {
|
||||
...currentQueryPayload.query,
|
||||
builder: {
|
||||
...currentQueryPayload.query.builder,
|
||||
queryData: currentQueryPayload.query.builder.queryData.map(
|
||||
(queryData) => ({
|
||||
...queryData,
|
||||
filters: {
|
||||
items: [...(queryData.filters?.items || []), ...filters.items],
|
||||
op: 'AND',
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, [domainName, minTime, maxTime, selectedRowData]);
|
||||
|
||||
const groupedByRowQueries = useQueries(
|
||||
groupedByRowDataQueryPayload
|
||||
? groupedByRowDataQueryPayload.map((payload) => ({
|
||||
queryKey: [
|
||||
`${REACT_QUERY_KEY.GET_NESTED_ENDPOINTS_LIST}-${domainName}-${selectedRowData?.key}`,
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
selectedRowData?.key,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload && !!selectedRowData,
|
||||
}))
|
||||
: [],
|
||||
);
|
||||
|
||||
const groupedByRowQuery = groupedByRowQueries[0];
|
||||
return (
|
||||
<div className="expanded-table-container">
|
||||
{groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<EndPointsTableRowData>[]}
|
||||
dataSource={
|
||||
groupedByRowQuery?.data
|
||||
? formatEndPointsDataForTable(
|
||||
groupedByRowQuery.data?.payload.data.result[0].table?.rows,
|
||||
[],
|
||||
)
|
||||
: []
|
||||
}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
showHeader={false}
|
||||
loading={{
|
||||
spinning: groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedEndPointName(record.endpointName);
|
||||
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
|
||||
logEvent('API Monitoring: Endpoint name row clicked', {});
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpandedRow;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Card } from 'antd';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
function MetricOverTimeGraph({ widget }: { widget: Widgets }): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Card bordered className="endpoint-details-card">
|
||||
<div className="graph-container">
|
||||
<GridCard
|
||||
widget={widget}
|
||||
isQueryEnabled
|
||||
onDragSelect={(): void => {}}
|
||||
customOnDragSelect={(): void => {}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricOverTimeGraph;
|
||||
@@ -0,0 +1,257 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Card, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
getCustomFiltersForBarChart,
|
||||
getFormattedEndPointStatusCodeChartData,
|
||||
getStatusCodeBarChartWidgetData,
|
||||
statusCodeWidgetInfo,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Options } from 'uplot';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
|
||||
function StatusCodeBarCharts({
|
||||
endPointStatusCodeBarChartsDataQuery,
|
||||
endPointStatusCodeLatencyBarChartsDataQuery,
|
||||
domainName,
|
||||
endPointName,
|
||||
domainListFilters,
|
||||
filters,
|
||||
}: {
|
||||
endPointStatusCodeBarChartsDataQuery: UseQueryResult<
|
||||
SuccessResponse<any>,
|
||||
unknown
|
||||
>;
|
||||
endPointStatusCodeLatencyBarChartsDataQuery: UseQueryResult<
|
||||
SuccessResponse<any>,
|
||||
unknown
|
||||
>;
|
||||
domainName: string;
|
||||
endPointName: string;
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
filters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
// 0 : Status Code Count
|
||||
// 1 : Status Code Latency
|
||||
const [currentWidgetInfoIndex, setCurrentWidgetInfoIndex] = useState(0);
|
||||
|
||||
const {
|
||||
data: endPointStatusCodeBarChartsData,
|
||||
} = endPointStatusCodeBarChartsDataQuery;
|
||||
|
||||
const {
|
||||
data: endPointStatusCodeLatencyBarChartsData,
|
||||
} = endPointStatusCodeLatencyBarChartsDataQuery;
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
const formattedEndPointStatusCodeBarChartsDataPayload = useMemo(
|
||||
() =>
|
||||
getFormattedEndPointStatusCodeChartData(
|
||||
endPointStatusCodeBarChartsData?.payload,
|
||||
'sum',
|
||||
),
|
||||
[endPointStatusCodeBarChartsData?.payload],
|
||||
);
|
||||
|
||||
const formattedEndPointStatusCodeLatencyBarChartsDataPayload = useMemo(
|
||||
() =>
|
||||
getFormattedEndPointStatusCodeChartData(
|
||||
endPointStatusCodeLatencyBarChartsData?.payload,
|
||||
'average',
|
||||
),
|
||||
[endPointStatusCodeLatencyBarChartsData?.payload],
|
||||
);
|
||||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getUPlotChartData(
|
||||
currentWidgetInfoIndex === 0
|
||||
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
),
|
||||
[
|
||||
currentWidgetInfoIndex,
|
||||
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
],
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const graphClick = useGraphClickToShowButton({
|
||||
graphRef,
|
||||
isButtonEnabled: true,
|
||||
buttonClassName: 'view-onclick-show-button',
|
||||
});
|
||||
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
|
||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
||||
isDarkMode,
|
||||
drawStyle: 'bars',
|
||||
colorMapping: {
|
||||
'200-299': Color.BG_FOREST_500,
|
||||
'300-399': Color.BG_AMBER_400,
|
||||
'400-499': Color.BG_CHERRY_500,
|
||||
'500-599': Color.BG_ROBIN_500,
|
||||
Other: Color.BG_SIENNA_500,
|
||||
},
|
||||
});
|
||||
|
||||
const widget = useMemo<Widgets>(
|
||||
() =>
|
||||
getStatusCodeBarChartWidgetData(domainName, endPointName, {
|
||||
items: [...domainListFilters.items, ...filters.items],
|
||||
op: filters.op,
|
||||
}),
|
||||
[domainName, endPointName, domainListFilters, filters],
|
||||
);
|
||||
|
||||
const graphClickHandler = useCallback(
|
||||
(
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
): void => {
|
||||
const customFilters = getCustomFiltersForBarChart(metric);
|
||||
handleGraphClick({
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
metric,
|
||||
queryData,
|
||||
widget,
|
||||
navigateToExplorerPages,
|
||||
navigateToExplorer,
|
||||
notifications,
|
||||
graphClick,
|
||||
customFilters,
|
||||
});
|
||||
},
|
||||
[
|
||||
widget,
|
||||
navigateToExplorerPages,
|
||||
navigateToExplorer,
|
||||
notifications,
|
||||
graphClick,
|
||||
],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse:
|
||||
currentWidgetInfoIndex === 0
|
||||
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: Math.floor(minTime / 1e9),
|
||||
maxTimeScale: Math.floor(maxTime / 1e9),
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
onClickHandler: graphClickHandler,
|
||||
customSeries: getCustomSeries,
|
||||
}),
|
||||
[
|
||||
minTime,
|
||||
maxTime,
|
||||
currentWidgetInfoIndex,
|
||||
dimensions,
|
||||
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
isDarkMode,
|
||||
graphClickHandler,
|
||||
getCustomSeries,
|
||||
],
|
||||
);
|
||||
|
||||
const renderCardContent = useCallback(
|
||||
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
|
||||
if (query.isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
if (query.error) {
|
||||
return <ErrorState refetch={query.refetch} />;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cx('chart-container', {
|
||||
'no-data-container':
|
||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||
})}
|
||||
>
|
||||
<Uplot options={options as Options} data={chartData} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[options, chartData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card bordered className="endpoint-details-card">
|
||||
<div className="header">
|
||||
<Typography.Text>Call response status</Typography.Text>
|
||||
<Button.Group className="views-tabs">
|
||||
<Button
|
||||
value={0}
|
||||
className={currentWidgetInfoIndex === 0 ? 'selected_view tab' : 'tab'}
|
||||
disabled={false}
|
||||
onClick={(): void => setCurrentWidgetInfoIndex(0)}
|
||||
>
|
||||
Number of calls
|
||||
</Button>
|
||||
<Button
|
||||
value={1}
|
||||
className={currentWidgetInfoIndex === 1 ? 'selected_view tab' : 'tab'}
|
||||
onClick={(): void => setCurrentWidgetInfoIndex(1)}
|
||||
>
|
||||
Latency
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</div>
|
||||
<div className="graph-container" ref={graphRef}>
|
||||
{renderCardContent(endPointStatusCodeBarChartsDataQuery)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default StatusCodeBarCharts;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Table, Typography } from 'antd';
|
||||
import {
|
||||
endPointStatusCodeColumns,
|
||||
getFormattedEndPointStatusCodeData,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
|
||||
function StatusCodeTable({
|
||||
endPointStatusCodeDataQuery,
|
||||
}: {
|
||||
endPointStatusCodeDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
data,
|
||||
refetch,
|
||||
} = endPointStatusCodeDataQuery;
|
||||
|
||||
const statusCodeData = useMemo(() => {
|
||||
if (isLoading || isRefetching || isError) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getFormattedEndPointStatusCodeData(
|
||||
data?.payload?.data?.result[0].table.rows,
|
||||
);
|
||||
}, [data?.payload?.data?.result, isLoading, isRefetching, isError]);
|
||||
|
||||
if (isError) {
|
||||
return <ErrorState refetch={refetch} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="status-code-table-container">
|
||||
<Table
|
||||
loading={isLoading || isRefetching}
|
||||
dataSource={statusCodeData || []}
|
||||
columns={endPointStatusCodeColumns}
|
||||
pagination={false}
|
||||
rowClassName={(_, index): string =>
|
||||
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||
}
|
||||
locale={{
|
||||
emptyText:
|
||||
isLoading || isRefetching ? null : (
|
||||
<div className="no-status-code-data-message-container">
|
||||
<div className="no-status-code-data-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-status-code-data-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusCodeTable;
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum VIEWS {
|
||||
ALL_ENDPOINTS = 'all_endpoints',
|
||||
ENDPOINT_DETAILS = 'endpoint_details',
|
||||
}
|
||||
|
||||
export const VIEW_TYPES = {
|
||||
ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS,
|
||||
ENDPOINT_DETAILS: VIEWS.ENDPOINT_DETAILS,
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import '../Explorer.styles.scss';
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Table, Typography } from 'antd';
|
||||
import axios from 'api';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { HandleChangeQueryData } from 'types/common/operations.types';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
columnsConfig,
|
||||
formatDataForTable,
|
||||
hardcodedAttributeKeys,
|
||||
} from '../../utils';
|
||||
import DomainDetails from './DomainDetails/DomainDetails';
|
||||
|
||||
function DomainList({
|
||||
query,
|
||||
showIP,
|
||||
handleChangeQueryData,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
showIP: boolean;
|
||||
handleChangeQueryData: HandleChangeQueryData;
|
||||
}): JSX.Element {
|
||||
const [selectedDomainIndex, setSelectedDomainIndex] = useState<number>(-1);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const fetchApiOverview = async (): Promise<
|
||||
SuccessResponse<any> | ErrorResponse
|
||||
> => {
|
||||
const requestBody = {
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
show_ip: showIP,
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: query?.filters.items,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'/third-party-apis/overview/list',
|
||||
requestBody,
|
||||
);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const { data, isLoading, isFetching } = useQuery(
|
||||
[REACT_QUERY_KEY.GET_DOMAINS_LIST, minTime, maxTime, query, showIP],
|
||||
fetchApiOverview,
|
||||
);
|
||||
|
||||
const formattedDataForTable = useMemo(
|
||||
() => formatDataForTable(data?.payload?.data?.result[0]?.table?.rows),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={cx('api-module-right-section')}>
|
||||
<div className={cx('api-monitoring-list-header')}>
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={(searchFilters): void =>
|
||||
handleChangeQueryData('filters', searchFilters)
|
||||
}
|
||||
placeholder="Search filters..."
|
||||
hardcodedAttributeKeys={hardcodedAttributeKeys}
|
||||
/>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={false}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
className={cx('api-monitoring-domain-list-table')}
|
||||
dataSource={isFetching || isLoading ? [] : formattedDataForTable}
|
||||
columns={columnsConfig}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText:
|
||||
isFetching || isLoading ? null : (
|
||||
<div className="no-filtered-domains-message-container">
|
||||
<div className="no-filtered-domains-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-domains-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onRow={(record, index): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
if (index !== undefined) {
|
||||
const dataIndex = formattedDataForTable.findIndex(
|
||||
(item) => item.key === record.key,
|
||||
);
|
||||
setSelectedDomainIndex(dataIndex);
|
||||
logEvent('API Monitoring: Domain name row clicked', {});
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
rowClassName={(_, index): string =>
|
||||
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||
}
|
||||
/>
|
||||
{selectedDomainIndex !== -1 && (
|
||||
<DomainDetails
|
||||
domainData={formattedDataForTable[selectedDomainIndex]}
|
||||
selectedDomainIndex={selectedDomainIndex}
|
||||
setSelectedDomainIndex={setSelectedDomainIndex}
|
||||
domainListLength={formattedDataForTable.length}
|
||||
handleClose={(): void => {
|
||||
setSelectedDomainIndex(-1);
|
||||
}}
|
||||
domainListFilters={query?.filters}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default DomainList;
|
||||
@@ -0,0 +1,219 @@
|
||||
.api-monitoring-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
.api-quick-filter-left-section {
|
||||
width: 0%;
|
||||
flex-shrink: 0;
|
||||
|
||||
.api-quick-filters-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.api-monitoring-list-header {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.query-builder-search-v2 {
|
||||
min-width: 80%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.api-monitoring-domain-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
/* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
background: none;
|
||||
|
||||
&::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.domain-list-name-col-header) {
|
||||
background: var(--bg-ink-300);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--bg-vanilla-100);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.domain-list-name-col-value) {
|
||||
background: var(--bg-ink-300);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.round-metric-tag {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: fit-content;
|
||||
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-slate-500);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:first-child {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(2) {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(n + 3) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.column-header-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-thead
|
||||
> tr
|
||||
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-empty-normal {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.table-row-light {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.table-row-dark {
|
||||
background: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
.error-rate {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
.api-quick-filter-left-section {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-filtered-domains-message-container {
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.no-filtered-domains-message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
width: fit-content;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.no-filtered-domains-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.api-monitoring-domain-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.domain-list-name-col-header) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.domain-list-name-col-value) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.table-row-light {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.table-row-dark {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.round-metric-tag {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx
Normal file
101
frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import './Explorer.styles.scss';
|
||||
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { ApiMonitoringQuickFiltersConfig } from '../utils';
|
||||
import DomainList from './Domains/DomainList';
|
||||
|
||||
function Explorer(): JSX.Element {
|
||||
const [showIP, setShowIP] = useState<boolean>(true);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('API Monitoring: Landing page visited', {});
|
||||
}, []);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[currentQuery],
|
||||
);
|
||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||
<section className="api-quick-filter-left-section">
|
||||
<div className="api-quick-filters-header">
|
||||
<FilterOutlined />
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="api-quick-filters-header">
|
||||
<Typography.Text>Show IP addresses</Typography.Text>
|
||||
<Switch
|
||||
size="small"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
checked={showIP}
|
||||
onClick={(): void => {
|
||||
setShowIP((showIP): boolean => {
|
||||
logEvent('API Monitoring: Show IP addresses clicked', {
|
||||
showIP: !showIP,
|
||||
});
|
||||
return !showIP;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
config={ApiMonitoringQuickFiltersConfig}
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
onFilterChange={(query: Query): void =>
|
||||
handleChangeQueryData('filters', query.builder.queryData[0].filters)
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<DomainList
|
||||
query={query}
|
||||
showIP={showIP}
|
||||
handleChangeQueryData={handleChangeQueryData}
|
||||
/>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default Explorer;
|
||||
2283
frontend/src/container/ApiMonitoring/utils.tsx
Normal file
2283
frontend/src/container/ApiMonitoring/utils.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -337,6 +337,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
routeKey === 'LOGS_PIPELINES' ||
|
||||
routeKey === 'LOGS_SAVE_VIEWS';
|
||||
|
||||
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
|
||||
|
||||
const isTracesView = (): boolean =>
|
||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||
|
||||
@@ -658,7 +660,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isAlertOverview() ||
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring()
|
||||
isInfraMonitoring() ||
|
||||
isApiMonitoringView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ function ExplorerOptions({
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
|
||||
|
||||
const extraData = viewsData?.data?.data?.find((view) => view.uuid === viewKey)
|
||||
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
|
||||
?.extraData;
|
||||
|
||||
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
|
||||
@@ -357,17 +357,12 @@ function ExplorerOptions({
|
||||
viewsData?.data?.data,
|
||||
);
|
||||
if (!currentViewDetails) return;
|
||||
const {
|
||||
query,
|
||||
name,
|
||||
uuid,
|
||||
panelType: currentPanelType,
|
||||
} = currentViewDetails;
|
||||
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
|
||||
|
||||
handleExplorerTabChange(currentPanelType, {
|
||||
query,
|
||||
name,
|
||||
uuid,
|
||||
id,
|
||||
});
|
||||
},
|
||||
[viewsData, handleExplorerTabChange],
|
||||
@@ -694,7 +689,7 @@ function ExplorerOptions({
|
||||
bgColor = extraData.color;
|
||||
}
|
||||
return (
|
||||
<Select.Option key={view.uuid} value={view.name}>
|
||||
<Select.Option key={view.id} value={view.name}>
|
||||
<div className="render-options">
|
||||
<span
|
||||
className="dot"
|
||||
|
||||
@@ -49,6 +49,7 @@ function FullView({
|
||||
isDependedDataLoaded = false,
|
||||
onToggleModelHandler,
|
||||
onClickHandler,
|
||||
customOnDragSelect,
|
||||
setCurrentGraphRef,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -252,7 +253,7 @@ function FullView({
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphVisibility={setGraphsVisibilityStates}
|
||||
graphVisibility={graphsVisibilityStates}
|
||||
onDragSelect={onDragSelect}
|
||||
onDragSelect={customOnDragSelect ?? onDragSelect}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
searchTerm={searchTerm}
|
||||
onClickHandler={onClickHandler}
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface FullViewProps {
|
||||
widget: Widgets;
|
||||
fullViewOptions?: boolean;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
customOnDragSelect?: (start: number, end: number) => void;
|
||||
name: string;
|
||||
tableProcessedDataRef: MutableRefObject<RowData[]>;
|
||||
version?: string;
|
||||
|
||||
@@ -50,6 +50,7 @@ function WidgetGraphComponent({
|
||||
setRequestData,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
customOnDragSelect,
|
||||
customTooltipElement,
|
||||
openTracesButton,
|
||||
onOpenTraceBtnClick,
|
||||
@@ -327,6 +328,7 @@ function WidgetGraphComponent({
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
onClickHandler={onClickHandler ?? graphClickHandler}
|
||||
customOnDragSelect={customOnDragSelect}
|
||||
setCurrentGraphRef={setCurrentGraphRef}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -36,6 +36,7 @@ function GridCardGraph({
|
||||
version,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
customOnDragSelect,
|
||||
customTooltipElement,
|
||||
dataAvailable,
|
||||
getGraphData,
|
||||
@@ -272,6 +273,7 @@ function GridCardGraph({
|
||||
setRequestData={setRequestData}
|
||||
onClickHandler={onClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
customOnDragSelect={customOnDragSelect}
|
||||
customTooltipElement={customTooltipElement}
|
||||
openTracesButton={openTracesButton}
|
||||
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface WidgetGraphComponentProps {
|
||||
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
customOnDragSelect?: (start: number, end: number) => void;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
openTracesButton?: boolean;
|
||||
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||
@@ -49,6 +50,7 @@ export interface GridCardGraphProps {
|
||||
variables?: Dashboard['data']['variables'];
|
||||
version?: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
customOnDragSelect?: (start: number, end: number) => void;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
dataAvailable?: (isDataAvailable: boolean) => void;
|
||||
getGraphData?: (graphData?: MetricRangePayloadProps['data']) => void;
|
||||
|
||||
@@ -178,6 +178,7 @@ interface HandleGraphClickParams {
|
||||
navigateToExplorer: (props: NavigateToExplorerProps) => void;
|
||||
notifications: NotificationInstance;
|
||||
graphClick: (props: GraphClickProps) => void;
|
||||
customFilters?: TagFilterItem[];
|
||||
}
|
||||
|
||||
export const handleGraphClick = async ({
|
||||
@@ -192,6 +193,7 @@ export const handleGraphClick = async ({
|
||||
navigateToExplorer,
|
||||
notifications,
|
||||
graphClick,
|
||||
customFilters,
|
||||
}: HandleGraphClickParams): Promise<void> => {
|
||||
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
|
||||
|
||||
@@ -221,7 +223,7 @@ export const handleGraphClick = async ({
|
||||
}: ${key}`,
|
||||
onClick: (): void =>
|
||||
navigateToExplorer({
|
||||
filters: result[key].filters,
|
||||
filters: [...result[key].filters, ...(customFilters || [])],
|
||||
dataSource: result[key].dataSource as DataSource,
|
||||
startTime: xValue,
|
||||
endTime: xValue + (stepInterval ?? 60),
|
||||
|
||||
@@ -44,7 +44,10 @@ import { EditMenuAction, ViewMenuAction } from './config';
|
||||
import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
|
||||
import GridCard from './GridCard';
|
||||
import { Card, CardContainer, ReactGridLayout } from './styles';
|
||||
import { removeUndefinedValuesFromLayout } from './utils';
|
||||
import {
|
||||
hasColumnWidthsChanged,
|
||||
removeUndefinedValuesFromLayout,
|
||||
} from './utils';
|
||||
import { MenuItemKeys } from './WidgetHeader/contants';
|
||||
import { WidgetRowHeader } from './WidgetRow';
|
||||
|
||||
@@ -68,6 +71,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
setDashboardQueryRangeCalled,
|
||||
setSelectedRowWidgetId,
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
} = useDashboard();
|
||||
const { data } = selectedDashboard || {};
|
||||
const { pathname } = useLocation();
|
||||
@@ -162,6 +166,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
@@ -171,6 +176,15 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
...selectedDashboard.data,
|
||||
panelMap: { ...currentPanelMap },
|
||||
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||
widgets: selectedDashboard?.data?.widgets?.map((widget) => {
|
||||
if (columnWidths?.[widget.id]) {
|
||||
return {
|
||||
...widget,
|
||||
columnWidths: columnWidths[widget.id],
|
||||
};
|
||||
}
|
||||
return widget;
|
||||
}),
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
@@ -227,20 +241,31 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDashboardLocked ||
|
||||
!saveLayoutPermission ||
|
||||
updateDashboardMutation.isLoading ||
|
||||
isDashboardFetching
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldSaveLayout =
|
||||
dashboardLayout &&
|
||||
Array.isArray(dashboardLayout) &&
|
||||
dashboardLayout.length > 0 &&
|
||||
!isEqual(layouts, dashboardLayout) &&
|
||||
!isDashboardLocked &&
|
||||
saveLayoutPermission &&
|
||||
!updateDashboardMutation.isLoading &&
|
||||
!isDashboardFetching
|
||||
) {
|
||||
!isEqual(layouts, dashboardLayout);
|
||||
|
||||
const shouldSaveColumnWidths =
|
||||
dashboardLayout &&
|
||||
Array.isArray(dashboardLayout) &&
|
||||
dashboardLayout.length > 0 &&
|
||||
hasColumnWidthsChanged(columnWidths, selectedDashboard);
|
||||
|
||||
if (shouldSaveLayout || shouldSaveColumnWidths) {
|
||||
onSaveHandler();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardLayout]);
|
||||
}, [dashboardLayout, columnWidths]);
|
||||
|
||||
const onSettingsModalSubmit = (): void => {
|
||||
const newTitle = form.getFieldValue('title');
|
||||
|
||||
@@ -12,7 +12,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { extractQueryNamesFromExpression } from './utils';
|
||||
|
||||
type GraphClickMetaData = {
|
||||
export type GraphClickMetaData = {
|
||||
[key: string]: string | boolean;
|
||||
queryName: string;
|
||||
inFocusOrNot: boolean;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FORMULA_REGEXP } from 'constants/regExp';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
||||
layout.map((obj) =>
|
||||
@@ -25,3 +27,27 @@ export function extractQueryNamesFromExpression(expression: string): string[] {
|
||||
// Extract matches and deduplicate
|
||||
return [...new Set(expression.match(queryNameRegex) || [])];
|
||||
}
|
||||
|
||||
export const hasColumnWidthsChanged = (
|
||||
columnWidths: Record<string, Record<string, number>>,
|
||||
selectedDashboard?: Dashboard,
|
||||
): boolean => {
|
||||
// If no column widths stored, no changes
|
||||
if (isEmpty(columnWidths) || !selectedDashboard) return false;
|
||||
|
||||
// Check each widget's column widths
|
||||
return Object.keys(columnWidths).some((widgetId) => {
|
||||
const dashboardWidget = selectedDashboard?.data?.widgets?.find(
|
||||
(widget) => widget.id === widgetId,
|
||||
) as Widgets;
|
||||
|
||||
const newWidths = columnWidths[widgetId];
|
||||
const existingWidths = dashboardWidget?.columnWidths;
|
||||
|
||||
// If both are empty/undefined, no change
|
||||
if (isEmpty(newWidths) || isEmpty(existingWidths)) return false;
|
||||
|
||||
// Compare stored column widths with dashboard widget's column widths
|
||||
return !isEqual(newWidths, existingWidths);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ function GridTableComponent({
|
||||
sticky,
|
||||
openTracesButton,
|
||||
onOpenTraceBtnClick,
|
||||
widgetId,
|
||||
...props
|
||||
}: GridTableComponentProps): JSX.Element {
|
||||
const { t } = useTranslation(['valueGraph']);
|
||||
@@ -229,6 +230,7 @@ function GridTableComponent({
|
||||
columns={openTracesButton ? columnDataWithOpenTracesButton : newColumnData}
|
||||
dataSource={dataSource}
|
||||
sticky={sticky}
|
||||
widgetId={widgetId}
|
||||
onRow={
|
||||
openTracesButton
|
||||
? (record): React.HTMLAttributes<HTMLElement> => ({
|
||||
|
||||
@@ -17,6 +17,7 @@ export type GridTableComponentProps = {
|
||||
searchTerm?: string;
|
||||
openTracesButton?: boolean;
|
||||
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||
widgetId?: string;
|
||||
} & Pick<LogsExplorerTableProps, 'data'> &
|
||||
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
||||
|
||||
|
||||
@@ -63,17 +63,17 @@ export default function SavedViews({
|
||||
|
||||
const handleRedirectQuery = (view: ViewProps): void => {
|
||||
logEvent('Homepage: Saved view clicked', {
|
||||
viewId: view.uuid,
|
||||
viewId: view.id,
|
||||
viewName: view.name,
|
||||
entity: selectedEntity,
|
||||
});
|
||||
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(
|
||||
view.uuid,
|
||||
view.id,
|
||||
selectedEntity === 'logs' ? logsViews : tracesViews,
|
||||
);
|
||||
if (!currentViewDetails) return;
|
||||
const { query, name, uuid, panelType: currentPanelType } = currentViewDetails;
|
||||
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
|
||||
|
||||
if (selectedEntity) {
|
||||
handleExplorerTabChange(
|
||||
@@ -81,7 +81,7 @@ export default function SavedViews({
|
||||
{
|
||||
query,
|
||||
name,
|
||||
uuid,
|
||||
id,
|
||||
},
|
||||
SOURCEPAGE_VS_ROUTES[selectedEntity],
|
||||
);
|
||||
|
||||
@@ -702,29 +702,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
min-width: 140px !important;
|
||||
max-width: 140px !important;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.pod-name-header) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.med-col) {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-k8s-list-table {
|
||||
.infra-monitoring-container {
|
||||
.ant-table-cell {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
min-width: 140px !important;
|
||||
max-width: 140px !important;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.pod-name-header) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.med-col) {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-k8s-list-table {
|
||||
.ant-table-cell {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
@@ -733,11 +740,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
|
||||
.event-content-container {
|
||||
.ant-table {
|
||||
background: var(--bg-ink-400);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import './LogsPanelComponent.styles.scss';
|
||||
|
||||
import { Table } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Controls from 'container/Controls';
|
||||
@@ -79,9 +79,14 @@ function LogsPanelComponent({
|
||||
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const columns = getLogPanelColumnsList(
|
||||
widget.selectedLogFields,
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getLogPanelColumnsList(
|
||||
widget.selectedLogFields,
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[widget.selectedLogFields],
|
||||
);
|
||||
|
||||
const dataLength =
|
||||
@@ -216,16 +221,18 @@ function LogsPanelComponent({
|
||||
<div className="logs-table">
|
||||
<div className="resize-table">
|
||||
<OverlayScrollbar>
|
||||
<Table
|
||||
<ResizeTable
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: `calc(50vw - 10px)` }}
|
||||
scroll={{ x: `max-content` }}
|
||||
sticky
|
||||
loading={queryResponse.isFetching}
|
||||
style={tableStyles}
|
||||
dataSource={flattenLogData}
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
widgetId={widget.id}
|
||||
shouldPersistColumnWidths
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
|
||||
@@ -159,6 +159,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.log-scale {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.panel-time-text {
|
||||
margin-top: 16px;
|
||||
color: var(--bg-vanilla-400);
|
||||
|
||||
@@ -61,6 +61,18 @@ export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
} as const;
|
||||
|
||||
export const panelTypeVsLogScale: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.TIME_SERIES]: true,
|
||||
[PANEL_TYPES.VALUE]: false,
|
||||
[PANEL_TYPES.TABLE]: false,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: false,
|
||||
[PANEL_TYPES.BAR]: true,
|
||||
[PANEL_TYPES.HISTOGRAM]: false,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
} as const;
|
||||
|
||||
export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.TIME_SERIES]: true,
|
||||
[PANEL_TYPES.VALUE]: true,
|
||||
|
||||
@@ -10,7 +10,7 @@ import GraphTypes, {
|
||||
} from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ConciergeBell, Plus } from 'lucide-react';
|
||||
import { ConciergeBell, LineChart, Plus, Spline } from 'lucide-react';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
panelTypeVsColumnUnitPreferences,
|
||||
panelTypeVsCreateAlert,
|
||||
panelTypeVsFillSpan,
|
||||
panelTypeVsLogScale,
|
||||
panelTypeVsPanelTimePreferences,
|
||||
panelTypeVsSoftMinMax,
|
||||
panelTypeVsStackingChartPreferences,
|
||||
@@ -41,6 +42,12 @@ import YAxisUnitSelector from './YAxisUnitSelector';
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
enum LogScale {
|
||||
LINEAR = 'linear',
|
||||
LOGARITHMIC = 'logarithmic',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function RightContainer({
|
||||
description,
|
||||
setDescription,
|
||||
@@ -71,6 +78,8 @@ function RightContainer({
|
||||
setSoftMin,
|
||||
columnUnits,
|
||||
setColumnUnits,
|
||||
isLogScale,
|
||||
setIsLogScale,
|
||||
}: RightContainerProps): JSX.Element {
|
||||
const onChangeHandler = useCallback(
|
||||
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
|
||||
@@ -87,6 +96,7 @@ function RightContainer({
|
||||
const allowThreshold = panelTypeVsThreshold[selectedGraph];
|
||||
const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph];
|
||||
const allowFillSpans = panelTypeVsFillSpan[selectedGraph];
|
||||
const allowLogScale = panelTypeVsLogScale[selectedGraph];
|
||||
const allowYAxisUnit = panelTypeVsYAxisUnit[selectedGraph];
|
||||
const allowCreateAlerts = panelTypeVsCreateAlert[selectedGraph];
|
||||
const allowBucketConfig = panelTypeVsBucketConfig[selectedGraph];
|
||||
@@ -293,6 +303,36 @@ function RightContainer({
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{allowLogScale && (
|
||||
<section className="log-scale">
|
||||
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
|
||||
<Select
|
||||
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
|
||||
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
|
||||
style={{ width: '100%' }}
|
||||
className="panel-type-select"
|
||||
defaultValue={LogScale.LINEAR}
|
||||
>
|
||||
<Option value={LogScale.LINEAR}>
|
||||
<div className="select-option">
|
||||
<div className="icon">
|
||||
<LineChart size={16} />
|
||||
</div>
|
||||
<Typography.Text className="display">Linear</Typography.Text>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value={LogScale.LOGARITHMIC}>
|
||||
<div className="select-option">
|
||||
<div className="icon">
|
||||
<Spline size={16} />
|
||||
</div>
|
||||
<Typography.Text className="display">Logarithmic</Typography.Text>
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{allowCreateAlerts && (
|
||||
@@ -356,6 +396,8 @@ interface RightContainerProps {
|
||||
setColumnUnits: Dispatch<SetStateAction<ColumnUnit>>;
|
||||
setSoftMin: Dispatch<SetStateAction<number | null>>;
|
||||
setSoftMax: Dispatch<SetStateAction<number | null>>;
|
||||
isLogScale: boolean;
|
||||
setIsLogScale: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
RightContainer.defaultProps = {
|
||||
|
||||
@@ -74,6 +74,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
setToScrollWidgetId,
|
||||
selectedRowWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
columnWidths,
|
||||
} = useDashboard();
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
@@ -170,6 +171,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
const [isFillSpans, setIsFillSpans] = useState<boolean>(
|
||||
selectedWidget?.fillSpans || false,
|
||||
);
|
||||
const [isLogScale, setIsLogScale] = useState<boolean>(
|
||||
selectedWidget?.isLogScale || false,
|
||||
);
|
||||
const [saveModal, setSaveModal] = useState(false);
|
||||
const [discardModal, setDiscardModal] = useState(false);
|
||||
|
||||
@@ -234,8 +238,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
mergeAllActiveQueries: combineHistogram,
|
||||
selectedLogFields,
|
||||
selectedTracesFields,
|
||||
isLogScale,
|
||||
columnWidths: columnWidths?.[selectedWidget?.id],
|
||||
};
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
columnUnits,
|
||||
currentQuery,
|
||||
@@ -255,6 +262,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
bucketCount,
|
||||
combineHistogram,
|
||||
stackedBarChart,
|
||||
isLogScale,
|
||||
columnWidths,
|
||||
]);
|
||||
|
||||
const closeModal = (): void => {
|
||||
@@ -369,6 +378,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
|
||||
query: stagedQuery,
|
||||
fillGaps: selectedWidget.fillSpans || false,
|
||||
isLogScale: selectedWidget.isLogScale || false,
|
||||
formatForWeb:
|
||||
getGraphTypeForFormat(selectedGraph || selectedWidget.panelTypes) ===
|
||||
PANEL_TYPES.TABLE,
|
||||
@@ -379,6 +389,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
stagedQuery,
|
||||
selectedTime,
|
||||
selectedWidget.fillSpans,
|
||||
selectedWidget.isLogScale,
|
||||
globalSelectedInterval,
|
||||
]);
|
||||
|
||||
@@ -442,6 +453,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
softMin: selectedWidget?.softMin || 0,
|
||||
softMax: selectedWidget?.softMax || 0,
|
||||
fillSpans: selectedWidget?.fillSpans,
|
||||
isLogScale: selectedWidget?.isLogScale || false,
|
||||
bucketWidth: selectedWidget?.bucketWidth || 0,
|
||||
bucketCount: selectedWidget?.bucketCount || 0,
|
||||
mergeAllActiveQueries: selectedWidget?.mergeAllActiveQueries || false,
|
||||
@@ -468,6 +480,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
softMin: selectedWidget?.softMin || 0,
|
||||
softMax: selectedWidget?.softMax || 0,
|
||||
fillSpans: selectedWidget?.fillSpans,
|
||||
isLogScale: selectedWidget?.isLogScale || false,
|
||||
bucketWidth: selectedWidget?.bucketWidth || 0,
|
||||
bucketCount: selectedWidget?.bucketCount || 0,
|
||||
mergeAllActiveQueries: selectedWidget?.mergeAllActiveQueries || false,
|
||||
@@ -730,6 +743,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedWidget={selectedWidget}
|
||||
isFillSpans={isFillSpans}
|
||||
setIsFillSpans={setIsFillSpans}
|
||||
isLogScale={isLogScale}
|
||||
setIsLogScale={setIsLogScale}
|
||||
softMin={softMin}
|
||||
setSoftMin={setSoftMin}
|
||||
softMax={softMax}
|
||||
|
||||
@@ -26,6 +26,7 @@ function TablePanelWrapper({
|
||||
searchTerm={searchTerm}
|
||||
openTracesButton={openTracesButton}
|
||||
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||
widgetId={widget.id}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...GRID_TABLE_CONFIG}
|
||||
/>
|
||||
|
||||
@@ -137,6 +137,7 @@ function UplotPanelWrapper({
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
timezone: timezone.value,
|
||||
customSeries,
|
||||
isLogScale: widget?.isLogScale,
|
||||
}),
|
||||
[
|
||||
widget?.id,
|
||||
@@ -161,6 +162,7 @@ function UplotPanelWrapper({
|
||||
customTooltipElement,
|
||||
timezone.value,
|
||||
customSeries,
|
||||
widget?.isLogScale,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
width: 0.625rem;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
@@ -54,7 +56,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
class="query-table"
|
||||
>
|
||||
<div
|
||||
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"
|
||||
class="ant-table-wrapper resize-main-table css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<div
|
||||
class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap"
|
||||
@@ -82,7 +84,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
<tr>
|
||||
<th
|
||||
aria-label="service_name"
|
||||
class="ant-table-cell ant-table-column-has-sorters react-resizable"
|
||||
class="resizable-header react-resizable"
|
||||
scope="col"
|
||||
tabindex="0"
|
||||
>
|
||||
@@ -143,12 +145,12 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="c1 react-resizable-handle"
|
||||
class="c1 resize-handle"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
aria-label="latency-per-service"
|
||||
class="ant-table-cell ant-table-column-has-sorters react-resizable"
|
||||
class="resizable-header react-resizable"
|
||||
scope="col"
|
||||
tabindex="0"
|
||||
>
|
||||
@@ -209,7 +211,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="c1 react-resizable-handle"
|
||||
class="c1 resize-handle"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -221,7 +223,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
|
||||
style="overflow-x: auto; overflow-y: hidden;"
|
||||
>
|
||||
<table
|
||||
style="width: auto; min-width: 100%; table-layout: fixed;"
|
||||
style="min-width: 100%; table-layout: fixed;"
|
||||
>
|
||||
<colgroup>
|
||||
<col
|
||||
|
||||
@@ -453,7 +453,7 @@ export const Query = memo(function Query({
|
||||
</Col>
|
||||
)}
|
||||
<Col flex="1" className="qb-search-container">
|
||||
{query.dataSource === DataSource.LOGS ? (
|
||||
{[DataSource.LOGS, DataSource.TRACES].includes(query.dataSource) ? (
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import './QueryBuilderSearchV2.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
@@ -25,6 +26,7 @@ interface ICustomDropdownProps {
|
||||
exampleQueries: TagFilter[];
|
||||
onChange: (value: TagFilter) => void;
|
||||
currentFilterItem?: ITag;
|
||||
isLogsDataSource: boolean;
|
||||
}
|
||||
|
||||
export default function QueryBuilderSearchDropdown(
|
||||
@@ -38,11 +40,14 @@ export default function QueryBuilderSearchDropdown(
|
||||
exampleQueries,
|
||||
options,
|
||||
onChange,
|
||||
isLogsDataSource,
|
||||
} = props;
|
||||
const userOs = getUserOperatingSystem();
|
||||
return (
|
||||
<>
|
||||
<div className="content">
|
||||
<div
|
||||
className={cx('content', { 'non-logs-data-source': !isLogsDataSource })}
|
||||
>
|
||||
{!currentFilterItem?.key ? (
|
||||
<div className="suggested-filters">Suggested Filters</div>
|
||||
) : !currentFilterItem?.op ? (
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
.rc-virtual-list-holder {
|
||||
height: 115px;
|
||||
}
|
||||
&.non-logs-data-source {
|
||||
.rc-virtual-list-holder {
|
||||
height: 256px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ interface QueryBuilderSearchV2Props {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@@ -119,6 +120,7 @@ function QueryBuilderSearchV2(
|
||||
className,
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
hardcodedAttributeKeys,
|
||||
} = props;
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@@ -233,7 +235,7 @@ function QueryBuilderSearchV2(
|
||||
},
|
||||
{
|
||||
queryKey: [searchParams],
|
||||
enabled: isQueryEnabled && !isLogsDataSource,
|
||||
enabled: isQueryEnabled && !isLogsDataSource && !hardcodedAttributeKeys,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -674,13 +676,42 @@ function QueryBuilderSearchV2(
|
||||
value: key,
|
||||
})) || []),
|
||||
]);
|
||||
} else {
|
||||
} else if (hardcodedAttributeKeys) {
|
||||
const filteredKeys = hardcodedAttributeKeys.filter((key) =>
|
||||
key.key
|
||||
.toLowerCase()
|
||||
.includes((searchValue?.split(' ')[0] || '').toLowerCase()),
|
||||
);
|
||||
setDropdownOptions(
|
||||
data?.payload?.attributeKeys?.map((key) => ({
|
||||
filteredKeys.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || [],
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
setDropdownOptions([
|
||||
// Add user typed option if it doesn't exist in the payload
|
||||
...(!isEmpty(tagKey) &&
|
||||
!data?.payload?.attributeKeys?.some((val) => isEqual(val.key, tagKey))
|
||||
? [
|
||||
{
|
||||
label: tagKey,
|
||||
value: {
|
||||
key: tagKey,
|
||||
dataType: DataTypes.EMPTY,
|
||||
type: '',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Map existing attribute keys from payload
|
||||
...(data?.payload?.attributeKeys?.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || []),
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (currentState === DropdownState.OPERATOR) {
|
||||
@@ -752,6 +783,7 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
}
|
||||
}, [
|
||||
hardcodedAttributeKeys,
|
||||
attributeValues?.payload,
|
||||
currentFilterItem?.key?.dataType,
|
||||
currentState,
|
||||
@@ -949,6 +981,7 @@ function QueryBuilderSearchV2(
|
||||
exampleQueries={suggestionsData?.payload?.example_queries || []}
|
||||
tags={tags}
|
||||
currentFilterItem={currentFilterItem}
|
||||
isLogsDataSource={isLogsDataSource}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
@@ -984,6 +1017,7 @@ QueryBuilderSearchV2.defaultProps = {
|
||||
className: '',
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
hardcodedAttributeKeys: undefined,
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchV2;
|
||||
|
||||
@@ -20,4 +20,5 @@ export type QueryTableProps = Omit<
|
||||
dataSource?: RowData[];
|
||||
sticky?: TableProps<RowData>['sticky'];
|
||||
searchTerm?: string;
|
||||
widgetId?: string;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export function QueryTable({
|
||||
dataSource,
|
||||
sticky,
|
||||
searchTerm,
|
||||
widgetId,
|
||||
...props
|
||||
}: QueryTableProps): JSX.Element {
|
||||
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
|
||||
@@ -95,8 +96,10 @@ export function QueryTable({
|
||||
columns={tableColumns}
|
||||
tableLayout="fixed"
|
||||
dataSource={filterTable === null ? newDataSource : filterTable}
|
||||
scroll={{ x: true }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={paginationConfig}
|
||||
widgetId={widgetId}
|
||||
shouldPersistColumnWidths
|
||||
sticky={sticky}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
|
||||
@@ -279,6 +279,17 @@ function SideNav(): JSX.Element {
|
||||
let updatedUserManagementItems: UserManagementMenuItems[] = [
|
||||
manageLicenseMenuItem,
|
||||
];
|
||||
|
||||
const isApiMonitoringEnabled = featureFlags?.find(
|
||||
(flag) => flag.name === FeatureKeys.THIRD_PARTY_API,
|
||||
)?.active;
|
||||
|
||||
if (!isApiMonitoringEnabled) {
|
||||
updatedMenuItems = updatedMenuItems.filter(
|
||||
(item) => item.key !== ROUTES.API_MONITORING,
|
||||
);
|
||||
}
|
||||
|
||||
if (isCloudUserVal || isEECloudUserVal) {
|
||||
const isOnboardingEnabled =
|
||||
featureFlags?.find((feature) => feature.name === FeatureKeys.ONBOARDING)
|
||||
|
||||
@@ -3,6 +3,7 @@ import ROUTES from 'constants/routes';
|
||||
import {
|
||||
BarChart2,
|
||||
BellDot,
|
||||
Binoculars,
|
||||
Boxes,
|
||||
BugIcon,
|
||||
Cloudy,
|
||||
@@ -123,6 +124,12 @@ const menuItems: SidebarItem[] = [
|
||||
label: 'Messaging Queues',
|
||||
icon: <ListMinus size={16} />,
|
||||
},
|
||||
{
|
||||
key: ROUTES.API_MONITORING,
|
||||
label: 'API Monitoring',
|
||||
icon: <Binoculars size={16} />,
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
key: ROUTES.LIST_ALL_ALERT,
|
||||
label: 'Alerts',
|
||||
|
||||
@@ -226,6 +226,7 @@ export const routesToSkip = [
|
||||
ROUTES.METRICS_EXPLORER,
|
||||
ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||
ROUTES.METRICS_EXPLORER_VIEWS,
|
||||
ROUTES.API_MONITORING,
|
||||
ROUTES.CHANNELS_NEW,
|
||||
ROUTES.CHANNELS_EDIT,
|
||||
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './TracesTableComponent.styles.scss';
|
||||
|
||||
import { Table } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import Controls from 'container/Controls';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
@@ -54,9 +54,14 @@ function TracesTableComponent({
|
||||
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const columns = getListColumns(
|
||||
widget.selectedTracesFields || [],
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getListColumns(
|
||||
widget.selectedTracesFields || [],
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[widget.selectedTracesFields],
|
||||
);
|
||||
|
||||
const dataLength =
|
||||
@@ -116,16 +121,18 @@ function TracesTableComponent({
|
||||
<div className="traces-table">
|
||||
<div className="resize-table">
|
||||
<OverlayScrollbar>
|
||||
<Table
|
||||
<ResizeTable
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: true }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
loading={queryResponse.isFetching}
|
||||
style={tableStyles}
|
||||
dataSource={transformedQueryTableData}
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
sticky
|
||||
widgetId={widget.id}
|
||||
shouldPersistColumnWidths
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
|
||||
@@ -170,11 +170,7 @@ export const useOptions = (
|
||||
(option, index, self) =>
|
||||
index ===
|
||||
self.findIndex(
|
||||
(o) =>
|
||||
// to remove duplicate & empty options from list
|
||||
o.label === option.label &&
|
||||
o.value === option.value &&
|
||||
o.dataType?.toLowerCase() === option.dataType?.toLowerCase(), // handle case sensitivity
|
||||
(o) => o.label === option.label && o.value === option.value, // to remove duplicate & empty options from list
|
||||
) && option.value !== '',
|
||||
) || []
|
||||
).map((option) => {
|
||||
|
||||
@@ -70,7 +70,7 @@ export const useHandleExplorerTabChange = (): {
|
||||
{
|
||||
[QueryParams.panelTypes]: newPanelType,
|
||||
[QueryParams.viewName]: currentQueryData?.name || viewName,
|
||||
[QueryParams.viewKey]: currentQueryData?.uuid || viewKey,
|
||||
[QueryParams.viewKey]: currentQueryData?.id || viewKey,
|
||||
},
|
||||
redirectToUrl,
|
||||
);
|
||||
@@ -78,7 +78,7 @@ export const useHandleExplorerTabChange = (): {
|
||||
redirectWithQueryBuilderData(query, {
|
||||
[QueryParams.panelTypes]: newPanelType,
|
||||
[QueryParams.viewName]: currentQueryData?.name || viewName,
|
||||
[QueryParams.viewKey]: currentQueryData?.uuid || viewKey,
|
||||
[QueryParams.viewKey]: currentQueryData?.id || viewKey,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -90,6 +90,6 @@ export const useHandleExplorerTabChange = (): {
|
||||
|
||||
interface ICurrentQueryData {
|
||||
name: string;
|
||||
uuid: string;
|
||||
id: string;
|
||||
query: Query;
|
||||
}
|
||||
|
||||
@@ -102,4 +102,5 @@ export interface GetQueryResultsProps {
|
||||
};
|
||||
start?: number;
|
||||
end?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface GetUPlotChartOptions {
|
||||
tzDate?: (timestamp: number) => Date;
|
||||
timezone?: string;
|
||||
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||
isLogScale?: boolean;
|
||||
}
|
||||
|
||||
/** the function converts series A , series B , series C to
|
||||
@@ -164,6 +165,7 @@ export const getUPlotChartOptions = ({
|
||||
tzDate,
|
||||
timezone,
|
||||
customSeries,
|
||||
isLogScale,
|
||||
}: GetUPlotChartOptions): uPlot.Options => {
|
||||
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||
|
||||
@@ -220,6 +222,7 @@ export const getUPlotChartOptions = ({
|
||||
softMax,
|
||||
softMin,
|
||||
}),
|
||||
distr: isLogScale ? 3 : 1,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -387,6 +390,6 @@ export const getUPlotChartOptions = ({
|
||||
hiddenGraph,
|
||||
isDarkMode,
|
||||
}),
|
||||
axes: getAxes({ isDarkMode, yAxisUnit, panelType }),
|
||||
axes: getAxes({ isDarkMode, yAxisUnit, panelType, isLogScale }),
|
||||
};
|
||||
};
|
||||
|
||||
51
frontend/src/lib/uPlotLib/utils/dataUtils.ts
Normal file
51
frontend/src/lib/uPlotLib/utils/dataUtils.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Checks if a value is invalid for plotting
|
||||
*
|
||||
* @param value - The value to check
|
||||
* @returns true if the value is invalid (should be replaced with null), false otherwise
|
||||
*/
|
||||
export function isInvalidPlotValue(value: unknown): boolean {
|
||||
// Check for null or undefined
|
||||
if (value === null || value === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle number checks
|
||||
if (typeof value === 'number') {
|
||||
// Check for NaN, Infinity, -Infinity
|
||||
return !Number.isFinite(value);
|
||||
}
|
||||
|
||||
// Handle string values
|
||||
if (typeof value === 'string') {
|
||||
// Check for string representations of infinity
|
||||
if (['+Inf', '-Inf', 'Infinity', '-Infinity', 'NaN'].includes(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to parse the string as a number
|
||||
const numValue = parseFloat(value);
|
||||
|
||||
// If parsing failed or resulted in a non-finite number, it's invalid
|
||||
if (Number.isNaN(numValue) || !Number.isFinite(numValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Value is valid for plotting
|
||||
return false;
|
||||
}
|
||||
|
||||
export function normalizePlotValue(value: unknown): number | null {
|
||||
if (isInvalidPlotValue(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert string numbers to actual numbers
|
||||
if (typeof value === 'string') {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
// Already a valid number
|
||||
return value as number;
|
||||
}
|
||||
@@ -17,16 +17,19 @@ const getAxes = ({
|
||||
isDarkMode,
|
||||
yAxisUnit,
|
||||
panelType,
|
||||
isLogScale,
|
||||
}: {
|
||||
isDarkMode: boolean;
|
||||
yAxisUnit?: string;
|
||||
panelType?: PANEL_TYPES;
|
||||
isLogScale?: boolean;
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
}): any => [
|
||||
{
|
||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||
grid: {
|
||||
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
||||
width: 0.2, // Width of the grid lines,
|
||||
width: isLogScale ? 0.1 : 0.2, // Width of the grid lines,
|
||||
show: true,
|
||||
},
|
||||
ticks: {
|
||||
@@ -45,17 +48,20 @@ const getAxes = ({
|
||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||
grid: {
|
||||
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
||||
width: 0.2, // Width of the grid lines
|
||||
width: isLogScale ? 0.1 : 0.2, // Width of the grid lines
|
||||
},
|
||||
ticks: {
|
||||
// stroke: isDarkMode ? 'white' : 'black', // Color of the tick lines
|
||||
width: 0.3, // Width of the tick lines
|
||||
show: true,
|
||||
},
|
||||
...(isLogScale ? { space: 20 } : {}),
|
||||
values: (_, t): string[] =>
|
||||
t.map((v) => {
|
||||
if (v === null || v === undefined || Number.isNaN(v)) {
|
||||
return '';
|
||||
}
|
||||
const value = getToolTipValue(v.toString(), yAxisUnit);
|
||||
|
||||
return `${value}`;
|
||||
}),
|
||||
gap: 5,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cloneDeep, isUndefined } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { normalizePlotValue } from './dataUtils';
|
||||
import { generateColor } from './generateColor';
|
||||
|
||||
function getXAxisTimestamps(seriesList: QueryData[]): number[] {
|
||||
@@ -43,16 +44,8 @@ function fillMissingXAxisTimestamps(timestampArr: number[], data: any[]): any {
|
||||
});
|
||||
|
||||
entry.values.forEach((v) => {
|
||||
if (Number.isNaN(v[1])) {
|
||||
const replaceValue = null;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v[1] = replaceValue;
|
||||
} else if (v[1] !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v[1] = parseFloat(v[1]);
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v[1] = normalizePlotValue(v[1]);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
||||
@@ -2,7 +2,7 @@ export const explorerView = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
uuid: 'test-uuid-1',
|
||||
id: 'test-uuid-1',
|
||||
name: 'Table View',
|
||||
category: '',
|
||||
createdAt: '2023-08-29T18:04:10.906310033Z',
|
||||
@@ -78,7 +78,7 @@ export const explorerView = {
|
||||
extraData: '{"color":"#00ffd0"}',
|
||||
},
|
||||
{
|
||||
uuid: '8c4bf492-d54d-4ab2-a8d6-9c1563f46e1f',
|
||||
id: '8c4bf492-d54d-4ab2-a8d6-9c1563f46e1f',
|
||||
name: 'R-test panel',
|
||||
category: '',
|
||||
createdAt: '2024-07-01T13:45:57.924686766Z',
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
.api-monitoring-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
.ant-tabs {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 0px;
|
||||
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.api-monitoring-page {
|
||||
.ant-tabs-nav {
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
frontend/src/pages/ApiMonitoring/ApiMonitoringPage.tsx
Normal file
22
frontend/src/pages/ApiMonitoring/ApiMonitoringPage.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import './ApiMonitoringPage.styles.scss';
|
||||
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import history from 'lib/history';
|
||||
import { useLocation } from 'react-use';
|
||||
|
||||
import { Explorer } from './constants';
|
||||
|
||||
function ApiMonitoringPage(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const routes: TabRoutes[] = [Explorer];
|
||||
|
||||
return (
|
||||
<div className="api-monitoring-page">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiMonitoringPage;
|
||||
15
frontend/src/pages/ApiMonitoring/constants.tsx
Normal file
15
frontend/src/pages/ApiMonitoring/constants.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ExplorerPage from 'container/ApiMonitoring/Explorer/Explorer';
|
||||
import { Compass } from 'lucide-react';
|
||||
|
||||
export const Explorer: TabRoutes = {
|
||||
Component: ExplorerPage,
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Compass size={16} /> Explorer
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.API_MONITORING,
|
||||
key: ROUTES.API_MONITORING,
|
||||
};
|
||||
3
frontend/src/pages/ApiMonitoring/index.ts
Normal file
3
frontend/src/pages/ApiMonitoring/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ApiMonitoring from './ApiMonitoringPage';
|
||||
|
||||
export default ApiMonitoring;
|
||||
@@ -81,7 +81,7 @@ function SaveView(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleEditModelOpen = (view: ViewProps, color: string): void => {
|
||||
setActiveViewKey(view.uuid);
|
||||
setActiveViewKey(view.id);
|
||||
setColor(color);
|
||||
setActiveViewName(view.name);
|
||||
setNewViewName(view.name);
|
||||
@@ -188,11 +188,11 @@ function SaveView(): JSX.Element {
|
||||
|
||||
const handleRedirectQuery = (view: ViewProps): void => {
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(
|
||||
view.uuid,
|
||||
view.id,
|
||||
viewsData?.data.data,
|
||||
);
|
||||
if (!currentViewDetails) return;
|
||||
const { query, name, uuid, panelType: currentPanelType } = currentViewDetails;
|
||||
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
|
||||
|
||||
if (sourcepage) {
|
||||
handleExplorerTabChange(
|
||||
@@ -200,7 +200,7 @@ function SaveView(): JSX.Element {
|
||||
{
|
||||
query,
|
||||
name,
|
||||
uuid,
|
||||
id,
|
||||
},
|
||||
SOURCEPAGE_VS_ROUTES[sourcepage],
|
||||
);
|
||||
@@ -258,7 +258,7 @@ function SaveView(): JSX.Element {
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
color={Color.BG_CHERRY_500}
|
||||
data-testid="delete-view"
|
||||
onClick={(): void => handleDeleteModelOpen(view.uuid, view.name)}
|
||||
onClick={(): void => handleDeleteModelOpen(view.id, view.name)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,18 +2,24 @@ import './TracesModulePage.styles.scss';
|
||||
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { tracesExplorer, tracesFunnel, tracesSaveView } from './constants';
|
||||
|
||||
function TracesModulePage(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { featureFlags } = useAppContext();
|
||||
|
||||
const isTraceFunnelsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.TRACE_FUNNELS)
|
||||
?.active || false;
|
||||
|
||||
const routes: TabRoutes[] = [
|
||||
tracesExplorer,
|
||||
// TODO(shaheer): remove this check after everything is ready
|
||||
process.env.NODE_ENV === 'development' ? tracesFunnel : null,
|
||||
isTraceFunnelsEnabled ? tracesFunnel : null,
|
||||
tracesSaveView,
|
||||
].filter(Boolean) as TabRoutes[];
|
||||
|
||||
|
||||
@@ -40,7 +40,11 @@ import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
import { DashboardSortOrder, IDashboardContext } from './types';
|
||||
import {
|
||||
DashboardSortOrder,
|
||||
IDashboardContext,
|
||||
WidgetColumnWidths,
|
||||
} from './types';
|
||||
import { sortLayout } from './util';
|
||||
|
||||
const DashboardContext = createContext<IDashboardContext>({
|
||||
@@ -74,6 +78,8 @@ const DashboardContext = createContext<IDashboardContext>({
|
||||
selectedRowWidgetId: '',
|
||||
setSelectedRowWidgetId: () => {},
|
||||
isDashboardFetching: false,
|
||||
columnWidths: {},
|
||||
setColumnWidths: () => {},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
@@ -408,6 +414,8 @@ export function DashboardProvider({
|
||||
}
|
||||
};
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState<WidgetColumnWidths>({});
|
||||
|
||||
const value: IDashboardContext = useMemo(
|
||||
() => ({
|
||||
toScrollWidgetId,
|
||||
@@ -435,6 +443,8 @@ export function DashboardProvider({
|
||||
selectedRowWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
setColumnWidths,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
@@ -457,6 +467,8 @@ export function DashboardProvider({
|
||||
selectedRowWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
setColumnWidths,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user