Compare commits

...

33 Commits

Author SHA1 Message Date
SagarRajput-7
b8d228a339 fix: make header sticky for table panel (#5892)
* fix: make header sticky for table panel

* fix: added sticky prop conditionally and updated test cases

* fix: added a smaller scrollbar

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-09-09 22:05:05 +05:30
Srikanth Chekuri
c6ba2b4598 fix: use inactive for empty alert state (#5902) 2024-09-09 21:47:07 +05:30
Vikrant Gupta
36adc17a34 fix: make the config isColumn false for all the filters (#5896) 2024-09-09 15:39:55 +05:30
Srikanth Chekuri
3e32dabf46 chore: alert state change and overall status (#5845) 2024-09-09 13:06:09 +05:30
Srikanth Chekuri
74c994fbab chore: make ee init rule manager with it's own prepareTask func (#5807) 2024-09-09 10:28:54 +05:30
Raj Kamal Singh
7844522691 Chore: qs filter suggestions: example queries for multiple top attributes (#5703)
* chore: put together helper for fetching values for multiple attributes

* chore: poc: use helper for filter suggestions

* chore: add a working impl for getting attrib values for multiple attributes

* chore: start updating integration test to account for new approach for getting log attrib values

* chore: use a global zap logger in filter suggestion tests

* chore: fix attrib values clickhouse query expectation

* chore: only query values for actual attributes when generating example queries

* chore: update clickhouse-go-mock

* chore: cleanup: separate params for attributesLimit and examplesLimit for filter suggestions

* chore: some test cleanup

* chore: some more cleanup

* chore: some more cleanup

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-09-09 10:12:36 +05:30
Nityananda Gohain
12f2f80958 feat: logsV4 resource table query builder (#5872)
* feat: logsV4 resource table query builder

* fix: address pr comments

* fix: escape %, _ for contains queries

* fix: resource attribute filtering case sensitive

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-09-08 14:14:13 +05:30
Sudeep MP
7b5ff54f47 refactor(alert timeline): update TopContributorsCard and Table styles (#5881)
* refactor(alert timeline): update TopContributorsCard and Table styles

- Update hover styles for collapsed section rows in TopContributorsCard
- Update text and icon colors on hover in TopContributorsCard
- Remove unnecessary styles for value column in Table
- Update font size and alignment for table headers in Table
- Update font size and alignment for created at column in Table
- Add actions column with ellipsis button in Table

* feat(alert history styles): update alert popover and top contributors card styles
2024-09-07 23:04:35 +05:30
Abhishek Mehandiratta
afc97511af feat(dashboard): add widget count to collapsed section rows (#5822) 2024-09-07 02:22:32 +05:30
Sudeep MP
ae857d3fcd feat(paywall blocker): improvements for trial end blocker screen (#5756)
* feat: add view templates option to dashboard menu

* feat: increase dropdown overlay width
Set the dropdown overlay width to 200px to provide breathing space for the dropdown button.
Added flex to wrap the dropdown button to create space between the right icon and the left elements.

* feat(paywall blocker): improvements for trial end blocker screen

- added new components locally for rendering static contents
- fixed SCSS code for better readablity
- seperated data to specific file
- added alert info style for the non admin users message

* chore: fixed few conditions

* feat(paywall title): added contact us to modal title

* feat: non admin users communication styles

* chore: added useState for the sidebar collapse state to be false

* test(WorkspaceLocked): update Jest test to sync with recent UX copy changes

* feat(workspaceLocked): added locale

added English and English-GB translations for workspace locked messages

* feat: reverted the translation for and sidebar collapse fix

- I have removed the scope for unitest having locale support
- remove the useEffect way to set sidebar collapse, instead added it in app layout
- removed the opacity effect on tabs

* refactor(workspaceLocked): refactor appLayout component to simplify the isWorkspaceLocked function

* refactor(workspaceLocked): simplify isWorkspaceLocked by converting it to a constant expression

* refactor(workspaceLocked): refactor modal classname and variable

---------

Co-authored-by: Pranay Prateek <pranay@signoz.io>
2024-09-06 18:56:13 +05:30
Vishal Sharma
0db2784d6b chore: calculate user count dynamically and set user role in identity… (#5870)
* chore: calculate user count dynamically and set user role in identity event

* chore: move to callbacks

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-09-06 14:46:18 +05:30
dependabot[bot]
47d1caf078 chore(deps): bump axios from 1.6.4 to 1.7.4 in /frontend (#5734)
Bumps [axios](https://github.com/axios/axios) from 1.6.4 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.4...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 11:57:24 +05:30
SagarRajput-7
292b3f418e chore: dashboard detail - panel data fetched - telemetry (#5871)
* chore: dashboard detail - panel data fetched - telemetry

* chore: dashboard detail - code refactor
2024-09-06 11:53:05 +05:30
SagarRajput-7
4eb533fff8 fix: added start and end time info text to educate user better around the schedule timelines (#5837)
* fix: added start and end time info text to educate user better around the schedule timelines

* fix: changed the start and endtime info text

* fix: changed the start and endtime info text

* fix: comment resolved
2024-09-06 11:50:02 +05:30
SagarRajput-7
7a10fe2b8c chore: hide old trace explorer cta btn from trace explorer page (#5850) 2024-09-06 11:23:28 +05:30
SagarRajput-7
4214e36d22 fix: added default fallback for selectedColumns, when the attributeKeys call gives empty (#5847) 2024-09-06 11:22:54 +05:30
Yunus M
b9ab6d3fd4 feat: show add credit card chat icon only to logged in users (#5863) 2024-09-06 11:21:07 +05:30
Yunus M
23704b00ce feat: show RPS message only if user is on trail and trail is not converted to sub (#5860)
* feat: show rps message only if user is on trail and trail is not converted to sub

* feat: show rps message only if user is on trail and trail is not converted to sub
2024-09-06 11:20:47 +05:30
Yunus M
266894b0f8 fix: strip starting and ending quotes from field value on copy to clipboard (#5831) 2024-09-06 11:17:56 +05:30
Vikrant Gupta
4a9847abdd feat: implement quick filters for the new logs explorer page (#5799)
* feat: logs quick filter

* feat: added open button in the closed state

* fix: build issues

* chore: minor css

* feat: handle changes for last used query,states and reset

* feat: refactor some code

* feat: handle on change functionality

* fix: handle only and all

* chore: handle empty edge cases

* feat: added necessary tooltips

* feat: use tag instead of tooltip icon

* feat: handle light mode designs

* feat: added correct facets

* feat: added resize observer for the graph resize

* chore: added local storage state for the toggle

* chore: make refresh text configurable

* feat: added environment and fix build

* feat: handle the cases for = and != operators

* feat: design changes and zoom out

* feat: minor css issue

* fix: light mode designs

* fix: handle the case for state initialization

* fix: onDelete query the last used index should be set to 0
2024-09-06 10:24:47 +05:30
Vishal Sharma
ba95ca682b chore: update posthog-js to 1.160.3 (#5869) 2024-09-05 17:00:33 +05:30
Vishal Sharma
5942c758f0 fix: broken links (#5867)
* fix: broken links

* Update eks-monitorUsingDashboard.md

* Update eks-monitorUsingDashboard.md

---------

Co-authored-by: CheetoDa <31571545+Calm-Rock@users.noreply.github.com>
2024-09-05 17:00:18 +05:30
Yunus M
e97d0ea51c Feat: alert history (#5774)
* feat: tabs and filters for alert history page (#5655)

* feat: alert history page route and component setup

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* chore: unused components and files cleanup

* chore: improve alert history and overview route paths

* chore: use parent selector in scss files

* chore: alert -> alerts

* feat: alert rule details metadata header (#5675)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* chore: unused components and files cleanup

* feat: copy to clipboard component

* feat: see more component

* feat: key value label component

* feat: alert rule details meta data header

* fix: apply the missing changes

* chore: uncomment the alert status with static data

* chore: compress the alert status svg icons and define props, types, and defaultProps

* feat: alert rule history skeleton using static data (#5688)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* feat: top contributors UI using static data

* feat: avg. resolution time and total triggered stats card UI using static data

* feat: tabs component

* feat: timeline tabs and filters

* feat: overall status graph UI using dummy data with graph placeholder

* feat: timeline table and pagination UI using dummy data

* fix: bugfix in reset tabs

* feat: add popover to go to logs/traces to top contributors and timeline table

* chore: remove comments

* chore: rename AlertIcon to AlertState

* fix: add cursor pointer to timeline table rows

* feat: add parent tabs to alert history

* chore: add icon to the configure tab

* fix: display popover on hovering the more button in see more component

* fix: wrap key value label

* feat: alert rule history enable/disable toggle UI

* Feat: get alert history data from API (#5718)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* feat: data state renderer component

* feat: get total triggered and avg. resolution cards data from API

* fix: hide stats card if we get NaN

* chore: improve rule stats types

* feat: get top contributors data from API

* feat: get timeline table data from API

* fix: properly render change percentage indicator

* feat: total triggered and avg resolution empty states

* fix: fix stats height issue that would cause short border-right in empty case

* feat: top contributors empty state

* fix: fix table and graph borders

* feat: build alert timeline labels filter and handle client side filtering

* fix: select the first tab on clicking reset

* feat: set param and send in payload on clicking timeline filter tabs

* Feat: alert history timeline remaining subtasks except graphs (#5720)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* feat: implement timeline table sorting

* chore: add initial count to see more and alert labels

* chore: move PaginationInfoText component to /periscope

* chore: implement top contributor rows using Ant Table

* feat: top contributors view all

* fix: hide border for last row and prevent layout shift in top contributors by specifying height

* feat: properly display duration in average resolution time

* fix: properly display normal alert rule state

* feat: add/remove view all top contributors param to url on opening/closing view all

* feat: calculate start and end time from relative time and add/remove param to url

* fix: fix console warnings

* fix: enable timeline table query only if start and end times exist

* feat: handle enable/disable alert rule toggle request

* chore: replace string values with constants

* fix: hide stats card if only past data is available + remove unnecessary states from AlertState

* fix: redirect configure alert rule to alert overview tab

* fix: display total triggers in timeline chart wrapper based on API response data

* fix: choosing the same relative time doesn't udpate start and end time

* Feat: total triggered and avg. resolution time graph (#5750)

* feat: alert history basic tabs and fitlers UI

* feat: route based tabs for alert history and overview and improve the UI to match designs

* feat: handle enable/disable alert rule toggle request

* feat: stats card line chart

* fix: overall improvements to stats card graph

* fix: overall UI improvements to match the Figma screens

* chore: remove duplicate hook

* fix: make the changes w.r.t timeline table API changes to prevent breaking the page

* fix: update stats card null check based on updated API response

* feat: stats card no previous data UI

* feat: redirect to 404 page if rule id is invalid

* chore: improve alert enable toggle success toast message

* feat: get top contributors row and timeline table row related logs and traces links from API

* feat: get total items from API and make pagination work

* feat: implement timeline filters based on API response

* fix: in case of current and target units, convert the value unit in timeline table

* fix: timeline table y axis unit null check

* fix: hide stats card graph if only a single entry is there in timeseries

* chore: redirect alert from all alerts to overview tab

* fix: prevent adding extra unnecessary params on clicking alerts top level tabs

* chore: use conditional alert popover in timeline table and import the scss file

* fix: prevent infinity if we receive totalPastTriggers as '0'

* fix: improve UI to be pixel perfect based on figma designs

* fix: fix the incorrect change direction

* fix: add height to top contributors row

* feat: alert history light mode

* fix: remove the extra padding from alert overview query builder tabs

* chore: overall improvements

* chore: remove mock file

* fix: overall improvements

* fix: add dark mode support for top contributors empty state

* chore: improve timeline chart placeholder bg in light mode

* Feat: alert history horizontal timeline chart (#5773)

* feat: timeline horizontal chart

* fix: remove the labels from horizontal timeline chart

* chore: add null check to timeline chart

* chore: hide cursor from timeline chart

* fix: fix the blank container being displayed in loading state

* fix: alert history UI fixes (#5776)

* fix: remove extra padding from alert overview query section tabs

* fix: add padding to alert overview container

* fix: improve breadcrumb click behavior

* chore: temporarily hide reset button from alert details timepicker

* fix: improve breadcrumb click behavior

* chore: hide alert firing since

* fix: don't use the data state renderer for timeline table

* fix: alert history pr review changes (#5778)

* chore: rename alert history scss files in pascal case

* fix: use proper variables

* chore: use color variable for action button dropdown item

* chore: improve the directory structure for alert history components

* chore: move inline style to scss file and extract dropdown renderer component

* chore: use colors from Color instead of css variables inside tsx files

* chore: return null in default case

* chore: update alert details spinner tip

* chore: timelinePlugin warnings and remove file wide warning disabling

* chore: change Arial to Geist Mono in timeline plugin

* feat: alert history remaining feats (#5825)

* fix: add switch case for inactive state to alert state component

* feat: add API enabled label search similar to Query Builder

* feat: add reset button to date and time picker

* feat: add vertical timeline chart using static data

* chore: use Colors instead of hex + dummy data for 90 days

* fix: label search light mode UI

* fix: remove placeholder logic, and display vertical charts if more than 1 day

* chore: extract dayjs manipulate types to a  constant

* fix: hide the overflow of top contributors card

* fix: throw instead of return error to prevent breaking alert history page in case of error

* chore: temporarily comment alert history vertical charts

* chore: calculate start and end times from relative time and remove query params (#5828)

* chore: calculate start and end times from relative time and remove query params

* fix: hide reset button if selected time is 30m

* feat: alert history dropdown functionality (#5833)

* feat: alert history dropdown actions

* chore: use query keys from react query key constant

* fix: properly handle error states for alert rule APIs

* fix: handle dropdown state using onOpenChange to fix clicking delete not closing the dropdown

* Fix: bugfixes and overall improvements to alert history  (#5841)

* fix: don't display severity label

* chore: remove id from alert header

* chore: add tooltip to enable/disable alert toggle

* chore: update enable/disbale toast message

* fix: set default relative time to 6h if relative time is not provided

* chore: update empty top contributors text and remove configure alert

* chore: temporarily hide value column from timeline column

* fix: use correct links for logs and traces in alert popover

* fix: properly set timeline table offset

* fix: display all values in graph

* fix: resolve conflicts

* chore: remove style for value column in timeline table

* chore: temporarily hide labels search

* fix: incorrect current page in pagination info text

* chore: remove label QB search

* chore: remove value column

* chore: remove commented code

* fix: show traces button when trace link is available

* fix: display horizontal chart even for a single entry

* fix: show inactive state in horizontal similar to normal state

* fix: properly render inactive state in horizontal chart

* fix: properly handle preserving alert toggle between overview and history tabs

* feat: get page size from query param

* chore: remove commented code + minor refactor

* chore: remove tsconfi.tmp

* fix: don't add default relative time if start and times exist in the url

* feat: display date range preview for stat cards

* chore: remove custom dropdown renderer component

* Fix: UI feedback changes (#5852)

* fix: add divider before delete button

* fix: timeline section title color in lightmode

* fix: remove the extra border from alert history tabs

* fix: populate alert rule disabled state on toggling alert state (#5854)

---------

Co-authored-by: Shaheer Kochai <ashaheerki@gmail.com>
2024-09-04 20:26:10 +04:30
Srikanth Chekuri
6019b38da5 fix: use better value for threshold value in alert description (#5844) 2024-09-04 18:30:04 +05:30
CheetoDa
3544ffdcc6 chore: fixed hostmetrics dashboard link (#5851) 2024-09-04 18:24:54 +05:30
Srikanth Chekuri
be7a687088 chore: make prepare task configurable (#5806) 2024-09-04 18:09:40 +05:30
Nityananda Gohain
1066b217cb fix: fix logic for cache (#5811)
* fix: fix logic for cache

* fix: replace cache during error

* fix: add todo comment for replaceCachedData

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-09-04 10:35:13 +05:30
Vikrant Gupta
709c286086 fix: handle operator case change in api and ui (#5835) 2024-09-03 00:24:06 +05:30
Sudeep MP
e4753e6b44 Feat/fix ux copy kafka view (#5814)
* feat: add view templates option to dashboard menu

* feat: increase dropdown overlay width
Set the dropdown overlay width to 200px to provide breathing space for the dropdown button.
Added flex to wrap the dropdown button to create space between the right icon and the left elements.

* feat: add localization for messaging queues Kafka overview page

Added localization support for the messaging queues Kafka overview page.

Closes #1735

---------

Co-authored-by: Pranay Prateek <pranay@signoz.io>
2024-09-02 22:29:37 +05:30
Vikrant Gupta
6f7999acb2 chore: make the raw view the default view for logs (#5816)
* chore: make the raw view the default view for logs

* fix: jest test cases

* fix: jest test cases

* chore: fix one test case

* fix: formatting issue

* fix: formatting issue
2024-09-02 11:48:51 +05:30
Srikanth Chekuri
16738ea7e3 chore: change returned error from list to error (#5805) 2024-08-30 18:51:55 +05:30
Vikrant Gupta
6b096576ee feat: consume the new search bar (#5728)
* feat: consume the new search bar

* fix: minor css issue

* chore: address review comments

* fix: very fast typing

* chore: added inline code comments

* chore: add the changes behind FF
2024-08-30 17:50:28 +05:30
SagarRajput-7
5dc5b2e366 chore: added logeEvents in Kafka-ui (#5810)
* chore: added logeEvents in Kafka-ui

* chore: changed event logic for graph data fetch
2024-08-30 15:16:44 +05:30
243 changed files with 10453 additions and 1127 deletions

View File

@@ -28,6 +28,7 @@ import (
"go.signoz.io/signoz/ee/query-service/dao"
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
"go.signoz.io/signoz/ee/query-service/interfaces"
"go.signoz.io/signoz/ee/query-service/rules"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/model"
@@ -52,7 +53,7 @@ import (
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
rules "go.signoz.io/signoz/pkg/query-service/rules"
baserules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/query-service/utils"
"go.uber.org/zap"
@@ -81,7 +82,7 @@ type ServerOptions struct {
// Server runs HTTP api service
type Server struct {
serverOptions *ServerOptions
ruleManager *rules.Manager
ruleManager *baserules.Manager
// public http router
httpConn net.Listener
@@ -727,7 +728,7 @@ func makeRulesManager(
db *sqlx.DB,
ch baseint.Reader,
disableRules bool,
fm baseint.FeatureLookup) (*rules.Manager, error) {
fm baseint.FeatureLookup) (*baserules.Manager, error) {
// create engine
pqle, err := pqle.FromConfigPath(promConfigPath)
@@ -743,9 +744,9 @@ func makeRulesManager(
}
// create manager opts
managerOpts := &rules.ManagerOptions{
managerOpts := &baserules.ManagerOptions{
NotifierOpts: notifierOpts,
Queriers: &rules.Queriers{
Queriers: &baserules.Queriers{
PqlEngine: pqle,
Ch: ch.GetConn(),
},
@@ -757,10 +758,12 @@ func makeRulesManager(
FeatureFlags: fm,
Reader: ch,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
}
// create Manager
manager, err := rules.NewManager(managerOpts)
manager, err := baserules.NewManager(managerOpts)
if err != nil {
return nil, fmt.Errorf("rule manager error: %v", err)
}

View File

@@ -13,6 +13,7 @@ const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2"
var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{
@@ -127,6 +128,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -235,6 +243,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -357,4 +372,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -0,0 +1,71 @@
package rules
import (
"fmt"
"time"
baserules "go.signoz.io/signoz/pkg/query-service/rules"
)
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
rules := make([]baserules.Rule, 0)
var task baserules.Task
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
if opts.Rule.RuleType == baserules.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(
ruleId,
opts.Rule,
baserules.ThresholdRuleOpts{
EvalDelay: opts.ManagerOpts.EvalDelay,
},
opts.FF,
opts.Reader,
)
if err != nil {
return task, err
}
rules = append(rules, tr)
// create ch rule task for evalution
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
} else if opts.Rule.RuleType == baserules.RuleTypeProm {
// create promql rule
pr, err := baserules.NewPromRule(
ruleId,
opts.Rule,
opts.Logger,
baserules.PromRuleOpts{},
opts.Reader,
)
if err != nil {
return task, err
}
rules = append(rules, pr)
// create promql rule task for evalution
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
} else {
return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", baserules.RuleTypeProm, baserules.RuleTypeThreshold)
}
return task, nil
}
// newTask returns an appropriate group for
// rule type
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {
if taskType == baserules.TaskTypeCh {
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
}
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
}

View File

@@ -51,7 +51,7 @@
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"axios": "1.6.4",
"axios": "1.7.4",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
@@ -88,7 +88,7 @@
"lucide-react": "0.379.0",
"mini-css-extract-plugin": "2.4.5",
"papaparse": "5.4.1",
"posthog-js": "1.142.1",
"posthog-js": "1.160.3",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",

View File

@@ -0,0 +1,30 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}

View File

@@ -38,5 +38,7 @@
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"SUPPORT": "SigNoz | Support",
"DEFAULT": "Open source Observability Platform | SigNoz"
"DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview"
}

View File

@@ -0,0 +1,22 @@
{
"trialPlanExpired": "Trial Plan Expired",
"gotQuestions": "Got Questions?",
"contactUs": "Contact Us",
"upgradeToContinue": "Upgrade to Continue",
"upgradeNow": "Upgrade now to keep enjoying all the great features youve been using.",
"yourDataIsSafe": "Your data is safe with us until",
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Continue My Journey",
"needMoreTime": "Need More Time?",
"extendTrial": "Extend Trial",
"extendTrialMsgPart1": "If you have a specific reason why you were not able to finish your PoC in the trial period, please write to us on",
"extendTrialMsgPart2": "with the reason. Sometimes we can extend trial by a few days on a case by case basis",
"whyChooseSignoz": "Why choose Signoz",
"enterpriseGradeObservability": "Enterprise-grade Observability",
"observabilityDescription": "Get access to observability at any scale with advanced security and compliance.",
"continueToUpgrade": "Continue to Upgrade",
"youAreInGoodCompany": "You are in good company",
"faqs": "FAQs",
"somethingWentWrong": "Something went wrong"
}

View File

@@ -0,0 +1,30 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}

View File

@@ -50,5 +50,7 @@
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS": "SigNoz | Integrations",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
}

View File

@@ -0,0 +1,22 @@
{
"trialPlanExpired": "Trial Plan Expired",
"gotQuestions": "Got Questions?",
"contactUs": "Contact Us",
"upgradeToContinue": "Upgrade to Continue",
"upgradeNow": "Upgrade now to keep enjoying all the great features youve been using.",
"yourDataIsSafe": "Your data is safe with us until",
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Continue My Journey",
"needMoreTime": "Need More Time?",
"extendTrial": "Extend Trial",
"extendTrialMsgPart1": "If you have a specific reason why you were not able to finish your PoC in the trial period, please write to us on",
"extendTrialMsgPart2": "with the reason. Sometimes we can extend trial by a few days on a case by case basis",
"whyChooseSignoz": "Why choose Signoz",
"enterpriseGradeObservability": "Enterprise-grade Observability",
"observabilityDescription": "Get access to observability at any scale with advanced security and compliance.",
"continueToUpgrade": "Continue to Upgrade",
"youAreInGoodCompany": "You are in good company",
"faqs": "FAQs",
"somethingWentWrong": "Something went wrong"
}

View File

@@ -19,6 +19,7 @@ import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history';
import { identity, pick, pickBy } from 'lodash-es';
import posthog from 'posthog-js';
import AlertRuleProvider from 'providers/Alert';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useEffect, useState } from 'react';
@@ -236,22 +237,24 @@ function App(): JSX.Element {
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>

View File

@@ -92,6 +92,14 @@ export const CreateNewAlerts = Loadable(
() => import(/* webpackChunkName: "Create Alerts" */ 'pages/CreateAlert'),
);
export const AlertHistory = Loadable(
() => import(/* webpackChunkName: "Alert History" */ 'pages/AlertList'),
);
export const AlertOverview = Loadable(
() => import(/* webpackChunkName: "Alert Overview" */ 'pages/AlertList'),
);
export const CreateAlertChannelAlerts = Loadable(
() =>
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),

View File

@@ -2,6 +2,8 @@ import ROUTES from 'constants/routes';
import { RouteProps } from 'react-router-dom';
import {
AlertHistory,
AlertOverview,
AllAlertChannels,
AllErrors,
APIKeys,
@@ -171,6 +173,20 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'ALERTS_NEW',
},
{
path: ROUTES.ALERT_HISTORY,
exact: true,
component: AlertHistory,
isPrivate: true,
key: 'ALERT_HISTORY',
},
{
path: ROUTES.ALERT_OVERVIEW,
exact: true,
component: AlertOverview,
isPrivate: true,
key: 'ALERT_OVERVIEW',
},
{
path: ROUTES.TRACE,
exact: true,

View File

@@ -1,26 +1,20 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/create';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/rules', {
...props.data,
});
const response = await axios.post('/rules', {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default create;

View File

@@ -1,24 +1,18 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/delete';
const deleteAlerts = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.delete(`/rules/${props.id}`);
const response = await axios.delete(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.rules,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.rules,
};
};
export default deleteAlerts;

View File

@@ -1,24 +1,16 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/get';
const get = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
const response = await axios.get(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default get;

View File

@@ -1,26 +1,20 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/patch';
const patch = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.patch(`/rules/${props.id}`, {
...props.data,
});
const response = await axios.patch(`/rules/${props.id}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default patch;

View File

@@ -1,26 +1,20 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/save';
const put = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/rules/${props.id}`, {
...props.data,
});
const response = await axios.put(`/rules/${props.id}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default put;

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleStatsPayload } from 'types/api/alerts/def';
import { RuleStatsProps } from 'types/api/alerts/ruleStats';
const ruleStats = async (
props: RuleStatsProps,
): Promise<SuccessResponse<AlertRuleStatsPayload> | ErrorResponse> => {
try {
const response = await axios.post(`/rules/${props.id}/history/stats`, {
start: props.start,
end: props.end,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default ruleStats;

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTimelineGraphResponsePayload } from 'types/api/alerts/def';
import { GetTimelineGraphRequestProps } from 'types/api/alerts/timelineGraph';
const timelineGraph = async (
props: GetTimelineGraphRequestProps,
): Promise<
SuccessResponse<AlertRuleTimelineGraphResponsePayload> | ErrorResponse
> => {
try {
const response = await axios.post(
`/rules/${props.id}/history/overall_status`,
{
start: props.start,
end: props.end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default timelineGraph;

View File

@@ -0,0 +1,36 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTimelineTableResponsePayload } from 'types/api/alerts/def';
import { GetTimelineTableRequestProps } from 'types/api/alerts/timelineTable';
const timelineTable = async (
props: GetTimelineTableRequestProps,
): Promise<
SuccessResponse<AlertRuleTimelineTableResponsePayload> | ErrorResponse
> => {
try {
const response = await axios.post(`/rules/${props.id}/history/timeline`, {
start: props.start,
end: props.end,
offset: props.offset,
limit: props.limit,
order: props.order,
state: props.state,
// TODO(shaheer): implement filters
filters: props.filters,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default timelineTable;

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTopContributorsPayload } from 'types/api/alerts/def';
import { TopContributorsProps } from 'types/api/alerts/topContributors';
const topContributors = async (
props: TopContributorsProps,
): Promise<
SuccessResponse<AlertRuleTopContributorsPayload> | ErrorResponse
> => {
try {
const response = await axios.post(
`/rules/${props.id}/history/top_contributors`,
{
start: props.start,
end: props.end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default topContributors;

View File

@@ -0,0 +1,41 @@
interface ConfigureIconProps {
width?: number;
height?: number;
fill?: string;
}
function ConfigureIcon({
width,
height,
fill,
}: ConfigureIconProps): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
fill={fill}
>
<path
stroke="#C0C1C3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.333"
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
/>
<path
stroke="#C0C1C3"
strokeLinecap="round"
strokeWidth="1.333"
d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3m5.417 7.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"
/>
</svg>
);
}
ConfigureIcon.defaultProps = {
width: 16,
height: 16,
fill: 'none',
};
export default ConfigureIcon;

View File

@@ -0,0 +1,65 @@
interface LogsIconProps {
width?: number;
height?: number;
fill?: string;
strokeColor?: string;
strokeWidth?: number;
}
function LogsIcon({
width,
height,
fill,
strokeColor,
strokeWidth,
}: LogsIconProps): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
fill={fill}
>
<path
stroke={strokeColor}
strokeWidth={strokeWidth}
d="M2.917 3.208v7.875"
/>
<ellipse
cx="6.417"
cy="3.208"
stroke={strokeColor}
strokeWidth={strokeWidth}
rx="3.5"
ry="1.458"
/>
<ellipse cx="6.417" cy="3.165" fill={strokeColor} rx="0.875" ry="0.365" />
<path
stroke={strokeColor}
strokeWidth={strokeWidth}
d="M9.917 11.083c0 .645-1.567 1.167-3.5 1.167s-3.5-.522-3.5-1.167"
/>
<path
stroke={strokeColor}
strokeLinecap="round"
strokeWidth={strokeWidth}
d="M5.25 6.417v1.117c0 .028.02.053.049.057l1.652.276A.058.058 0 017 7.924v1.993"
/>
<path
stroke={strokeColor}
strokeWidth={strokeWidth}
d="M9.917 3.208v3.103c0 .046.05.074.089.05L12.182 5a.058.058 0 01.088.035l.264 1.059a.058.058 0 01-.013.053l-2.59 2.877a.058.058 0 00-.014.04v2.018"
/>
</svg>
);
}
LogsIcon.defaultProps = {
width: 14,
height: 14,
fill: 'none',
strokeColor: '#C0C1C3',
strokeWidth: 1.167,
};
export default LogsIcon;

View File

@@ -0,0 +1,39 @@
interface SeverityCriticalIconProps {
width?: number;
height?: number;
fill?: string;
stroke?: string;
}
function SeverityCriticalIcon({
width,
height,
fill,
stroke,
}: SeverityCriticalIconProps): JSX.Element {
return (
<svg
width={width}
height={height}
fill={fill}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M.99707.666056.99707 2.99939M.99707 5.33337H.991237M3.00293.666056 3.00293 2.99939M3.00293 5.33337H2.9971M5.00879.666056V2.99939M5.00879 5.33337H5.00296"
stroke={stroke}
strokeWidth="1.16667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
SeverityCriticalIcon.defaultProps = {
width: 6,
height: 6,
fill: 'none',
stroke: '#F56C87',
};
export default SeverityCriticalIcon;

View File

@@ -0,0 +1,42 @@
interface SeverityErrorIconProps {
width?: number;
height?: number;
fill?: string;
stroke?: string;
strokeWidth?: string;
}
function SeverityErrorIcon({
width,
height,
fill,
stroke,
strokeWidth,
}: SeverityErrorIconProps): JSX.Element {
return (
<svg
width={width}
height={height}
fill={fill}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.00781.957845 1.00781 2.99951M1.00781 5.04175H1.00228"
stroke={stroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
SeverityErrorIcon.defaultProps = {
width: 2,
height: 6,
fill: 'none',
stroke: '#F56C87',
strokeWidth: '1.02083',
};
export default SeverityErrorIcon;

View File

@@ -0,0 +1,46 @@
interface SeverityInfoIconProps {
width?: number;
height?: number;
fill?: string;
stroke?: string;
}
function SeverityInfoIcon({
width,
height,
fill,
stroke,
}: SeverityInfoIconProps): JSX.Element {
return (
<svg
width={width}
height={height}
fill={fill}
xmlns="http://www.w3.org/2000/svg"
>
<rect
width={width}
height={height}
rx="3.5"
fill={stroke}
fillOpacity=".2"
/>
<path
d="M7 9.33346V7.00012M7 4.66675H7.00583"
stroke={stroke}
strokeWidth="1.16667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
SeverityInfoIcon.defaultProps = {
width: 14,
height: 14,
fill: 'none',
stroke: '#7190F9',
};
export default SeverityInfoIcon;

View File

@@ -0,0 +1,42 @@
interface SeverityWarningIconProps {
width?: number;
height?: number;
fill?: string;
stroke?: string;
strokeWidth?: string;
}
function SeverityWarningIcon({
width,
height,
fill,
stroke,
strokeWidth,
}: SeverityWarningIconProps): JSX.Element {
return (
<svg
width={width}
height={height}
fill={fill}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.00732.957845 1.00732 2.99951M1.00732 5.04175H1.00179"
stroke={stroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
SeverityWarningIcon.defaultProps = {
width: 2,
height: 6,
fill: 'none',
stroke: '#FFD778',
strokeWidth: '0.978299',
};
export default SeverityWarningIcon;

View File

@@ -0,0 +1,14 @@
.reset-button {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
}
.lightMode {
.reset-button {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
}

View File

@@ -0,0 +1,11 @@
import './Filters.styles.scss';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
export function Filters(): JSX.Element {
return (
<div className="filters">
<DateTimeSelector showAutoRefresh={false} hideShareModal showResetButton />
</div>
);
}

View File

@@ -0,0 +1,145 @@
.checkbox-filter {
display: flex;
flex-direction: column;
padding: 12px;
gap: 12px;
border-bottom: 1px solid var(--bg-slate-400);
.filter-header-checkbox {
display: flex;
align-items: center;
justify-content: space-between;
.left-action {
display: flex;
align-items: center;
gap: 6px;
.title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
text-transform: capitalize;
}
}
.right-action {
display: flex;
align-items: center;
.clear-all {
font-size: 12px;
color: var(--bg-robin-500);
cursor: pointer;
}
}
}
.values {
display: flex;
flex-direction: column;
gap: 8px;
.value {
display: flex;
align-items: center;
gap: 8px;
.checkbox-value-section {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: pointer;
&.filter-disabled {
cursor: not-allowed;
.value-string {
color: var(--bg-slate-200);
}
.only-btn {
cursor: not-allowed;
color: var(--bg-slate-200);
}
.toggle-btn {
cursor: not-allowed;
color: var(--bg-slate-200);
}
}
.value-string {
}
.only-btn {
display: none;
}
.toggle-btn {
display: none;
}
.toggle-btn:hover {
background-color: unset;
}
.only-btn:hover {
background-color: unset;
}
}
.checkbox-value-section:hover {
.toggle-btn {
display: none;
}
.only-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
}
}
.value:hover {
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
}
}
.no-data {
align-self: center;
}
.show-more {
display: flex;
align-items: center;
justify-content: center;
.show-more-text {
color: var(--bg-robin-500);
cursor: pointer;
}
}
}
.lightMode {
.checkbox-filter {
border-bottom: 1px solid var(--bg-vanilla-300);
.filter-header-checkbox {
.left-action {
.title {
color: var(--bg-ink-400);
}
}
}
}
}

View File

@@ -0,0 +1,503 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './Checkbox.styles.scss';
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
import { OPERATORS } from 'constants/queryBuilder';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useMemo, useState } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'nin'];
function setDefaultValues(
values: string[],
trueOrFalse: boolean,
): Record<string, boolean> {
const defaultState: Record<string, boolean> = {};
values.forEach((val) => {
defaultState[val] = trueOrFalse;
});
return defaultState;
}
interface ICheckboxProps {
filter: IQuickFiltersConfig;
}
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { filter } = props;
const [searchText, setSearchText] = useState<string>('');
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
const {
lastUsedQuery,
currentQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const { data, isLoading } = useGetAggregateValues(
{
aggregateOperator: 'noop',
dataSource: DataSource.LOGS,
aggregateAttribute: '',
attributeKey: filter.attributeKey.key,
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
tagType: filter.attributeKey.type || '',
searchText: searchText ?? '',
},
{
enabled: isOpen,
keepPreviousData: true,
},
);
const attributeValues: string[] = useMemo(
() =>
((Object.values(data?.payload || {}).find((el) => !!el) ||
[]) as string[]).filter((val) => !isEmpty(val)),
[data?.payload],
);
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
// derive the state of each filter key here in the renderer itself and keep it in sync with staged query
// also we need to keep a note of last focussed query.
// eslint-disable-next-line sonarjs/cognitive-complexity
const currentFilterState = useMemo(() => {
let filterState: Record<string, boolean> = setDefaultValues(
attributeValues,
false,
);
const filterSync = currentQuery?.builder.queryData?.[
lastUsedQuery || 0
]?.filters?.items.find((item) => isEqual(item.key, filter.attributeKey));
if (filterSync) {
if (SELECTED_OPERATORS.includes(filterSync.op)) {
if (isArray(filterSync.value)) {
filterSync.value.forEach((val) => {
filterState[val] = true;
});
} else if (typeof filterSync.value === 'string') {
filterState[filterSync.value] = true;
} else if (typeof filterSync.value === 'boolean') {
filterState[String(filterSync.value)] = true;
} else if (typeof filterSync.value === 'number') {
filterState[String(filterSync.value)] = true;
}
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
filterState = setDefaultValues(attributeValues, true);
if (isArray(filterSync.value)) {
filterSync.value.forEach((val) => {
filterState[val] = false;
});
} else if (typeof filterSync.value === 'string') {
filterState[filterSync.value] = false;
} else if (typeof filterSync.value === 'boolean') {
filterState[String(filterSync.value)] = false;
} else if (typeof filterSync.value === 'number') {
filterState[String(filterSync.value)] = false;
}
}
} else {
filterState = setDefaultValues(attributeValues, true);
}
return filterState;
}, [
attributeValues,
currentQuery?.builder.queryData,
filter.attributeKey,
lastUsedQuery,
]);
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
const isFilterDisabled = useMemo(
() =>
(currentQuery?.builder?.queryData?.[
lastUsedQuery || 0
]?.filters?.items?.filter((item) => isEqual(item.key, filter.attributeKey))
?.length || 0) > 1,
[currentQuery?.builder?.queryData, lastUsedQuery, filter.attributeKey],
);
// variable to check if the current filter has multiple values to its name in the key op value section
const isMultipleValuesTrueForTheKey =
Object.values(currentFilterState).filter((val) => val).length > 1;
const handleClearFilterAttribute = (): void => {
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => ({
...item,
filters: {
...item.filters,
items:
idx === lastUsedQuery
? item.filters.items.filter(
(fil) => !isEqual(fil.key, filter.attributeKey),
)
: [...item.filters.items],
},
})),
},
};
redirectWithQueryBuilderData(preparedQuery);
};
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
lastUsedQuery || 0
]?.filters?.items?.some((item) => isEqual(item.key, filter.attributeKey));
const onChange = (
value: string,
checked: boolean,
isOnlyOrAllClicked: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
const query = cloneDeep(currentQuery.builder.queryData?.[lastUsedQuery || 0]);
// if only or all are clicked we do not need to worry about anything just override whatever we have
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
if (isOnlyOrAllClicked && query?.filters?.items) {
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only';
query.filters.items = query.filters.items.filter(
(q) => !isEqual(q.key, filter.attributeKey),
);
if (isOnlyOrAll === 'Only') {
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.IN),
key: filter.attributeKey,
value,
};
query.filters.items = [...query.filters.items, newFilterItem];
}
} else if (query?.filters?.items) {
if (
query.filters?.items?.some((item) => isEqual(item.key, filter.attributeKey))
) {
// if there is already a running filter for the current attribute key then
// we split the cases by which particular operator is present right now!
const currentFilter = query.filters?.items?.find((q) =>
isEqual(q.key, filter.attributeKey),
);
if (currentFilter) {
const runningOperator = currentFilter?.op;
switch (runningOperator) {
case 'in':
if (checked) {
// if it's an IN operator then if we are checking another value it get's added to the
// filter clause. example - key IN [value1, currentSelectedValue]
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
} else {
// if the current state wasn't an array we make it one and add our value
const newFilter = {
...currentFilter,
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
}
} else if (!checked) {
// if we are removing some value when the running operator is IN we filter.
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: currentFilter.value.filter((val) => val !== value),
};
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
} else {
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
}
} else {
// if not an array remove the whole thing altogether!
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
}
}
break;
case 'nin':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
if (!checked) {
// in case of array add the currentUnselectedValue to the list.
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
} else {
// in case of not an array make it one!
const newFilter = {
...currentFilter,
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
}
} else if (checked) {
// opposite of above!
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: currentFilter.value.filter((val) => val !== value),
};
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
} else {
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
}
} else {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
}
}
break;
case '=':
if (checked) {
const newFilter = {
...currentFilter,
op: getOperatorValue(OPERATORS.IN),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
} else if (!checked) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
}
break;
case '!=':
if (!checked) {
const newFilter = {
...currentFilter,
op: getOperatorValue(OPERATORS.NIN),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
} else if (checked) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
}
break;
default:
break;
}
}
} else {
// case - when there is no filter for the current key that means all are selected right now.
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.NIN),
key: filter.attributeKey,
value,
};
query.filters.items = [...query.filters.items, newFilterItem];
}
}
const finalQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
...currentQuery.builder.queryData.map((q, idx) => {
if (idx === lastUsedQuery) {
return query;
}
return q;
}),
],
},
};
redirectWithQueryBuilderData(finalQuery);
};
return (
<div className="checkbox-filter">
<section className="filter-header-checkbox">
<section className="left-action">
{isOpen ? (
<ChevronDown
size={13}
cursor="pointer"
onClick={(): void => {
setIsOpen(false);
setVisibleItemsCount(10);
}}
/>
) : (
<ChevronRight
size={13}
onClick={(): void => setIsOpen(true)}
cursor="pointer"
/>
)}
<Typography.Text className="title">{filter.title}</Typography.Text>
</section>
<section className="right-action">
{isOpen && (
<Typography.Text
className="clear-all"
onClick={handleClearFilterAttribute}
>
Clear All
</Typography.Text>
)}
</section>
</section>
{isOpen && isLoading && !attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && (
<>
<section className="search">
<Input
placeholder="Filter values"
onChange={(e): void => setSearchText(e.target.value)}
disabled={isFilterDisabled}
/>
</section>
{attributeValues.length > 0 ? (
<section className="values">
{currentAttributeKeys.map((value: string) => (
<div key={value} className="value">
<Checkbox
onChange={(e): void => onChange(value, e.target.checked, false)}
checked={currentFilterState[value]}
disabled={isFilterDisabled}
rootClassName="check-box"
/>
<div
className={cx(
'checkbox-value-section',
isFilterDisabled ? 'filter-disabled' : '',
)}
onClick={(): void => {
if (isFilterDisabled) {
return;
}
onChange(value, currentFilterState[value], true);
}}
>
{filter.customRendererForValue ? (
filter.customRendererForValue(value)
) : (
<Typography.Text
className="value-string"
ellipsis={{ tooltip: { placement: 'right' } }}
>
{value}
</Typography.Text>
)}
<Button type="text" className="only-btn">
{isSomeFilterPresentForCurrentAttribute
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only'}
</Button>
<Button type="text" className="toggle-btn">
Toggle
</Button>
</div>
</div>
))}
</section>
) : (
<section className="no-data">
<Typography.Text>No values found</Typography.Text>{' '}
</section>
)}
{visibleItemsCount < attributeValues?.length && (
<section className="show-more">
<Typography.Text
className="show-more-text"
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
>
Show More...
</Typography.Text>
</section>
)}
</>
)}
</div>
);
}

View File

@@ -0,0 +1,14 @@
import './Slider.styles.scss';
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
interface ISliderProps {
filter: IQuickFiltersConfig;
}
// not needed for now build when required
export default function Slider(props: ISliderProps): JSX.Element {
const { filter } = props;
console.log(filter);
return <div>Slider</div>;
}

View File

@@ -0,0 +1,93 @@
.quick-filters {
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid var(--bg-slate-400);
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10.5px;
border-bottom: 1px solid var(--bg-slate-400);
.left-actions {
display: flex;
align-items: center;
gap: 6px;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
}
.sync-tag {
display: flex;
padding: 5px 9px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 2px;
border: 1px solid rgba(78, 116, 248, 0.2);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-500);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
text-transform: uppercase;
}
}
.right-actions {
display: flex;
align-items: center;
gap: 12px;
.divider-filter {
width: 1px;
height: 14px;
background: #161922;
}
.sync-icon {
background-color: var(--bg-ink-500);
border: 0;
box-shadow: none;
}
}
}
}
.lightMode {
.quick-filters {
background-color: var(--bg-vanilla-100);
border-right: 1px solid var(--bg-vanilla-300);
.header {
border-bottom: 1px solid var(--bg-vanilla-300);
.left-actions {
.text {
color: var(--bg-ink-400);
}
.sync-icon {
background-color: var(--bg-vanilla-100);
}
}
.right-actions {
.sync-icon {
background-color: var(--bg-vanilla-100);
}
}
}
}
}

View File

@@ -0,0 +1,124 @@
import './QuickFilters.styles.scss';
import {
FilterOutlined,
SyncOutlined,
VerticalAlignTopOutlined,
} from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import Slider from './FilterRenderers/Slider/Slider';
export enum FiltersType {
SLIDER = 'SLIDER',
CHECKBOX = 'CHECKBOX',
}
export enum MinMax {
MIN = 'MIN',
MAX = 'MAX',
}
export enum SpecficFilterOperations {
ALL = 'ALL',
ONLY = 'ONLY',
}
export interface IQuickFiltersConfig {
type: FiltersType;
title: string;
attributeKey: BaseAutocompleteData;
customRendererForValue?: (value: string) => JSX.Element;
defaultOpen: boolean;
}
interface IQuickFiltersProps {
config: IQuickFiltersConfig[];
handleFilterVisibilityChange: () => void;
}
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
const { config, handleFilterVisibilityChange } = props;
const {
currentQuery,
lastUsedQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
// clear all the filters for the query which is in sync with filters
const handleReset = (): void => {
const updatedQuery = cloneDeep(
currentQuery?.builder.queryData?.[lastUsedQuery || 0],
);
if (!updatedQuery) {
return;
}
if (updatedQuery?.filters?.items) {
updatedQuery.filters.items = [];
}
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => ({
...item,
filters: {
...item.filters,
items: idx === lastUsedQuery ? [] : [...item.filters.items],
},
})),
},
};
redirectWithQueryBuilderData(preparedQuery);
};
const lastQueryName =
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
return (
<div className="quick-filters">
<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>
</section>
<section className="filters">
{config.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return <Checkbox filter={filter} />;
case FiltersType.SLIDER:
return <Slider filter={filter} />;
default:
return <Checkbox filter={filter} />;
}
})}
</section>
</div>
);
}

View File

@@ -0,0 +1,5 @@
.tab-title {
display: flex;
gap: 4px;
align-items: center;
}

View File

@@ -0,0 +1,41 @@
import './Tabs.styles.scss';
import { Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import { History, Table } from 'lucide-react';
import { useState } from 'react';
import { ALERT_TABS } from '../constants';
export function Tabs(): JSX.Element {
const [selectedTab, setSelectedTab] = useState('overview');
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedTab(e.target.value);
};
return (
<Radio.Group className="tabs" onChange={handleTabChange} value={selectedTab}>
<Radio.Button
className={
selectedTab === ALERT_TABS.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={ALERT_TABS.OVERVIEW}
>
<div className="tab-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedTab === ALERT_TABS.HISTORY ? 'selected_view tab' : 'tab'}
value={ALERT_TABS.HISTORY}
>
<div className="tab-title">
<History size={14} />
History
</div>
</Radio.Button>
</Radio.Group>
);
}

View File

@@ -0,0 +1,18 @@
@mixin flex-center {
display: flex;
justify-content: space-between;
align-items: center;
}
.tabs-and-filters {
@include flex-center;
margin-top: 1rem;
margin-bottom: 1rem;
.filters {
@include flex-center;
gap: 16px;
.reset-button {
@include flex-center;
}
}
}

View File

@@ -0,0 +1,16 @@
import './TabsAndFilters.styles.scss';
import { Filters } from 'components/AlertDetailsFilters/Filters';
import { Tabs } from './Tabs/Tabs';
function TabsAndFilters(): JSX.Element {
return (
<div className="tabs-and-filters">
<Tabs />
<Filters />
</div>
);
}
export default TabsAndFilters;

View File

@@ -0,0 +1,5 @@
export const ALERT_TABS = {
OVERVIEW: 'OVERVIEW',
HISTORY: 'HISTORY',
ACTIVITY: 'ACTIVITY',
} as const;

View File

@@ -21,4 +21,5 @@ export enum FeatureKeys {
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
}

View File

@@ -1,4 +1,17 @@
import { ManipulateType } from 'dayjs';
const MAX_RPS_LIMIT = 100;
export { MAX_RPS_LIMIT };
export const LEGEND = 'legend';
export const DAYJS_MANIPULATE_TYPES: { [key: string]: ManipulateType } = {
DAY: 'day',
WEEK: 'week',
MONTH: 'month',
YEAR: 'year',
HOUR: 'hour',
MINUTE: 'minute',
SECOND: 'second',
MILLISECOND: 'millisecond',
};

View File

@@ -19,4 +19,5 @@ export enum LOCALSTORAGE {
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
}

View File

@@ -8,5 +8,14 @@ export const REACT_QUERY_KEY = {
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
ALERT_RULE_DETAILS: 'ALERT_RULE_DETAILS',
ALERT_RULE_STATS: 'ALERT_RULE_STATS',
ALERT_RULE_TOP_CONTRIBUTORS: 'ALERT_RULE_TOP_CONTRIBUTORS',
ALERT_RULE_TIMELINE_TABLE: 'ALERT_RULE_TIMELINE_TABLE',
ALERT_RULE_TIMELINE_GRAPH: 'ALERT_RULE_TIMELINE_GRAPH',
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
TOGGLE_ALERT_STATE: 'TOGGLE_ALERT_STATE',
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
};

View File

@@ -22,6 +22,8 @@ const ROUTES = {
EDIT_ALERTS: '/alerts/edit',
LIST_ALL_ALERT: '/alerts',
ALERTS_NEW: '/alerts/new',
ALERT_HISTORY: '/alerts/history',
ALERT_OVERVIEW: '/alerts/overview',
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/settings/channels/new',
CHANNELS_EDIT: '/settings/channels/:id',

View File

@@ -0,0 +1,5 @@
.alert-history {
display: flex;
flex-direction: column;
gap: 24px;
}

View File

@@ -0,0 +1,22 @@
import './AlertHistory.styles.scss';
import { useState } from 'react';
import Statistics from './Statistics/Statistics';
import Timeline from './Timeline/Timeline';
function AlertHistory(): JSX.Element {
const [totalCurrentTriggers, setTotalCurrentTriggers] = useState(0);
return (
<div className="alert-history">
<Statistics
totalCurrentTriggers={totalCurrentTriggers}
setTotalCurrentTriggers={setTotalCurrentTriggers}
/>
<Timeline totalCurrentTriggers={totalCurrentTriggers} />
</div>
);
}
export default AlertHistory;

View File

@@ -0,0 +1,21 @@
.alert-popover-trigger-action {
cursor: pointer;
}
.alert-history-popover {
.ant-popover-inner {
border: 1px solid var(--bg-slate-400);
.lightMode & {
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300);
}
}
.ant-popover-arrow {
&::before {
.lightMode & {
background: var(--bg-vanilla-100);
}
}
}
}

View File

@@ -0,0 +1,115 @@
import './AlertPopover.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Popover } from 'antd';
import LogsIcon from 'assets/AlertHistory/LogsIcon';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { DraftingCompass } from 'lucide-react';
import React from 'react';
import { Link } from 'react-router-dom';
type Props = {
children: React.ReactNode;
relatedTracesLink?: string;
relatedLogsLink?: string;
};
function PopoverContent({
relatedTracesLink,
relatedLogsLink,
}: {
relatedTracesLink?: Props['relatedTracesLink'];
relatedLogsLink?: Props['relatedLogsLink'];
}): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div className="contributor-row-popover-buttons">
{!!relatedLogsLink && (
<Link
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
className="contributor-row-popover-buttons__button"
>
<div className="icon">
<LogsIcon />
</div>
<div className="text">View Logs</div>
</Link>
)}
{!!relatedTracesLink && (
<Link
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
className="contributor-row-popover-buttons__button"
>
<div className="icon">
<DraftingCompass
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.TEXT_INK_400}
/>
</div>
<div className="text">View Traces</div>
</Link>
)}
</div>
);
}
PopoverContent.defaultProps = {
relatedTracesLink: '',
relatedLogsLink: '',
};
function AlertPopover({
children,
relatedTracesLink,
relatedLogsLink,
}: Props): JSX.Element {
return (
<div className="alert-popover-trigger-action">
<Popover
showArrow={false}
placement="bottom"
color="linear-gradient(139deg, rgba(18, 19, 23, 1) 0%, rgba(18, 19, 23, 1) 98.68%)"
destroyTooltipOnHide
rootClassName="alert-history-popover"
content={
<PopoverContent
relatedTracesLink={relatedTracesLink}
relatedLogsLink={relatedLogsLink}
/>
}
trigger="click"
>
{children}
</Popover>
</div>
);
}
AlertPopover.defaultProps = {
relatedTracesLink: '',
relatedLogsLink: '',
};
type ConditionalAlertPopoverProps = {
relatedTracesLink: string;
relatedLogsLink: string;
children: React.ReactNode;
};
export function ConditionalAlertPopover({
children,
relatedTracesLink,
relatedLogsLink,
}: ConditionalAlertPopoverProps): JSX.Element {
if (relatedTracesLink || relatedLogsLink) {
return (
<AlertPopover
relatedTracesLink={relatedTracesLink}
relatedLogsLink={relatedLogsLink}
>
{children}
</AlertPopover>
);
}
return <div>{children}</div>;
}
export default AlertPopover;

View File

@@ -0,0 +1,28 @@
import { AlertRuleStats } from 'types/api/alerts/def';
import { formatTime } from 'utils/timeUtils';
import StatsCard from '../StatsCard/StatsCard';
type TotalTriggeredCardProps = {
currentAvgResolutionTime: AlertRuleStats['currentAvgResolutionTime'];
pastAvgResolutionTime: AlertRuleStats['pastAvgResolutionTime'];
timeSeries: AlertRuleStats['currentAvgResolutionTimeSeries']['values'];
};
function AverageResolutionCard({
currentAvgResolutionTime,
pastAvgResolutionTime,
timeSeries,
}: TotalTriggeredCardProps): JSX.Element {
return (
<StatsCard
displayValue={formatTime(currentAvgResolutionTime)}
totalCurrentCount={currentAvgResolutionTime}
totalPastCount={pastAvgResolutionTime}
title="Avg. Resolution Time"
timeSeries={timeSeries}
/>
);
}
export default AverageResolutionCard;

View File

@@ -0,0 +1,14 @@
.statistics {
display: flex;
justify-content: space-between;
height: 280px;
border: 1px solid var(--bg-slate-500);
border-radius: 4px;
margin: 0 16px;
}
.lightMode {
.statistics {
border: 1px solid var(--bg-vanilla-300);
}
}

View File

@@ -0,0 +1,23 @@
import './Statistics.styles.scss';
import { AlertRuleStats } from 'types/api/alerts/def';
import StatsCardsRenderer from './StatsCardsRenderer/StatsCardsRenderer';
import TopContributorsRenderer from './TopContributorsRenderer/TopContributorsRenderer';
function Statistics({
setTotalCurrentTriggers,
totalCurrentTriggers,
}: {
setTotalCurrentTriggers: (value: number) => void;
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
}): JSX.Element {
return (
<div className="statistics">
<StatsCardsRenderer setTotalCurrentTriggers={setTotalCurrentTriggers} />
<TopContributorsRenderer totalCurrentTriggers={totalCurrentTriggers} />
</div>
);
}
export default Statistics;

View File

@@ -0,0 +1,112 @@
.stats-card {
width: 21.7%;
border-right: 1px solid var(--bg-slate-500);
padding: 9px 12px 13px;
&--empty {
justify-content: normal;
}
&__title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.title {
text-transform: uppercase;
font-size: 13px;
line-height: 22px;
color: var(--bg-vanilla-400);
font-weight: 500;
}
.duration-indicator {
display: flex;
align-items: center;
gap: 4px;
.icon {
display: flex;
align-self: center;
}
.text {
text-transform: uppercase;
color: var(--text-slate-200);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.48px;
}
}
}
&__stats {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 4px;
.count-label {
color: var(--text-vanilla-100);
font-family: 'Geist Mono';
font-size: 24px;
line-height: 36px;
}
}
&__graph {
margin-top: 80px;
.graph {
width: 100%;
height: 72px;
}
}
}
.change-percentage {
width: max-content;
display: flex;
padding: 4px 8px;
border-radius: 20px;
align-items: center;
gap: 4px;
&--success {
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-500);
}
&--error {
background: rgba(229, 72, 77, 0.1);
color: var(--bg-cherry-500);
}
&--no-previous-data {
color: var(--text-robin-500);
background: rgba(78, 116, 248, 0.1);
padding: 4px 16px;
}
&__icon {
display: flex;
align-self: center;
}
&__label {
font-size: 12px;
font-weight: 500;
line-height: 16px;
}
}
.lightMode {
.stats-card {
border-color: var(--bg-vanilla-300);
&__title-wrapper {
.title {
color: var(--text-ink-400);
}
.duration-indicator {
.text {
color: var(--text-ink-200);
}
}
}
&__stats {
.count-label {
color: var(--text-ink-100);
}
}
}
}

View File

@@ -0,0 +1,158 @@
import './StatsCard.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { ArrowDownLeft, ArrowUpRight, Calendar } from 'lucide-react';
import { AlertRuleStats } from 'types/api/alerts/def';
import { calculateChange } from 'utils/calculateChange';
import StatsGraph from './StatsGraph/StatsGraph';
import {
convertTimestampToLocaleDateString,
extractDayFromTimestamp,
} from './utils';
type ChangePercentageProps = {
percentage: number;
direction: number;
duration: string | null;
};
function ChangePercentage({
percentage,
direction,
duration,
}: ChangePercentageProps): JSX.Element {
if (direction > 0) {
return (
<div className="change-percentage change-percentage--success">
<div className="change-percentage__icon">
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
</div>
<div className="change-percentage__label">
{percentage}% vs Last {duration}
</div>
</div>
);
}
if (direction < 0) {
return (
<div className="change-percentage change-percentage--error">
<div className="change-percentage__icon">
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
</div>
<div className="change-percentage__label">
{percentage}% vs Last {duration}
</div>
</div>
);
}
return (
<div className="change-percentage change-percentage--no-previous-data">
<div className="change-percentage__label">no previous data</div>
</div>
);
}
type StatsCardProps = {
totalCurrentCount?: number;
totalPastCount?: number;
title: string;
isEmpty?: boolean;
emptyMessage?: string;
displayValue?: string | number;
timeSeries?: AlertRuleStats['currentTriggersSeries']['values'];
};
function StatsCard({
displayValue,
totalCurrentCount,
totalPastCount,
title,
isEmpty,
emptyMessage,
timeSeries = [],
}: StatsCardProps): JSX.Element {
const urlQuery = useUrlQuery();
const relativeTime = urlQuery.get('relativeTime');
const { changePercentage, changeDirection } = calculateChange(
totalCurrentCount,
totalPastCount,
);
const startTime = urlQuery.get(QueryParams.startTime);
const endTime = urlQuery.get(QueryParams.endTime);
let displayTime = relativeTime;
if (!displayTime && startTime && endTime) {
const formattedStartDate = extractDayFromTimestamp(startTime);
const formattedEndDate = extractDayFromTimestamp(endTime);
displayTime = `${formattedStartDate} to ${formattedEndDate}`;
}
if (!displayTime) {
displayTime = '';
}
const formattedStartTimeForTooltip = convertTimestampToLocaleDateString(
startTime,
);
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
return (
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
<div className="stats-card__title-wrapper">
<div className="title">{title}</div>
<div className="duration-indicator">
<div className="icon">
<Calendar size={14} color={Color.BG_SLATE_200} />
</div>
{relativeTime ? (
<div className="text">{displayTime}</div>
) : (
<Tooltip
title={`From ${formattedStartTimeForTooltip} to ${formattedEndTimeForTooltip}`}
>
<div className="text">{displayTime}</div>
</Tooltip>
)}
</div>
</div>
<div className="stats-card__stats">
<div className="count-label">
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
</div>
<ChangePercentage
direction={changeDirection}
percentage={changePercentage}
duration={relativeTime}
/>
</div>
<div className="stats-card__graph">
<div className="graph">
{!isEmpty && timeSeries.length > 1 && (
<StatsGraph timeSeries={timeSeries} changeDirection={changeDirection} />
)}
</div>
</div>
</div>
);
}
StatsCard.defaultProps = {
totalCurrentCount: 0,
totalPastCount: 0,
isEmpty: false,
emptyMessage: 'No Data',
displayValue: '',
timeSeries: [],
};
export default StatsCard;

View File

@@ -0,0 +1,90 @@
import { Color } from '@signozhq/design-tokens';
import Uplot from 'components/Uplot';
import { useResizeObserver } from 'hooks/useDimensions';
import { useMemo, useRef } from 'react';
import { AlertRuleStats } from 'types/api/alerts/def';
type Props = {
timeSeries: AlertRuleStats['currentTriggersSeries']['values'];
changeDirection: number;
};
const getStyle = (
changeDirection: number,
): { stroke: string; fill: string } => {
if (changeDirection === 0) {
return {
stroke: Color.BG_ROBIN_500,
fill: 'rgba(78, 116, 248, 0.20)',
};
}
if (changeDirection > 0) {
return {
stroke: Color.BG_FOREST_500,
fill: 'rgba(37, 225, 146, 0.20)',
};
}
return {
stroke: Color.BG_CHERRY_500,
fill: ' rgba(229, 72, 77, 0.20)',
};
};
function StatsGraph({ timeSeries, changeDirection }: Props): JSX.Element {
const { xData, yData } = useMemo(
() => ({
xData: timeSeries.map((item) => item.timestamp),
yData: timeSeries.map((item) => Number(item.value)),
}),
[timeSeries],
);
const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef);
const options: uPlot.Options = useMemo(
() => ({
width: containerDimensions.width,
height: containerDimensions.height,
legend: {
show: false,
},
cursor: {
x: false,
y: false,
drag: {
x: false,
y: false,
},
},
padding: [0, 0, 2, 0],
series: [
{},
{
...getStyle(changeDirection),
points: {
show: false,
},
width: 1.4,
},
],
axes: [
{ show: false },
{
show: false,
},
],
}),
[changeDirection, containerDimensions.height, containerDimensions.width],
);
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot data={[xData, yData]} options={options} />
</div>
);
}
export default StatsGraph;

View File

@@ -0,0 +1,12 @@
export const extractDayFromTimestamp = (timestamp: string | null): string => {
if (!timestamp) return '';
const date = new Date(parseInt(timestamp, 10));
return date.getDate().toString();
};
export const convertTimestampToLocaleDateString = (
timestamp: string | null,
): string => {
if (!timestamp) return '';
return new Date(parseInt(timestamp, 10)).toLocaleString();
};

View File

@@ -0,0 +1,102 @@
import { useGetAlertRuleDetailsStats } from 'pages/AlertDetails/hooks';
import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateRenderer';
import { useEffect } from 'react';
import AverageResolutionCard from '../AverageResolutionCard/AverageResolutionCard';
import StatsCard from '../StatsCard/StatsCard';
import TotalTriggeredCard from '../TotalTriggeredCard/TotalTriggeredCard';
const hasTotalTriggeredStats = (
totalCurrentTriggers: number | string,
totalPastTriggers: number | string,
): boolean =>
(Number(totalCurrentTriggers) > 0 && Number(totalPastTriggers) > 0) ||
Number(totalCurrentTriggers) > 0;
const hasAvgResolutionTimeStats = (
currentAvgResolutionTime: number | string,
pastAvgResolutionTime: number | string,
): boolean =>
(Number(currentAvgResolutionTime) > 0 && Number(pastAvgResolutionTime) > 0) ||
Number(currentAvgResolutionTime) > 0;
type StatsCardsRendererProps = {
setTotalCurrentTriggers: (value: number) => void;
};
// TODO(shaheer): render the DataStateRenderer inside the TotalTriggeredCard/AverageResolutionCard, it should display the title
function StatsCardsRenderer({
setTotalCurrentTriggers,
}: StatsCardsRendererProps): JSX.Element {
const {
isLoading,
isRefetching,
isError,
data,
isValidRuleId,
ruleId,
} = useGetAlertRuleDetailsStats();
useEffect(() => {
if (data?.payload?.data?.totalCurrentTriggers !== undefined) {
setTotalCurrentTriggers(data.payload.data.totalCurrentTriggers);
}
}, [data, setTotalCurrentTriggers]);
return (
<DataStateRenderer
isLoading={isLoading}
isRefetching={isRefetching}
isError={isError || !isValidRuleId || !ruleId}
data={data?.payload?.data || null}
>
{(data): JSX.Element => {
const {
currentAvgResolutionTime,
pastAvgResolutionTime,
totalCurrentTriggers,
totalPastTriggers,
currentAvgResolutionTimeSeries,
currentTriggersSeries,
} = data;
return (
<>
{hasTotalTriggeredStats(totalCurrentTriggers, totalPastTriggers) ? (
<TotalTriggeredCard
totalCurrentTriggers={totalCurrentTriggers}
totalPastTriggers={totalPastTriggers}
timeSeries={currentTriggersSeries?.values}
/>
) : (
<StatsCard
title="Total Triggered"
isEmpty
emptyMessage="None Triggered."
/>
)}
{hasAvgResolutionTimeStats(
currentAvgResolutionTime,
pastAvgResolutionTime,
) ? (
<AverageResolutionCard
currentAvgResolutionTime={currentAvgResolutionTime}
pastAvgResolutionTime={pastAvgResolutionTime}
timeSeries={currentAvgResolutionTimeSeries?.values}
/>
) : (
<StatsCard
title="Avg. Resolution Time"
isEmpty
emptyMessage="No Resolutions."
/>
)}
</>
);
}}
</DataStateRenderer>
);
}
export default StatsCardsRenderer;

View File

@@ -0,0 +1,213 @@
.top-contributors-card {
width: 56.6%;
overflow: hidden;
&--view-all {
width: auto;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--bg-slate-500);
.title {
color: var(--text-vanilla-400);
font-size: 13px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
.view-all {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 0;
height: 20px;
&:hover {
background-color: transparent !important;
}
.label {
color: var(--text-vanilla-400);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
.icon {
display: flex;
}
}
}
.contributors-row {
height: 80px;
}
&__content {
.ant-table {
&-cell {
padding: 12px !important;
}
}
.contributors-row {
background: var(--bg-ink-500);
td {
border: none !important;
}
&:not(:last-of-type) td {
border-bottom: 1px solid var(--bg-slate-500) !important;
}
}
.total-contribution {
color: var(--text-robin-500);
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 500;
letter-spacing: -0.06px;
padding: 4px 8px;
background: rgba(78, 116, 248, 0.1);
border-radius: 50px;
width: max-content;
}
}
.empty-content {
margin: 16px 12px;
padding: 40px 45px;
display: flex;
flex-direction: column;
gap: 12px;
border: 1px dashed var(--bg-slate-500);
border-radius: 6px;
&__icon {
font-family: Inter;
font-size: 20px;
line-height: 26px;
letter-spacing: -0.103px;
}
&__text {
color: var(--text-vanilla-400);
line-height: 18px;
.bold-text {
color: var(--text-vanilla-100);
font-weight: 500;
}
}
&__button-wrapper {
margin-top: 12px;
.configure-alert-rule-button {
padding: 8px 16px;
border-radius: 2px;
background: var(--bg-slate-400);
border-width: 0;
color: var(--text-vanilla-100);
line-height: 24px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
}
}
}
}
.ant-popover-inner:has(.contributor-row-popover-buttons) {
padding: 0 !important;
}
.contributor-row-popover-buttons {
display: flex;
flex-direction: column;
&__button {
display: flex;
align-items: center;
gap: 6px;
padding: 12px 15px;
color: var(--text-vanilla-400);
font-size: 14px;
letter-spacing: 0.14px;
width: 160px;
cursor: pointer;
.text,
.icon {
color: var(--text-vanilla-100);
.lightMode & {
color: var(--text-ink-500);
}
}
&:hover {
background: var(--bg-slate-400);
.text,
.icon {
color: var(--text-vanilla-100);
.lightMode & {
color: var(--text-ink-500);
}
}
}
.icon {
display: flex;
}
.lightMode & {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
}
}
}
.view-all-drawer {
border-radius: 4px;
}
.lightMode {
.ant-table {
background: inherit;
}
.top-contributors-card {
&__header {
border-color: var(--bg-vanilla-300);
.title {
color: var(--text-ink-400);
}
.view-all {
.label {
color: var(--text-ink-400);
}
}
}
&__content {
.contributors-row {
background: inherit;
&:not(:last-of-type) td {
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}
}
.empty-content {
border-color: var(--bg-vanilla-300);
&__text {
color: var(--text-ink-400);
.bold-text {
color: var(--text-ink-500);
}
}
&__button-wrapper {
.configure-alert-rule-button {
background: var(--bg-vanilla-300);
color: var(--text-ink-500);
}
}
}
}
}

View File

@@ -0,0 +1,84 @@
import './TopContributorsCard.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import { ArrowRight } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import TopContributorsContent from './TopContributorsContent';
import { TopContributorsCardProps } from './types';
import ViewAllDrawer from './ViewAllDrawer';
function TopContributorsCard({
topContributorsData,
totalCurrentTriggers,
}: TopContributorsCardProps): JSX.Element {
const { search } = useLocation();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
const viewAllTopContributorsParam = searchParams.get('viewAllTopContributors');
const [isViewAllVisible, setIsViewAllVisible] = useState(
!!viewAllTopContributorsParam ?? false,
);
const isDarkMode = useIsDarkMode();
const toggleViewAllParam = (isOpen: boolean): void => {
if (isOpen) {
searchParams.set('viewAllTopContributors', 'true');
} else {
searchParams.delete('viewAllTopContributors');
}
};
const toggleViewAllDrawer = (): void => {
setIsViewAllVisible((prev) => {
const newState = !prev;
toggleViewAllParam(newState);
return newState;
});
history.push({ search: searchParams.toString() });
};
return (
<>
<div className="top-contributors-card">
<div className="top-contributors-card__header">
<div className="title">top contributors</div>
{topContributorsData.length > 3 && (
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
<div className="label">View all</div>
<div className="icon">
<ArrowRight
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
/>
</div>
</Button>
)}
</div>
<TopContributorsContent
topContributorsData={topContributorsData}
totalCurrentTriggers={totalCurrentTriggers}
/>
</div>
{isViewAllVisible && (
<ViewAllDrawer
isViewAllVisible={isViewAllVisible}
toggleViewAllDrawer={toggleViewAllDrawer}
totalCurrentTriggers={totalCurrentTriggers}
topContributorsData={topContributorsData}
/>
)}
</>
);
}
export default TopContributorsCard;

View File

@@ -0,0 +1,32 @@
import TopContributorsRows from './TopContributorsRows';
import { TopContributorsCardProps } from './types';
function TopContributorsContent({
topContributorsData,
totalCurrentTriggers,
}: TopContributorsCardProps): JSX.Element {
const isEmpty = !topContributorsData.length;
if (isEmpty) {
return (
<div className="empty-content">
<div className="empty-content__icon"></div>
<div className="empty-content__text">
Top contributors highlight the most frequently triggering group-by
attributes in multi-dimensional alerts
</div>
</div>
);
}
return (
<div className="top-contributors-card__content">
<TopContributorsRows
topContributors={topContributorsData.slice(0, 3)}
totalCurrentTriggers={totalCurrentTriggers}
/>
</div>
);
}
export default TopContributorsContent;

View File

@@ -0,0 +1,87 @@
import { Color } from '@signozhq/design-tokens';
import { Progress, Table } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
import PaginationInfoText from 'periscope/components/PaginationInfoText/PaginationInfoText';
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
function TopContributorsRows({
topContributors,
totalCurrentTriggers,
}: {
topContributors: AlertRuleTopContributors[];
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
}): JSX.Element {
const columns: ColumnsType<AlertRuleTopContributors> = [
{
title: 'labels',
dataIndex: 'labels',
key: 'labels',
width: '51%',
render: (
labels: AlertRuleTopContributors['labels'],
record,
): JSX.Element => (
<ConditionalAlertPopover
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div>
<AlertLabels labels={labels} />
</div>
</ConditionalAlertPopover>
),
},
{
title: 'progressBar',
dataIndex: 'count',
key: 'progressBar',
width: '39%',
render: (count: AlertRuleTopContributors['count'], record): JSX.Element => (
<ConditionalAlertPopover
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<Progress
percent={(count / totalCurrentTriggers) * 100}
showInfo={false}
trailColor="rgba(255, 255, 255, 0)"
strokeColor={Color.BG_ROBIN_500}
/>
</ConditionalAlertPopover>
),
},
{
title: 'count',
dataIndex: 'count',
key: 'count',
width: '10%',
render: (count: AlertRuleTopContributors['count'], record): JSX.Element => (
<ConditionalAlertPopover
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div className="total-contribution">
{count}/{totalCurrentTriggers}
</div>
</ConditionalAlertPopover>
),
},
];
return (
<Table
rowClassName="contributors-row"
rowKey={(row): string => `top-contributor-${row.fingerprint}`}
columns={columns}
showHeader={false}
dataSource={topContributors}
pagination={
topContributors.length > 10 ? { showTotal: PaginationInfoText } : false
}
/>
);
}
export default TopContributorsRows;

View File

@@ -0,0 +1,46 @@
import { Color } from '@signozhq/design-tokens';
import { Drawer } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
import TopContributorsRows from './TopContributorsRows';
function ViewAllDrawer({
isViewAllVisible,
toggleViewAllDrawer,
totalCurrentTriggers,
topContributorsData,
}: {
isViewAllVisible: boolean;
toggleViewAllDrawer: () => void;
topContributorsData: AlertRuleTopContributors[];
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
}): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<Drawer
open={isViewAllVisible}
destroyOnClose
onClose={toggleViewAllDrawer}
placement="right"
width="50%"
className="view-all-drawer"
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
title="Viewing All Contributors"
>
<div className="top-contributors-card--view-all">
<div className="top-contributors-card__content">
<TopContributorsRows
topContributors={topContributorsData}
totalCurrentTriggers={totalCurrentTriggers}
/>
</div>
</div>
</Drawer>
);
}
export default ViewAllDrawer;

View File

@@ -0,0 +1,6 @@
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
export type TopContributorsCardProps = {
topContributorsData: AlertRuleTopContributors[];
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
};

View File

@@ -0,0 +1,42 @@
import { useGetAlertRuleDetailsTopContributors } from 'pages/AlertDetails/hooks';
import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateRenderer';
import { AlertRuleStats } from 'types/api/alerts/def';
import TopContributorsCard from '../TopContributorsCard/TopContributorsCard';
type TopContributorsRendererProps = {
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
};
function TopContributorsRenderer({
totalCurrentTriggers,
}: TopContributorsRendererProps): JSX.Element {
const {
isLoading,
isRefetching,
isError,
data,
isValidRuleId,
ruleId,
} = useGetAlertRuleDetailsTopContributors();
const response = data?.payload?.data;
// TODO(shaheer): render the DataStateRenderer inside the TopContributorsCard, it should display the title and view all
return (
<DataStateRenderer
isLoading={isLoading}
isRefetching={isRefetching}
isError={isError || !isValidRuleId || !ruleId}
data={response || null}
>
{(topContributorsData): JSX.Element => (
<TopContributorsCard
topContributorsData={topContributorsData}
totalCurrentTriggers={totalCurrentTriggers}
/>
)}
</DataStateRenderer>
);
}
export default TopContributorsRenderer;

View File

@@ -0,0 +1,26 @@
import { AlertRuleStats } from 'types/api/alerts/def';
import StatsCard from '../StatsCard/StatsCard';
type TotalTriggeredCardProps = {
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
totalPastTriggers: AlertRuleStats['totalPastTriggers'];
timeSeries: AlertRuleStats['currentTriggersSeries']['values'];
};
function TotalTriggeredCard({
totalCurrentTriggers,
totalPastTriggers,
timeSeries,
}: TotalTriggeredCardProps): JSX.Element {
return (
<StatsCard
totalCurrentCount={totalCurrentTriggers}
totalPastCount={totalPastTriggers}
title="Total Triggered"
timeSeries={timeSeries}
/>
);
}
export default TotalTriggeredCard;

View File

@@ -0,0 +1,52 @@
.timeline-graph {
display: flex;
flex-direction: column;
gap: 24px;
background: var(--bg-ink-400);
padding: 12px;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
height: 150px;
&__title {
width: max-content;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #1d212d;
background: rgba(29, 33, 45, 0.5);
color: #ebebeb;
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
}
&__chart {
.chart-placeholder {
width: 100%;
height: 52px;
background: rgba(255, 255, 255, 0.1215686275);
display: flex;
align-items: center;
justify-content: center;
.chart-icon {
font-size: 2rem;
}
}
}
}
.lightMode {
.timeline-graph {
background: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-300);
&__title {
background: var(--bg-vanilla-100);
color: var(--text-ink-400);
border-color: var(--bg-vanilla-300);
}
&__chart {
.chart-placeholder {
background: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -0,0 +1,184 @@
import { Color } from '@signozhq/design-tokens';
import Uplot from 'components/Uplot';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
import { useMemo, useRef } from 'react';
import { AlertRuleTimelineGraphResponse } from 'types/api/alerts/def';
import uPlot, { AlignedData } from 'uplot';
import { ALERT_STATUS, TIMELINE_OPTIONS } from './constants';
type Props = { type: string; data: AlertRuleTimelineGraphResponse[] };
function HorizontalTimelineGraph({
width,
isDarkMode,
data,
}: {
width: number;
isDarkMode: boolean;
data: AlertRuleTimelineGraphResponse[];
}): JSX.Element {
const transformedData: AlignedData = useMemo(() => {
if (!data?.length) {
return [[], []];
}
// add a first and last entry to make sure the graph displays all the data
const FIVE_MINUTES_IN_SECONDS = 300;
const timestamps = [
data[0].start / 1000 - FIVE_MINUTES_IN_SECONDS, // 5 minutes before the first entry
...data.map((item) => item.start / 1000),
data[data.length - 1].end / 1000, // end value of last entry
];
const states = [
ALERT_STATUS[data[0].state], // Same state as the first entry
...data.map((item) => ALERT_STATUS[item.state]),
ALERT_STATUS[data[data.length - 1].state], // Same state as the last entry
];
return [timestamps, states];
}, [data]);
const options: uPlot.Options = useMemo(
() => ({
width,
height: 85,
cursor: { show: false },
axes: [
{
gap: 10,
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
},
{ show: false },
],
legend: {
show: false,
},
padding: [null, 0, null, 0],
series: [
{
label: 'Time',
},
{
label: 'States',
},
],
plugins:
transformedData?.length > 1
? [
timelinePlugin({
count: transformedData.length - 1,
...TIMELINE_OPTIONS,
}),
]
: [],
}),
[width, isDarkMode, transformedData],
);
return <Uplot data={transformedData} options={options} />;
}
const transformVerticalTimelineGraph = (data: any[]): any => [
data.map((item: { timestamp: any }) => item.timestamp),
Array(data.length).fill(0),
Array(data.length).fill(10),
Array(data.length).fill([0, 1, 2, 3, 4, 5]),
data.map((item: { value: number }) => {
const count = Math.floor(item.value / 10);
return [...Array(count).fill(1), 2];
}),
];
const datatest: any[] = [];
const now = Math.floor(Date.now() / 1000); // current timestamp in seconds
const oneDay = 24 * 60 * 60; // one day in seconds
for (let i = 0; i < 90; i++) {
const timestamp = now - i * oneDay;
const startOfDay = timestamp - (timestamp % oneDay);
datatest.push({
timestamp: startOfDay,
value: Math.floor(Math.random() * 30) + 1,
});
}
function VerticalTimelineGraph({
isDarkMode,
width,
}: {
width: number;
isDarkMode: boolean;
}): JSX.Element {
const transformedData = useMemo(
() => transformVerticalTimelineGraph(datatest),
[],
);
const options: uPlot.Options = useMemo(
() => ({
width,
height: 90,
plugins: [heatmapPlugin()],
cursor: { show: false },
legend: {
show: false,
},
axes: [
{
gap: 10,
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
},
{ show: false },
],
series: [
{},
{
paths: (): null => null,
points: { show: false },
},
{
paths: (): null => null,
points: { show: false },
},
],
}),
[isDarkMode, width],
);
return <Uplot data={transformedData} options={options} />;
}
function Graph({ type, data }: Props): JSX.Element | null {
const graphRef = useRef<HTMLDivElement>(null);
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
if (type === 'horizontal') {
return (
<div ref={graphRef}>
<HorizontalTimelineGraph
isDarkMode={isDarkMode}
width={containerDimensions.width}
data={data}
/>
</div>
);
}
return (
<div ref={graphRef}>
<VerticalTimelineGraph
isDarkMode={isDarkMode}
width={containerDimensions.width}
/>
</div>
);
}
export default Graph;

View File

@@ -0,0 +1,33 @@
import { Color } from '@signozhq/design-tokens';
export const ALERT_STATUS: { [key: string]: number } = {
firing: 0,
inactive: 1,
normal: 1,
'no-data': 2,
disabled: 3,
muted: 4,
};
export const STATE_VS_COLOR: {
[key: string]: { stroke: string; fill: string };
}[] = [
{},
{
0: { stroke: Color.BG_CHERRY_500, fill: Color.BG_CHERRY_500 },
1: { stroke: Color.BG_FOREST_500, fill: Color.BG_FOREST_500 },
2: { stroke: Color.BG_SIENNA_400, fill: Color.BG_SIENNA_400 },
3: { stroke: Color.BG_VANILLA_400, fill: Color.BG_VANILLA_400 },
4: { stroke: Color.BG_INK_100, fill: Color.BG_INK_100 },
},
];
export const TIMELINE_OPTIONS = {
mode: 1,
fill: (seriesIdx: any, _: any, value: any): any =>
STATE_VS_COLOR[seriesIdx][value].fill,
stroke: (seriesIdx: any, _: any, value: any): any =>
STATE_VS_COLOR[seriesIdx][value].stroke,
laneWidthOption: 0.3,
showGrid: false,
};

View File

@@ -0,0 +1,67 @@
import '../Graph/Graph.styles.scss';
import useUrlQuery from 'hooks/useUrlQuery';
import { useGetAlertRuleDetailsTimelineGraphData } from 'pages/AlertDetails/hooks';
import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateRenderer';
import Graph from '../Graph/Graph';
function GraphWrapper({
totalCurrentTriggers,
}: {
totalCurrentTriggers: number;
}): JSX.Element {
const urlQuery = useUrlQuery();
const relativeTime = urlQuery.get('relativeTime');
const {
isLoading,
isRefetching,
isError,
data,
isValidRuleId,
ruleId,
} = useGetAlertRuleDetailsTimelineGraphData();
// TODO(shaheer): uncomment when the API is ready for
// const { startTime } = useAlertHistoryQueryParams();
// const [isVerticalGraph, setIsVerticalGraph] = useState(false);
// useEffect(() => {
// const checkVerticalGraph = (): void => {
// if (startTime) {
// const startTimeDate = dayjs(Number(startTime));
// const twentyFourHoursAgo = dayjs().subtract(
// HORIZONTAL_GRAPH_HOURS_THRESHOLD,
// DAYJS_MANIPULATE_TYPES.HOUR,
// );
// setIsVerticalGraph(startTimeDate.isBefore(twentyFourHoursAgo));
// }
// };
// checkVerticalGraph();
// }, [startTime]);
return (
<div className="timeline-graph">
<div className="timeline-graph__title">
{totalCurrentTriggers} triggers in {relativeTime}
</div>
<div className="timeline-graph__chart">
<DataStateRenderer
isLoading={isLoading}
isError={isError || !isValidRuleId || !ruleId}
isRefetching={isRefetching}
data={data?.payload?.data || null}
>
{(data): JSX.Element => <Graph type="horizontal" data={data} />}
</DataStateRenderer>
</div>
</div>
);
}
export default GraphWrapper;

View File

@@ -0,0 +1,123 @@
.timeline-table {
border-top: 1px solid var(--text-slate-500);
border-radius: 6px;
overflow: hidden;
margin-top: 4px;
min-height: 600px;
.ant-table {
background: var(--bg-ink-500);
&-cell {
padding: 12px 16px !important;
vertical-align: baseline;
&::before {
display: none;
}
}
&-thead > tr > th {
border-color: var(--bg-slate-500);
background: var(--bg-ink-500);
font-size: 12px;
font-weight: 500;
padding: 12px 16px 8px !important;
}
&-tbody > tr > td {
border: none;
}
}
.label-filter {
padding: 6px 8px;
border-radius: 4px;
background: var(--text-ink-400);
border-width: 0;
line-height: 18px;
& ::placeholder {
color: var(--text-vanilla-400);
font-size: 12px;
letter-spacing: 0.6px;
text-transform: uppercase;
font-weight: 500;
}
}
.alert-rule {
&-value,
&__created-at {
font-size: 14px;
color: var(--text-vanilla-400);
}
&-value {
font-weight: 500;
line-height: 20px;
}
&__created-at {
line-height: 18px;
letter-spacing: -0.07px;
}
}
.ant-table.ant-table-middle {
border-bottom: 1px solid var(--bg-slate-500);
border-left: 1px solid var(--bg-slate-500);
border-right: 1px solid var(--bg-slate-500);
border-radius: 6px;
}
.ant-pagination-item {
&-active {
display: flex;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
padding: 1px 8px;
border-radius: 2px;
background: var(--bg-robin-500);
& > a {
color: var(--text-ink-500);
line-height: 20px;
font-weight: 500;
}
}
}
.alert-history-label-search {
.ant-select-selector {
border: none;
}
}
}
.lightMode {
.timeline-table {
border-color: var(--bg-vanilla-300);
.ant-table {
background: var(--bg-vanilla-100);
&-thead {
& > tr > th {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
}
&.ant-table-middle {
border-color: var(--bg-vanilla-300);
}
}
.alert-history-label-search {
.ant-select-selector {
background: var(--bg-vanilla-200);
}
}
.alert-rule {
&-value,
&-created-at {
color: var(--text-ink-400);
}
}
.ant-pagination-item {
&-active > a {
color: var(--text-vanilla-100);
}
}
}
}

View File

@@ -0,0 +1,56 @@
import './Table.styles.scss';
import { Table } from 'antd';
import {
useGetAlertRuleDetailsTimelineTable,
useTimelineTable,
} from 'pages/AlertDetails/hooks';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { timelineTableColumns } from './useTimelineTable';
function TimelineTable(): JSX.Element {
const {
isLoading,
isRefetching,
isError,
data,
isValidRuleId,
ruleId,
} = useGetAlertRuleDetailsTimelineTable();
const { timelineData, totalItems } = useMemo(() => {
const response = data?.payload?.data;
return {
timelineData: response?.items,
totalItems: response?.total,
};
}, [data?.payload?.data]);
const { paginationConfig, onChangeHandler } = useTimelineTable({
totalItems: totalItems ?? 0,
});
const { t } = useTranslation('common');
if (isError || !isValidRuleId || !ruleId) {
return <div>{t('something_went_wrong')}</div>;
}
return (
<div className="timeline-table">
<Table
rowKey={(row): string => `${row.fingerprint}-${row.value}-${row.unixMilli}`}
columns={timelineTableColumns()}
dataSource={timelineData}
pagination={paginationConfig}
size="middle"
onChange={onChangeHandler}
loading={isLoading || isRefetching}
/>
</div>
);
}
export default TimelineTable;

View File

@@ -0,0 +1,9 @@
import {
AlertRuleTimelineTableResponse,
AlertRuleTimelineTableResponsePayload,
} from 'types/api/alerts/def';
export type TimelineTableProps = {
timelineData: AlertRuleTimelineTableResponse[];
totalItems: AlertRuleTimelineTableResponsePayload['data']['total'];
};

View File

@@ -0,0 +1,54 @@
import { EllipsisOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
import { formatEpochTimestamp } from 'utils/timeUtils';
export const timelineTableColumns = (): ColumnsType<AlertRuleTimelineTableResponse> => [
{
title: 'STATE',
dataIndex: 'state',
sorter: true,
width: 140,
render: (value): JSX.Element => (
<div className="alert-rule-state">
<AlertState state={value} showLabel />
</div>
),
},
{
title: 'LABELS',
dataIndex: 'labels',
render: (labels): JSX.Element => (
<div className="alert-rule-labels">
<AlertLabels labels={labels} />
</div>
),
},
{
title: 'CREATED AT',
dataIndex: 'unixMilli',
width: 200,
render: (value): JSX.Element => (
<div className="alert-rule__created-at">{formatEpochTimestamp(value)}</div>
),
},
{
title: 'ACTIONS',
width: 140,
align: 'right',
render: (record): JSX.Element => (
<ConditionalAlertPopover
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<Button type="text" ghost>
<EllipsisOutlined className="dropdown-icon" />
</Button>
</ConditionalAlertPopover>
),
},
];

View File

@@ -0,0 +1,32 @@
.timeline-tabs-and-filters {
display: flex;
justify-content: space-between;
align-items: center;
.reset-button,
.top-5-contributors {
display: flex;
align-items: center;
gap: 10px;
}
.coming-soon {
display: inline-flex;
padding: 4px 8px;
border-radius: 20px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
justify-content: center;
align-items: center;
gap: 5px;
&__text {
color: var(--text-sienna-400);
font-size: 10px;
font-weight: 500;
letter-spacing: -0.05px;
line-height: normal;
}
&__icon {
display: flex;
}
}
}

View File

@@ -0,0 +1,90 @@
import './TabsAndFilters.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { TimelineFilter, TimelineTab } from 'container/AlertHistory/types';
import history from 'lib/history';
import { Info } from 'lucide-react';
import Tabs2 from 'periscope/components/Tabs2';
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
function ComingSoon(): JSX.Element {
return (
<div className="coming-soon">
<div className="coming-soon__text">Coming Soon</div>
<div className="coming-soon__icon">
<Info size={10} color={Color.BG_SIENNA_400} />
</div>
</div>
);
}
function TimelineTabs(): JSX.Element {
const tabs = [
{
value: TimelineTab.OVERALL_STATUS,
label: 'Overall Status',
},
{
value: TimelineTab.TOP_5_CONTRIBUTORS,
label: (
<div className="top-5-contributors">
Top 5 Contributors
<ComingSoon />
</div>
),
disabled: true,
},
];
return <Tabs2 tabs={tabs} initialSelectedTab={TimelineTab.OVERALL_STATUS} />;
}
function TimelineFilters(): JSX.Element {
const { search } = useLocation();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
const initialSelectedTab = useMemo(
() => searchParams.get('timelineFilter') ?? TimelineFilter.ALL,
[searchParams],
);
const handleFilter = (value: TimelineFilter): void => {
searchParams.set('timelineFilter', value);
history.push({ search: searchParams.toString() });
};
const tabs = [
{
value: TimelineFilter.ALL,
label: 'All',
},
{
value: TimelineFilter.FIRED,
label: 'Fired',
},
{
value: TimelineFilter.RESOLVED,
label: 'Resolved',
},
];
return (
<Tabs2
tabs={tabs}
initialSelectedTab={initialSelectedTab}
onSelectTab={handleFilter}
hasResetButton
/>
);
}
function TabsAndFilters(): JSX.Element {
return (
<div className="timeline-tabs-and-filters">
<TimelineTabs />
<TimelineFilters />
</div>
);
}
export default TabsAndFilters;

View File

@@ -0,0 +1,22 @@
.timeline {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0 16px;
&__title {
color: var(--text-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
}
.lightMode {
.timeline {
&__title {
color: var(--text-ink-400);
}
}
}

View File

@@ -0,0 +1,32 @@
import './Timeline.styles.scss';
import GraphWrapper from './GraphWrapper/GraphWrapper';
import TimelineTable from './Table/Table';
import TabsAndFilters from './TabsAndFilters/TabsAndFilters';
function TimelineTableRenderer(): JSX.Element {
return <TimelineTable />;
}
function Timeline({
totalCurrentTriggers,
}: {
totalCurrentTriggers: number;
}): JSX.Element {
return (
<div className="timeline">
<div className="timeline__title">Timeline</div>
<div className="timeline__tabs-and-filters">
<TabsAndFilters />
</div>
<div className="timeline__graph">
<GraphWrapper totalCurrentTriggers={totalCurrentTriggers} />
</div>
<div className="timeline__table">
<TimelineTableRenderer />
</div>
</div>
);
}
export default Timeline;

View File

@@ -0,0 +1,2 @@
// setting to 25 hours because we want to display the horizontal graph when the user selects 'Last 1 day' from date and time selector
export const HORIZONTAL_GRAPH_HOURS_THRESHOLD = 25;

View File

@@ -0,0 +1 @@
export const TIMELINE_TABLE_PAGE_SIZE = 20;

View File

@@ -0,0 +1,3 @@
import AlertHistory from './AlertHistory';
export default AlertHistory;

View File

@@ -0,0 +1,15 @@
export enum AlertDetailsTab {
OVERVIEW = 'OVERVIEW',
HISTORY = 'HISTORY',
}
export enum TimelineTab {
OVERALL_STATUS = 'OVERALL_STATUS',
TOP_5_CONTRIBUTORS = 'TOP_5_CONTRIBUTORS',
}
export enum TimelineFilter {
ALL = 'ALL',
FIRED = 'FIRED',
RESOLVED = 'RESOLVED',
}

View File

@@ -78,6 +78,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isCloudUserVal = isCloudUser();
const showAddCreditCardModal =
isLoggedIn &&
isChatSupportEnabled &&
isCloudUserVal &&
!isPremiumChatSupportEnabled &&
@@ -213,7 +214,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const pageTitle = t(routeKey);
const renderFullScreen =
pathname === ROUTES.GET_STARTED ||
pathname === ROUTES.WORKSPACE_LOCKED ||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
@@ -253,6 +253,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL';
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isDashboardView = (): boolean => {
/**
* need to match using regex here as the getRoute function will not work for
@@ -279,6 +281,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
/**
* Note: Right now we don't have a page-level method to pass the sidebar collapse state.
* Since the use case for overriding is not widely needed, we are setting it here
* so that the workspace locked page will have an expanded sidebar regardless of how users
* have set it or what is stored in localStorage. This will not affect the localStorage config.
*/
const isWorkspaceLocked = pathname === ROUTES.WORKSPACE_LOCKED;
return (
<Layout
className={cx(
@@ -323,7 +333,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
licenseData={licenseData}
isFetching={isFetching}
onCollapse={onCollapse}
collapsed={collapsed}
collapsed={isWorkspaceLocked ? false : collapsed}
/>
)}
<div
@@ -341,6 +351,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView() ||
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues()
? 0
: '0 1rem',

View File

@@ -42,6 +42,10 @@
display: flex;
align-items: center;
}
.ant-tabs-tab-btn {
padding: 0 !important;
}
}
.lightMode {

View File

@@ -19,6 +19,7 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
@@ -369,7 +370,7 @@ function FormAlertRules({
});
// invalidate rule in cache
ruleCache.invalidateQueries(['ruleId', ruleId]);
ruleCache.invalidateQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
// eslint-disable-next-line sonarjs/no-identical-functions
setTimeout(() => {

View File

@@ -22,6 +22,7 @@ import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import EmptyWidget from '../EmptyWidget';
import { MenuItemKeys } from '../WidgetHeader/contants';
import { GridCardGraphProps } from './types';
import { isDataAvailableByPanelType } from './utils';
import WidgetGraphComponent from './WidgetGraphComponent';
function GridCardGraph({
@@ -34,6 +35,7 @@ function GridCardGraph({
onClickHandler,
onDragSelect,
customTooltipElement,
dataAvailable,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@@ -180,6 +182,11 @@ function GridCardGraph({
onError: (error) => {
setErrorMessage(error.message);
},
onSettled: (data) => {
dataAvailable?.(
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
);
},
},
);

View File

@@ -44,6 +44,7 @@ export interface GridCardGraphProps {
version?: string;
onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
dataAvailable?: (isDataAvailable: boolean) => void;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -1,6 +1,8 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { LOCALSTORAGE } from 'constants/localStorage';
import { PANEL_TYPES } from 'constants/queryBuilder';
import getLabelName from 'lib/getLabelName';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData } from 'types/api/widgets/getQuery';
import { LegendEntryProps } from './FullView/types';
@@ -131,3 +133,21 @@ export const toggleGraphsVisibilityInChart = ({
lineChartRef?.current?.toggleGraph(index, showLegendData);
});
};
export const isDataAvailableByPanelType = (
data?: MetricRangePayloadProps['data'],
panelType?: string,
): boolean => {
const getPanelData = (): any[] | undefined => {
switch (panelType) {
case PANEL_TYPES.TABLE:
return (data?.result?.[0] as any)?.table?.rows;
case PANEL_TYPES.LIST:
return data?.newResult?.data?.result?.[0]?.list as any[];
default:
return data?.result;
}
};
return Boolean(getPanelData()?.length);
};

View File

@@ -438,6 +438,10 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
: true,
[selectedDashboard],
);
let isDataAvailableInAnyWidget = false;
const isLogEventCalled = useRef<boolean>(false);
return isDashboardEmpty ? (
<DashboardEmptyState />
) : (
@@ -468,6 +472,15 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
const rowWidgetProperties = currentPanelMap[id] || {};
let { title } = currentWidget;
if (rowWidgetProperties.collapsed) {
const widgetCount = rowWidgetProperties.widgets?.length || 0;
const collapsedText = `(${widgetCount} widget${
widgetCount > 1 ? 's' : ''
})`;
title += ` ${collapsedText}`;
}
return (
<CardContainer
className="row-card"
@@ -485,9 +498,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
cursor="move"
/>
)}
<Typography.Text className="section-title">
{currentWidget.title}
</Typography.Text>
<Typography.Text className="section-title">{title}</Typography.Text>
{rowWidgetProperties.collapsed ? (
<ChevronDown
size={14}
@@ -516,6 +527,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
);
}
const checkIfDataExists = (isDataAvailable: boolean): void => {
if (!isDataAvailableInAnyWidget && isDataAvailable) {
isDataAvailableInAnyWidget = true;
}
if (!isLogEventCalled.current && isDataAvailableInAnyWidget) {
isLogEventCalled.current = true;
logEvent('Dashboard Detail: Panel data fetched', {
isDataAvailableInAnyWidget,
});
}
};
return (
<CardContainer
className={isDashboardLocked ? '' : 'enable-resize'}
@@ -534,6 +557,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
variables={variables}
version={selectedDashboard?.data?.version}
onDragSelect={onDragSelect}
dataAvailable={checkIfDataExists}
/>
</Card>
</CardContainer>

View File

@@ -40,6 +40,7 @@ const GridPanelSwitch = forwardRef<
data: panelData,
query,
thresholds,
sticky: true,
},
[PANEL_TYPES.LIST]: null,
[PANEL_TYPES.PIE]: null,

View File

@@ -23,6 +23,7 @@ function GridTableComponent({
thresholds,
columnUnits,
tableProcessedDataRef,
sticky,
...props
}: GridTableComponentProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
@@ -146,6 +147,7 @@ function GridTableComponent({
loading={false}
columns={newColumnData}
dataSource={dataSource}
sticky={sticky}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>

View File

@@ -13,6 +13,7 @@ export type GridTableComponentProps = {
thresholds?: ThresholdProps[];
columnUnits?: ColumnUnit;
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
sticky?: TableProps<RowData>['sticky'];
} & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@@ -139,7 +139,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
params.set(QueryParams.ruleId, record.id.toString());
setEditLoader(false);
history.push(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
})
.catch(handleError)
.finally(() => setEditLoader(false));

View File

@@ -67,7 +67,6 @@ export function TableViewActions(
);
const [isOpen, setIsOpen] = useState<boolean>(false);
const textToCopy = fieldData.value;
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(fieldData.value);
@@ -89,6 +88,17 @@ export function TableViewActions(
: { __html: '' };
const fieldFilterKey = filterKeyForField(fieldData.field);
let textToCopy = fieldData.value;
// remove starting and ending quotes from the value
try {
textToCopy = textToCopy.replace(/^"|"$/g, '');
} catch (error) {
console.error(
'Failed to remove starting and ending quotes from the value',
error,
);
}
return (
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>

View File

@@ -1,5 +1,6 @@
import './LogsExplorerQuerySection.styles.scss';
import { FeatureKeys } from 'constants/features';
import {
initialQueriesMap,
OPERATORS,
@@ -9,11 +10,13 @@ import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useFeatureFlags from 'hooks/useFeatureFlag';
import {
prepareQueryWithDefaultTimestamp,
SELECTED_VIEWS,
@@ -86,15 +89,26 @@ function LogExplorerQuerySection({
[handleChangeQueryData],
);
const isSearchV2Enabled =
useFeatureFlags(FeatureKeys.QUERY_BUILDER_SEARCH_V2)?.active || false;
return (
<>
{selectedView === SELECTED_VIEWS.SEARCH && (
<div className="qb-search-view-container">
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
{isSearchV2Enabled ? (
<QueryBuilderSearchV2
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
) : (
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
)}
</div>
)}

View File

@@ -30,6 +30,7 @@ function LogsExplorerTable({
queryTableData={data}
loading={isLoading}
rootClassName="logs-table"
sticky
/>
);
}

View File

@@ -114,6 +114,7 @@ function TopOperationMetrics(): JSX.Element {
loading={isLoading}
renderColumnCell={renderColumnCell}
downloadOption={topOperationMetricsDownloadOptions}
sticky
/>
);
}

View File

@@ -1,6 +1,6 @@
### Step 1: Download/Copy this hostmetrics JSON file
Download/Copy the `hostmetrics-with-variable.json` from [here](https://github.com/SigNoz/dashboards/blob/main/hostmetrics/hostmetrics-with-variable.json)
Download/Copy the `hostmetrics.json` from [here](https://github.com/SigNoz/dashboards/blob/main/hostmetrics/hostmetrics.json)
&nbsp;
&nbsp;

View File

@@ -1,6 +1,6 @@
### Step 1: Download/Copy this hostmetrics JSON file
Download/Copy the `hostmetrics-with-variable.json` from [here](https://github.com/SigNoz/dashboards/blob/main/hostmetrics/hostmetrics-with-variable.json)
Download/Copy the `hostmetrics.json` from [here](https://github.com/SigNoz/dashboards/blob/main/hostmetrics/hostmetrics.json)
&nbsp;
&nbsp;

View File

@@ -1,6 +1,6 @@
### Step 1: Download/Copy this hostmetrics JSON file
Download/Copy the `hostmetrics-with-variable.json` from [here](https://github.com/SigNoz/dashboards/blob/main/hostmetrics/hostmetrics-with-variable.json)
Download/Copy the `hostmetrics.json` from [here](https://github.com/SigNoz/dashboards/blob/main/hostmetrics/hostmetrics.json)
&nbsp;
&nbsp;

Some files were not shown because too many files have changed in this diff Show More