Compare commits

..

71 Commits

Author SHA1 Message Date
Yunus M
f13b083277 feat: api / ingestion keys - initial commit 2024-02-03 02:44:29 +05:30
Vikrant Gupta
efc7aff7a2 fix: loading states for list log view (#4486) 2024-02-02 17:25:07 +05:30
Vikrant Gupta
f04937bbd1 feat: support custom times unique to pages new design changes (#4485) 2024-02-02 17:05:46 +05:30
Vikrant Gupta
fce66a2c5c fix: logs explorer issues (#4483)
* fix: logs explorer issues

* fix: jest test cases
2024-02-02 16:58:58 +05:30
Yunus M
3b4fb3eb80 UI feedback updates (#4482)
* feat: handle loading and fix ui issues

* feat: ui updates
2024-02-02 14:52:28 +05:30
Yunus M
e3ddd0470e feat: handle loading states and incorporate ui feedback (#4479) 2024-02-02 11:14:55 +05:30
Vikrant Gupta
e88b6e400d fix: update TODO and remove extra braces 2024-01-31 22:35:12 +05:30
Vikrant Gupta
857a1e9356 fix: jest config issues 2024-01-31 19:32:30 +05:30
Yunus M
5c0096623c chore: fix tsc issues 2024-01-31 16:34:14 +05:30
Rajat Dabade
32380d5688 [Feat]: View in Traces (#4450)
* refactor: datetime selector beside run query removed add to dashboard

* refactor: added tab for traces view details page

* refactor: done with the save view in traces

* fix: the gittery effect when navigatigating from views

* refactor: view tab view title light mode support

* refactor: removed console

* fix: gittery effect when switch view from views tabs

* refactor: separate traces routes

* refactor: remove query params
2024-01-31 16:16:53 +05:30
Vikrant Gupta
55a0cbc3f1 fix: switching between logs display tabs (#4457) 2024-01-31 16:16:53 +05:30
Rajat Dabade
d319ce99d6 [Refactor]: Tab Switch deplay issue and UI improvement for Clickhouse (#4409) 2024-01-31 16:16:49 +05:30
Vikrant Gupta
dc74592347 feat: added loading and error states for logs design (#4452)
* feat: added loading and error states for logs design

* feat: added error states for table view and time series view

* feat: handle error and loading states

* feat: loading states
2024-01-31 16:16:13 +05:30
Yunus M
5ef07a229d feat: update ui (#4449) 2024-01-31 16:16:13 +05:30
Vikrant Gupta
17b4e69f8b feat: [GH-4436]: date range enhancements (#4448)
* feat: [GH-4436]: when selecting custom time range it should be from start of day to end of date

* fix: custom time width and refresh text visibility issues (#4428)

---------

Co-authored-by: Yunus M <myounis.ar@live.com>
2024-01-31 16:16:12 +05:30
Rajat Dabade
aecd10ef24 refactor: New design for Save views. (#4435) 2024-01-31 16:16:12 +05:30
Vikrant Gupta
7f0574061f fix: logs UI improvements (#4426)
* fix: remove fixed times from the date time picker v2

* fix: added old logs explorer CTA in new designs

* feat: handle active logs indicator update

* fix: address review comments

* fix: old logs explorer page

* fix: remove info text and add relative time buttons

* fix: update logs explorer tab designs

* fix: update logs explorer tab designs

* fix: update logs explorer tab designs
2024-01-31 16:16:12 +05:30
Vikrant Gupta
71266836f8 feat: add raw view attributes in the logs list view (#4422)
* feat: add raw view attributes in the logs list view

* feat: add raw view attributes in the logs list view

* fix: raw attributes
2024-01-31 16:16:12 +05:30
Yunus M
4d4cd72ab2 feat: update styles for logs detail view (#4407)
* feat: update styles for logs detail view

* feat: update styles for logs detail view
2024-01-31 16:16:12 +05:30
Rajat Dabade
b3f1fdefe6 feat: save view for new design (#4392)
* feat: save view for new design

* refactor: done with save view
2024-01-31 16:16:12 +05:30
Rajat Dabade
5fea20f4e7 [Refactor]: New design for Log details page (#4362)
New design for Log details page 

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
Co-authored-by: Yunus M <myounis.ar@live.com>
2024-01-31 16:16:12 +05:30
Yunus M
195035f035 Settings theme change (#4368)
* feat: settings theme change
2024-01-31 16:16:08 +05:30
Vikrant Gupta
5c283a3013 chore: styles improvement across new design (#4389)
* fix: improve date time styles

* feat: table view changes according to new design

* fix: button visibility in clickhouse and promQL headers (#4390)

* feat: change the tabs to new design buttons for query builder
2024-01-31 16:15:24 +05:30
Vikrant Gupta
b81d3598bc feat: handle new typing changes for date time picker v2 (#4386)
Co-authored-by: Yunus M <myounis.ar@live.com>
2024-01-31 16:15:20 +05:30
Vikrant Gupta
01f1bee3cd fix: virtuoso scroll refresh issue 2024-01-31 16:12:17 +05:30
Vikrant Gupta
077d23e7b4 feat: handle light theme 2024-01-31 16:12:17 +05:30
Vikrant Gupta
212a36a233 feat: handle light theme 2024-01-31 16:12:17 +05:30
Vikrant Gupta
d296ea9574 feat: handle qb design changes across the application 2024-01-31 16:12:17 +05:30
Vikrant Gupta
065bc3d55d fix: remove dangling border after element removal 2024-01-31 16:12:17 +05:30
Vikrant Gupta
e60f430d66 feat: integrate date time selector across app 2024-01-31 16:12:17 +05:30
Vikrant Gupta
fca51d3d06 feat: date time custom time modal to render inside the new popover (#4366)
* feat: single calender for range picker

* fix: edgecases
2024-01-31 16:12:12 +05:30
Vikrant Gupta
630bf74e4f feat: handle light theme for logs explorer design changes (#4363)
* feat: handle light theme for list tables and dateTime selection

* feat: handle light theme for popover

* fix: address review comments
2024-01-31 16:11:50 +05:30
Vikrant Gupta
109e4e0bed fix: type errors (#4360) 2024-01-31 16:11:50 +05:30
Vikrant Gupta
7b5bb46b84 fix: eslint error 2024-01-31 16:11:50 +05:30
Vikrant Gupta
bc5e409c73 feat: new table view for logs explorer list section (#4353)
* feat: table view changes for logs list

* feat: code refactor to support log line actions

* feat: code refactor to support log line actions

* fix: the positioning of the btns

* feat: fix the table onclick

* fix: header issue

* fix: on hover

* fix: type issue
2024-01-31 16:11:50 +05:30
Yunus M
ccc7cb5b42 fix: lint errors 2024-01-31 16:11:50 +05:30
Yunus M
9423531db1 Query builder design update (#4359)
* feat: QB design update

* fix: add functionality and light mode styles

* fix: ts issues

* fix: update all css color variables to correct names
2024-01-31 16:11:50 +05:30
Vikrant Gupta
3ec56c12dd feat: logs list view changes (#4348)
* feat: logs list view changes

* fix: list view and toolbar styles

* feat: side btns

* feat: added auto refresh handler

* feat: handle popover close for btn click date time

* feat: extract the common log actions btn component

* feat: update the button for log line actions

* fix: event propagation from context button

* feat: use styles from ui-library
2024-01-31 16:11:47 +05:30
Vikrant Gupta
27b4b98ed0 feat: added new toolbar for logs explorer (#4336) 2024-01-31 16:11:15 +05:30
Yunus M
4eb3a4c570 feat: update styles 2024-01-31 16:11:11 +05:30
Yunus M
cb2aec9139 feat: logs explorer - new design 2024-01-31 16:10:44 +05:30
Yunus M
5c60a862e5 fix: update only showTotal property by spreading pagination object (#4467)
* fix: update only showTotal property by spreading pagination object
2024-01-31 15:59:40 +05:30
Yunus M
78c9330666 fix: set light bg for full screen in dashboard (#4465) 2024-01-31 14:25:27 +05:30
Vikrant Gupta
01fc7a7fd4 fix: [GH-4451]: custom time range modal closed on focussing closed date (#4456)
* fix: [GH-4451]: custom time range modal closed on focussing closed date

* fix: jest test
2024-01-30 18:50:02 +05:30
Yunus M
0200fb3a21 fix: close delete modal on delete success (#4459) 2024-01-30 16:47:58 +05:30
Yunus M
e977963763 feat: show total items count in table (#4453) 2024-01-29 18:21:51 +05:30
Yunus M
824d9aaf85 feat: users should choose either to broadcast all or enter specific channels to alert (#4441)
* feat: users should choose either to broadcast all or enter specific channels to alert

* fix: remove console logs
2024-01-29 11:12:41 +05:30
Srikanth Chekuri
4db3e5e542 chore: include status (#4447) 2024-01-29 00:42:19 +05:30
Yunus M
a8b293a510 fix: variable selection flow - dependent variable option not updated … (#4438)
* fix: variable selection flow - dependent variable option not updated on change

* fix: dropdown width and parent element update

* fix: add key to variable item inputs
2024-01-27 12:59:28 +05:30
Srikanth Chekuri
4a4f48cec8 chore: support p9{9,5,0},75,50 for space aggregation (#4382) 2024-01-26 17:07:23 +05:30
Vikrant Gupta
7e5cf65ea3 fix: [GH-4434]: dashboard variables performance issues (#4437) 2024-01-25 23:12:19 +05:30
Keshav Gupta
bb7417ffbd fix:edit the nameon sign up page if name is blank (#4216)
Co-authored-by: keshav <keshav.gupta@jarvis.consulting>
2024-01-25 19:42:14 +05:30
Raj Kamal Singh
085cf34a49 fix: Logs UI: querybuildersearch: avoid emptying out query on sourceKeys update if tags are yet to be populated (#4355)
* fix: querybuildersearch: do not call query onChange from sourceKeys useEffect if tags is empty

* chore: add comment explaining change
2024-01-25 12:35:36 +05:30
Srikanth Chekuri
be27a92fc9 chore: add functions support (#4381) 2024-01-25 01:14:45 +05:30
Vikrant Gupta
253137a6b8 fix: center align the dashboard delete modal (#4429) 2024-01-25 01:05:47 +05:30
Prashanth Banda
fce7ab7d24 fix(frontend,serviceMap): dynamically truncate service map node label (#4365) 2024-01-25 00:47:09 +05:30
Yunus M
5b0e3d375a fix: custom time width and refresh text visibility issues (#4428) 2024-01-24 16:09:32 +05:30
Rajat Dabade
9e05cb48fe refactor: fill span for full view and dashboard view (#4424)
* refactor: fill span for full view and dashboard view

* refactor: fill span works in full view and dashboard
2024-01-24 15:37:34 +05:30
Yunus M
6d67ca72a0 fix: update search logic in dashboard to search for title, description, tags (#4427) 2024-01-24 14:18:15 +05:30
Vikrant Gupta
0626081eee feat: added log attributes in the raw view old designs (#4423)
* feat: added log attributes in the raw view old designs

* feat: support it in old explorer page
2024-01-24 11:32:48 +05:30
Rajat Dabade
199d52b39f refactor: added null check while searching for dashboard (#4421)
* refactor: added null check while searching for dashboard

* refactor: flitering null value out

* chore: removed extra space

* refactor: remove unnecessary null check
2024-01-23 16:36:25 +05:30
Ankit Nayan
204cad8448 merging main 2024-01-22 23:45:18 +05:30
Yunus M
8c6096d60e fix: reset env on data source select, set logo center aligned (#4417) 2024-01-22 21:47:55 +05:30
Prashant Shahi
9de9fb5863 Merge pull request #4413 from SigNoz/release/v0.37.1
Release/v0.37.1
2024-01-22 19:54:11 +05:30
Prashant Shahi
64d854ffa2 chore(signoz): 📌 pin versions: SigNoz 0.37.1
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-01-22 19:55:04 +05:45
Prashant Shahi
6b073280a4 Merge branch 'main' into release/v0.37.1 2024-01-22 19:54:38 +05:45
Vikrant Gupta
79e6699b37 fix: logs page crash when special chars present in the value of query (#4408) 2024-01-22 19:06:33 +05:30
Rajat Dabade
d563778479 [Fix]: Resolve glitch in graph due to variable and time stamp change (#4406)
* refactor: resolve glitch in variable and time stamp change

* refactor: removed unwanted code

* refactor: updated code
2024-01-22 16:36:03 +05:30
Vikrant Gupta
255b3dd3b0 fix: back btn navigation on first load (#4405)
* fix: back btn navigation on first load

* fix: remove console logs

* chore: refactor the logic to a function and added code comments
2024-01-22 14:47:25 +05:30
Vikrant Gupta
00e97fa401 fix: trial banner moving to left screen and breaking trace detail page (#4403)
* fix: trial banner moving to left screen and breaking trace detail page

* fix: trial banner moving to left screen and breaking trace detail page
2024-01-22 13:12:02 +05:30
Prashant Shahi
9755ba6b47 Merge pull request #4400 from SigNoz/release/v0.37.0
Release/v0.37.0
2024-01-20 02:03:04 +05:30
277 changed files with 13247 additions and 2296 deletions

View File

@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.37.0
image: signoz/query-service:0.37.1
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.37.0
image: signoz/frontend:0.37.1
deploy:
restart_policy:
condition: on-failure

View File

@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.37.0}
image: signoz/query-service:${DOCKER_TAG:-0.37.1}
container_name: signoz-query-service
command:
[
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.37.0}
image: signoz/frontend:${DOCKER_TAG:-0.37.1}
container_name: signoz-frontend
restart: on-failure
depends_on:

View File

@@ -152,9 +152,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
// PAT APIs
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.createPAT)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.getPATs)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/pat/{id}", am.OpenAccess(ah.deletePAT)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.getPATs)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/pat/{id}", am.AdminAccess(ah.deletePAT)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)

View File

@@ -40,6 +40,7 @@ type billingDetails struct {
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
Details details `json:"details"`
Discount float64 `json:"discount"`
SubscriptionStatus string `json:"subscriptionStatus"`
} `json:"data"`
}

View File

@@ -22,7 +22,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -36,7 +36,9 @@
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@signozhq/design-tokens": "0.0.6",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@signozhq/design-tokens": "0.0.8",
"@uiw/react-md-editor": "3.23.5",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
@@ -75,7 +77,7 @@
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"lucide-react": "0.288.0",
"lucide-react": "0.321.0",
"mini-css-extract-plugin": "2.4.5",
"papaparse": "5.4.1",
"react": "18.2.0",

View File

@@ -0,0 +1 @@
<svg width="32" height="33" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.91 28.675c-6.199 0-12.888-3.888-12.888-12.421S9.711 3.832 15.911 3.832c3.444 0 6.621 1.134 8.977 3.2 2.555 2.267 3.91 5.466 3.91 9.222 0 3.755-1.355 6.933-3.91 9.2-2.356 2.066-5.555 3.221-8.977 3.221z" fill="url(#prefix__paint0_radial_2122_6520)"/><path d="M26.552 8.87c1.185 1.91 1.803 4.186 1.803 6.717 0 3.756-1.356 6.933-3.911 9.2-2.356 2.066-5.556 3.222-8.978 3.222-4.013 0-8.221-1.634-10.706-5.098 2.391 3.924 6.889 5.764 11.15 5.764 3.423 0 6.623-1.155 8.978-3.222 2.555-2.266 3.911-5.444 3.911-9.2 0-2.83-.771-5.346-2.247-7.383z" fill="#EB8F00"/><path d="M20.123 22.905c0 1.685-1.846 2.667-4.124 2.667-2.277 0-4.124-.989-4.124-2.667 0-1.677 1.847-3.522 4.124-3.522 2.278 0 4.124 1.838 4.124 3.522zM12.06 14.852l1.88-1.748c.267-.331.307-.778.038-1.045-.353-.355-.98-.269-1.32.136-.018.033-.03.042-.049.075l-1.333 1.938-1.804-1.682c-.027-.03-.042-.034-.067-.062-.42-.32-1.05-.267-1.315.157-.207.32-.07.745.264 1.011l2.313 1.372-1.96 1.833c-.262.326-.31.77-.04 1.044.351.358.978.276 1.32-.127.018-.033.031-.042.051-.075l1.405-2.031 1.706 1.609c.027.029.043.035.067.064.418.322 1.049.273 1.318-.149.206-.32.07-.746-.26-1.013l-2.213-1.307zM20.61 14.852l-1.879-1.748c-.267-.331-.307-.778-.036-1.045.354-.355.978-.269 1.318.136.018.033.034.042.051.075l1.334 1.938 1.806-1.682c.025-.03.04-.034.065-.062.422-.32 1.05-.267 1.317.157.205.32.067.745-.266 1.011L22 15.004l1.96 1.833c.268.33.313.775.042 1.044-.349.358-.976.276-1.318-.127-.02-.033-.033-.042-.051-.075l-1.404-2.031-1.71 1.609c-.024.029-.04.035-.066.064-.418.322-1.046.273-1.315-.149-.21-.32-.074-.746.257-1.013l2.216-1.307zM11.911 8.696c.511.044.711-.645.178-.8a4.07 4.07 0 00-1.289-.133A4.596 4.596 0 007.689 9.14c-.378.4.156.89.556.6a5.829 5.829 0 013.666-1.044zM20.044 8.696a5.85 5.85 0 013.689 1.044c.4.29.933-.2.555-.6a4.645 4.645 0 00-3.11-1.377 4.07 4.07 0 00-1.29.133.408.408 0 00-.282.504c.053.194.24.318.438.296z" fill="#422B0D"/><defs><radialGradient id="prefix__paint0_radial_2122_6520" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.91 16.254) scale(12.657)"><stop offset=".5" stop-color="#FDE030"/><stop offset=".92" stop-color="#F7C02B"/><stop offset="1" stop-color="#F4A223"/></radialGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -0,0 +1 @@
<svg width="32" height="33" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.309 13.108l-6.704-3.32s-.016-.317.284-.477c.302-.16 5.053-2.107 5.435-2.107.383 0 2.62.431 4.249.793 1.629.363 5.933 1.287 5.953 1.57.02.281-4.404 4.806-4.404 4.806l-4.813-1.265z" fill="#C3FECE"/><path d="M20.423 11.037s-2.811-.826-5.546-1.469c-1.274-.3-5.016-1.084-5.016-1.084s.398-.173.698-.3c.305-.127.547-.193.547-.193s2.44.486 4.253.873c2.453.522 5.886 1.547 5.966 1.709.082.16-.902.464-.902.464z" fill="#fff"/><path d="M14.98 10.26c-.598.415-.011.666 1.09.924 1.207.282 2.127.698 2.903.247.7-.405-1.014-.845-1.8-1.014-.6-.129-1.731-.478-2.193-.158z" fill="#ACB1B2"/><path d="M17.17 11.095c-.005 0 .02-4.869.02-5.049 0-.18-.203-.342.02-.724.222-.382.804-.342.804-.342s2.416-.702 3.38-.945c.964-.242 3.098-.804 3.098-.804l.142 1.22s-2.236.631-3.342.913c-1.107.282-2.616.745-2.616.745l-.222.202.064 4.757s-.206.231-.668.231c-.45-.002-.68-.204-.68-.204z" fill="#FFD816"/><path d="M24.095 3.855c.018.38.22.616.46.616.24 0 .404-.307.369-.707-.038-.398-.296-.58-.516-.506-.22.073-.327.32-.313.597zM18.46 6.422a.209.209 0 01-.123-.038l-1.153-.769a.225.225 0 01-.063-.309.222.222 0 01.31-.062l1.153.769a.224.224 0 01.062.309.228.228 0 01-.187.1z" fill="#FEB804"/><path d="M18.636 6.235a.225.225 0 01-.178-.089c-.295-.393-.633-.84-.693-.909a.225.225 0 01-.031-.284.222.222 0 01.309-.062c.04.027.062.042.771.986.073.098.007.238-.091.312-.04.03-.04.046-.087.046z" fill="#FEB804"/><path d="M18.365 6.609c-.01 0-.022 0-.035-.003l-1.111-.175a.221.221 0 11.069-.438l1.11.176c.12.02.225.042.205.164-.016.107-.129.276-.238.276z" fill="#FEB804"/><path d="M7.596 9.764c.353 0 3.188.744 4.65 1.013 1.463.27 5.878 1.314 6.027 1.342.149.03.12 1.94.12 1.94s2.089 10.8 2.029 11.309c-.06.506-1.431 4.415-1.431 4.415s-.807.12-2.865-.478c-2.057-.598-7.488-2.089-7.817-2.506-.329-.418-.12-5.938-.298-9.338-.182-3.402-.415-7.697-.415-7.697z" fill="#79DD8A"/><path d="M24.06 27.036c.113-.375-.518-4.402-.607-8.101-.089-3.698.229-9.324.076-9.369-.154-.042-5.256 2.553-5.256 2.553s-.022 3.671.04 7.133c.08 4.48.438 10.41.676 10.53.238.12 2.302-1.035 2.924-1.372 1.102-.598 2.058-1.074 2.147-1.374z" fill="#02AB46"/><path d="M20.408 13.82l.011-2.787.914-.45.026 3.056-.422.74-.529-.56z" fill="#DBDFE1"/><path d="M12.322 14.797c-1.973-.211-3.34 1.549-3.233 3.842.127 2.709 1.91 4.704 3.842 5.102 1.93.398 3.802-.44 3.842-3.402.044-3.087-2.669-5.353-4.451-5.542z" fill="#FEFEFD"/><path d="M13.637 17.27s-.4-1.344-1.602-.986c-1.202.357-1.853 2.973.187 4.15 1.96 1.131 3.764-.944 3.133-2.288-.574-1.227-1.718-.876-1.718-.876z" fill="#EF5B44"/><path d="M13.18 15.626c-.136.049-.243.602-.1 1.13.106.396.446.939.643.903.158-.029.278-.651.13-1.173-.174-.602-.516-.918-.674-.86z" fill="#B8CF17"/><path d="M13.15 18.746c-.564-.171-1.2 1.769-.057 2.977 1.26 1.331 2.73.158 2.69-.1-.057-.358-1.044-.615-1.53-1.215-.487-.605-.774-1.562-1.102-1.662z" fill="#FD8F01"/><path d="M11.346 18.417s.113-.849-.673-.802c-.76.046-.574.944-.574.944s-.633.076-.526.778c.08.53.64.524.64.524s-.616.242-.336.945c.249.624.822.373.822.373s-.21.609.287.93c.42.272.787.043.787.043s-.023.52.557.616c.703.115 1.007-.74.507-1.136-.38-.3-.724-.067-.724-.067s.07-.166.004-.357c-.045-.125-.116-.171-.116-.171s.616-.058.516-.758c-.1-.702-.716-.616-.716-.616s.358-.286.216-.802c-.14-.518-.671-.444-.671-.444z" fill="#A281D0"/><path d="M21.04 14.595c-.511 0-2.691-2.167-2.711-2.189a.222.222 0 01.024-.313.224.224 0 01.314.022c.14.155 1.806 1.702 2.286 2 .311-.465 1.322-2.498 2.191-4.333a.224.224 0 01.296-.107.223.223 0 01.106.296c-2.142 4.526-2.353 4.586-2.466 4.617-.013.007-.027.007-.04.007z" fill="#2D802D"/></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,19 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.36806 25.9481C5.93935 25.9481 3.15283 21.7098 3.15283 16.5002C3.15283 11.2907 5.94157 7.05238 9.36806 7.05238C12.7945 7.05238 15.5833 11.2907 15.5833 16.5002C15.5833 21.7098 12.7945 25.9481 9.36806 25.9481Z" fill="#FAFAFA"/>
<path d="M9.36815 7.49694C10.8414 7.49694 12.2524 8.38594 13.3391 10.0017C14.499 11.7241 15.139 14.0333 15.139 16.5003C15.139 18.9673 14.499 21.2764 13.3391 22.9989C12.2524 24.6146 10.8414 25.5036 9.36815 25.5036C7.89489 25.5036 6.48385 24.6146 5.39724 22.9989C4.23508 21.2764 3.59734 18.9673 3.59734 16.5003C3.59734 14.0333 4.23731 11.7241 5.39724 10.0017C6.48385 8.38594 7.89267 7.49694 9.36815 7.49694ZM9.36815 6.60794C5.69056 6.60794 2.7085 11.0374 2.7085 16.5003C2.7085 21.9632 5.69056 26.3926 9.36815 26.3926C13.0457 26.3926 16.0278 21.9632 16.0278 16.5003C16.0278 11.0374 13.0457 6.60794 9.36815 6.60794Z" fill="#B0BEC5"/>
<path d="M7.47266 15.5762C6.87269 15.0118 7.00602 13.8919 7.77487 13.0741C7.81486 13.0319 7.85486 12.9919 7.89708 12.9541C7.55488 12.7608 7.17934 12.6519 6.78381 12.6519C5.18611 12.6519 3.89062 14.414 3.89062 16.585C3.89062 18.756 5.18611 20.5182 6.78381 20.5182C8.3815 20.5182 9.67699 18.756 9.67699 16.585C9.67699 16.1962 9.63477 15.8184 9.55699 15.4629C8.83703 15.9806 7.97708 16.0495 7.47266 15.5762Z" fill="url(#paint0_linear_2122_5062)"/>
<path d="M22.6294 26.3932C26.3074 26.3932 29.289 21.9642 29.289 16.5008C29.289 11.0374 26.3074 6.60847 22.6294 6.60847C18.9514 6.60847 15.9697 11.0374 15.9697 16.5008C15.9697 21.9642 18.9514 26.3932 22.6294 26.3932Z" fill="#EEEEEE"/>
<path d="M22.6283 25.9493C19.2018 25.9493 16.4131 21.711 16.4131 16.5014C16.4131 11.2919 19.2018 7.05357 22.6283 7.05357C26.0548 7.05357 28.8435 11.2919 28.8435 16.5014C28.8435 21.711 26.057 25.9493 22.6283 25.9493Z" fill="#FAFAFA"/>
<path d="M22.6284 7.49816C24.1017 7.49816 25.5127 8.38716 26.5993 10.0029C27.7592 11.7254 28.3992 14.0345 28.3992 16.5015C28.3992 18.9685 27.7592 21.2777 26.5993 23.0001C25.5127 24.6159 24.1017 25.5049 22.6284 25.5049C21.1551 25.5049 19.7441 24.6159 18.6575 23.0001C17.4976 21.2777 16.8576 18.9685 16.8576 16.5015C16.8576 14.0345 17.4976 11.7254 18.6575 10.0029C19.7441 8.38716 21.1551 7.49816 22.6284 7.49816ZM22.6284 6.60916C18.9508 6.60916 15.9688 11.0386 15.9688 16.5015C15.9688 21.9644 18.9508 26.3939 22.6284 26.3939C26.306 26.3939 29.2881 21.9644 29.2881 16.5015C29.2881 11.0386 26.306 6.60916 22.6284 6.60916Z" fill="#B0BEC5"/>
<path d="M20.7339 15.5767C20.1339 15.0123 20.2672 13.8924 21.0361 13.0746C21.0761 13.0324 21.1161 12.9924 21.1583 12.9546C20.8161 12.7613 20.4406 12.6524 20.045 12.6524C18.4473 12.6524 17.1519 14.4146 17.1519 16.5856C17.1519 18.7566 18.4473 20.5187 20.045 20.5187C21.6427 20.5187 22.9382 18.7566 22.9382 16.5856C22.9382 16.1967 22.896 15.8189 22.8182 15.4634C22.1005 15.9812 21.2383 16.05 20.7339 15.5767Z" fill="url(#paint1_linear_2122_5062)"/>
<defs>
<linearGradient id="paint0_linear_2122_5062" x1="6.78232" y1="12.651" x2="6.78232" y2="20.5188" gradientUnits="userSpaceOnUse">
<stop stop-color="#424242"/>
<stop offset="1" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint1_linear_2122_5062" x1="20.0449" y1="12.6515" x2="20.0449" y2="20.5193" gradientUnits="userSpaceOnUse">
<stop stop-color="#424242"/>
<stop offset="1" stop-color="#212121"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -62,6 +62,7 @@
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_notification_channel": "Notification Channel",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
@@ -100,7 +101,7 @@
"user_guide_ch_step3a": "Set alert severity, name and descriptions",
"user_guide_ch_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts",
"choose_alert_type": "Choose a type for the alert:",
"choose_alert_type": "Choose a type for the alert",
"metric_based_alert": "Metric based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data",
"log_based_alert": "Log-based Alert",

View File

@@ -3,6 +3,7 @@
"alert_channels": "Alert Channels",
"organization_settings": "Organization Settings",
"ingestion_settings": "Ingestion Settings",
"api_keys": "API Keys",
"my_settings": "My Settings",
"overview_metrics": "Overview Metrics",
"dbcall_metrics": "Database Calls",

View File

@@ -54,6 +54,7 @@
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_notification_channel": "Notification Channel",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",

View File

@@ -26,6 +26,7 @@
"MY_SETTINGS": "SigNoz | My Settings",
"ORG_SETTINGS": "SigNoz | Organization Settings",
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
"API_KEYS": "SigNoz | API Keys",
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
"UN_AUTHORIZED": "SigNoz | Unauthorized",
"NOT_FOUND": "SigNoz | Page Not Found",

View File

@@ -63,6 +63,7 @@
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_notification_channel": "Notification Channel",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
@@ -100,7 +101,7 @@
"user_guide_ch_step3a": "Set alert severity, name and descriptions",
"user_guide_ch_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts",
"choose_alert_type": "Choose a type for the alert:",
"choose_alert_type": "Choose a type for the alert",
"metric_based_alert": "Metric based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data",
"log_based_alert": "Log-based Alert",

View File

@@ -0,0 +1,3 @@
{
"delete_confirm_message": "Are you sure you want to delete {{keyName}} key? Deleting a key is irreversible and cannot be undone."
}

View File

@@ -1,3 +1,4 @@
{
"name_of_the_view": "Name of the view"
"name_of_the_view": "Name of the view",
"delete_confirm_message": "Are you sure you want to delete {{viewName}} view? Deleting a view is irreversible and cannot be undone."
}

View File

@@ -3,6 +3,7 @@
"alert_channels": "Alert Channels",
"organization_settings": "Organization Settings",
"ingestion_settings": "Ingestion Settings",
"api_keys": "API Keys",
"my_settings": "My Settings",
"overview_metrics": "Overview Metrics",
"dbcall_metrics": "Database Calls",

View File

@@ -54,6 +54,7 @@
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_notification_channel": "Notification Channel",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",

View File

@@ -26,6 +26,7 @@
"MY_SETTINGS": "SigNoz | My Settings",
"ORG_SETTINGS": "SigNoz | Organization Settings",
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
"API_KEYS": "SigNoz | API Keys",
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
"UN_AUTHORIZED": "SigNoz | Unauthorized",
"NOT_FOUND": "SigNoz | Page Not Found",
@@ -39,5 +40,7 @@
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"SUPPORT": "SigNoz | Support",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Save Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Save Views",
"DEFAULT": "Open source Observability Platform | SigNoz"
}

View File

@@ -98,6 +98,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (
userResponse &&
route &&
route.find((e) => e === userResponse.payload.role) === undefined
) {
history.push(ROUTES.UN_AUTHORIZED);
@@ -160,7 +161,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (currentRoute) {
const { isPrivate, key } = currentRoute;
if (isPrivate && key !== ROUTES.WORKSPACE_LOCKED) {
if (isPrivate && key !== String(ROUTES.WORKSPACE_LOCKED)) {
handlePrivateRoutes(key);
} else {
// no need to fetch the user and make user fetching false

View File

@@ -28,7 +28,11 @@ import AppReducer, { User } from 'types/reducer/app';
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
import PrivateRoute from './Private';
import defaultRoutes, { AppRoutes, SUPPORT_ROUTE } from './routes';
import defaultRoutes, {
AppRoutes,
LIST_LICENSES,
SUPPORT_ROUTE,
} from './routes';
function App(): JSX.Element {
const themeConfig = useThemeConfig();
@@ -150,6 +154,10 @@ function App(): JSX.Element {
if (isCloudUserVal || isEECloudUser()) {
const newRoutes = [...routes, SUPPORT_ROUTE];
setRoutes(newRoutes);
} else {
const newRoutes = [...routes, LIST_LICENSES];
setRoutes(newRoutes);
}

View File

@@ -15,9 +15,20 @@ export const ServiceMapPage = Loadable(
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
);
export const LogsSaveViews = Loadable(
() => import(/* webpackChunkName: "LogsSaveViews" */ 'pages/LogsModulePage'), // TODO: Add a wrapper so that the same component can be used in traces
);
export const TracesExplorer = Loadable(
() =>
import(/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesExplorer'),
import(
/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesModulePage'
),
);
export const TracesSaveViews = Loadable(
() =>
import(/* webpackChunkName: "Traces Save Views" */ 'pages/TracesModulePage'),
);
export const TraceFilter = Loadable(
@@ -107,6 +118,10 @@ export const IngestionSettings = Loadable(
() => import(/* webpackChunkName: "Ingestion Settings" */ 'pages/Settings'),
);
export const APIKeys = Loadable(
() => import(/* webpackChunkName: "All Settings" */ 'pages/Settings'),
);
export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
);

View File

@@ -5,6 +5,7 @@ import { RouteProps } from 'react-router-dom';
import {
AllAlertChannels,
AllErrors,
APIKeys,
BillingPage,
CreateAlertChannelAlerts,
CreateNewAlerts,
@@ -21,6 +22,7 @@ import {
Logs,
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MySettings,
NewDashboardPage,
OldLogsExplorer,
@@ -39,6 +41,7 @@ import {
TraceDetail,
TraceFilter,
TracesExplorer,
TracesSaveViews,
UnAuthorized,
UsageExplorerPage,
} from './pageComponents';
@@ -86,6 +89,13 @@ const routes: AppRoutes[] = [
exact: true,
key: 'SERVICE_MAP',
},
{
path: ROUTES.LOGS_SAVE_VIEWS,
component: LogsSaveViews,
isPrivate: true,
exact: true,
key: 'LOGS_SAVE_VIEWS',
},
{
path: ROUTES.TRACE_DETAIL,
exact: true,
@@ -163,6 +173,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'TRACES_EXPLORER',
},
{
path: ROUTES.TRACES_SAVE_VIEWS,
exact: true,
component: TracesSaveViews,
isPrivate: true,
key: 'TRACES_SAVE_VIEWS',
},
{
path: ROUTES.CHANNELS_NEW,
exact: true,
@@ -191,13 +208,6 @@ const routes: AppRoutes[] = [
component: AllErrors,
key: 'ALL_ERROR',
},
{
path: ROUTES.LIST_LICENSES,
exact: true,
component: LicensePage,
isPrivate: true,
key: 'LIST_LICENSES',
},
{
path: ROUTES.ERROR_DETAIL,
exact: true,
@@ -226,6 +236,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'INGESTION_SETTINGS',
},
{
path: ROUTES.API_KEYS,
exact: true,
component: APIKeys,
isPrivate: true,
key: 'API_KEYS',
},
{
path: ROUTES.MY_SETTINGS,
exact: true,
@@ -320,6 +337,14 @@ export const SUPPORT_ROUTE: AppRoutes = {
isPrivate: true,
};
export const LIST_LICENSES: AppRoutes = {
path: ROUTES.LIST_LICENSES,
exact: true,
component: LicensePage,
isPrivate: true,
key: 'LIST_LICENSES',
};
export interface AppRoutes {
component: RouteProps['component'];
path: RouteProps['path'];

View File

@@ -33,10 +33,17 @@
.timeSelection-input {
display: flex;
gap: 8px;
height: 33px;
align-items: center;
padding: 4px 8px;
padding-left: 0px !important;
&.custom-time {
input:not(:focus) {
min-width: 240px;
}
}
input::placeholder {
color: white;
}
@@ -53,6 +60,26 @@
font-weight: 400 !important;
}
.info-text {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
cursor: default;
color: var(--bg-vanilla-400, #c0c1c3) !important;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
}
.info-text:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.lightMode {
.time-options-container {
.time-options-item {
@@ -87,4 +114,8 @@
color: rgba($color: #000000, $alpha: 0.4);
}
}
.info-text {
color: var(--bg-slate-400) !important;
}
}

View File

@@ -4,22 +4,43 @@ import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { Options } from 'container/TopNav/DateTimeSelection/config';
import {
FixedDurationSuggestionOptions,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { defaultTo, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import { ChangeEvent, useEffect, useState } from 'react';
import {
ChangeEvent,
Dispatch,
SetStateAction,
useEffect,
useState,
} from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
const maxAllowedMinTimeInMonths = 6;
interface CustomTimePickerProps {
onSelect: (value: string) => void;
onError: (value: boolean) => void;
items: any[];
selectedValue: string;
selectedTime: string;
onValidCustomDateChange: ([t1, t2]: any[]) => void;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
items: any[];
newPopover?: boolean;
customDateTimeVisible?: boolean;
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
handleGoLive?: () => void;
}
function CustomTimePicker({
@@ -28,9 +49,15 @@ function CustomTimePicker({
items,
selectedValue,
selectedTime,
open,
setOpen,
onValidCustomDateChange,
newPopover,
customDateTimeVisible,
setCustomDTPickerVisible,
onCustomDateHandler,
handleGoLive,
}: CustomTimePickerProps): JSX.Element {
const [open, setOpen] = useState(false);
const [
selectedTimePlaceholderValue,
setSelectedTimePlaceholderValue,
@@ -56,6 +83,20 @@ function CustomTimePicker({
return Options[index].label;
}
}
for (
let index = 0;
index < RelativeDurationSuggestionOptions.length;
index++
) {
if (RelativeDurationSuggestionOptions[index].value === selectedTime) {
return RelativeDurationSuggestionOptions[index].label;
}
}
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
if (FixedDurationSuggestionOptions[index].value === selectedTime) {
return FixedDurationSuggestionOptions[index].label;
}
}
return '';
};
@@ -140,19 +181,25 @@ function CustomTimePicker({
debouncedHandleInputChange(inputValue);
};
const handleSelect = (label: string, value: string): void => {
onSelect(value);
setSelectedTimePlaceholderValue(label);
setInputStatus('');
onError(false);
setInputErrorMessage(null);
setInputValue('');
if (value !== 'custom') {
hide();
}
};
const content = (
<div className="time-selection-dropdown-content">
<div className="time-options-container">
{items.map(({ value, label }) => (
{items?.map(({ value, label }) => (
<div
onClick={(): void => {
onSelect(value);
setSelectedTimePlaceholderValue(label);
setInputStatus('');
onError(false);
setInputErrorMessage(null);
setInputValue('');
hide();
handleSelect(label, value);
}}
key={value}
className={cx(
@@ -178,14 +225,33 @@ function CustomTimePicker({
return (
<div className="custom-time-picker">
<Popover
className="timeSelection-input-container"
className={cx(
'timeSelection-input-container',
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
)}
placement="bottomRight"
getPopupContainer={popupContainer}
content={content}
rootClassName="date-time-root"
content={
newPopover ? (
<CustomTimePickerPopoverContent
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
selectedTime={selectedTime}
/>
) : (
content
)
}
arrow={false}
trigger="hover"
open={open}
onOpenChange={handleOpenChange}
trigger={['click']}
style={{
padding: 0,
}}
@@ -193,12 +259,7 @@ function CustomTimePicker({
<Input
className="timeSelection-input"
type="text"
style={{
minWidth: '120px',
width: '100%',
}}
status={inputValue && inputStatus === 'error' ? 'error' : ''}
allowClear={!isInputFocused && selectedTime === 'custom'}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
@@ -238,3 +299,11 @@ function CustomTimePicker({
}
export default CustomTimePicker;
CustomTimePicker.defaultProps = {
newPopover: false,
customDateTimeVisible: false,
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
handleGoLive: noop,
};

View File

@@ -0,0 +1,133 @@
import './CustomTimePicker.styles.scss';
import { Button, DatePicker } from 'antd';
import cx from 'classnames';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
interface CustomTimePickerPopoverContentProps {
options: any[];
setIsOpen: Dispatch<SetStateAction<boolean>>;
customDateTimeVisible: boolean;
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (dateTimeRange: DateTimeRangeType) => void;
onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void;
selectedTime: string;
}
function CustomTimePickerPopoverContent({
options,
setIsOpen,
customDateTimeVisible,
setCustomDTPickerVisible,
onCustomDateHandler,
onSelectHandler,
handleGoLive,
selectedTime,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { RangePicker } = DatePicker;
const { pathname } = useLocation();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time);
};
function getTimeChips(options: Option[]): JSX.Element {
return (
<div className="relative-date-time-section">
{options.map((option) => (
<Button
type="text"
className="time-btns"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
>
{option.label}
</Button>
))}
</div>
);
}
return (
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div className="relative-date-time">
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePicker
disabledDate={disabledDate}
allowClear
onCalendarChange={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
})}
/>
) : (
<div>
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
</div>
);
}
export default CustomTimePickerPopoverContent;

View File

@@ -6,7 +6,6 @@ import {
} from '@ant-design/icons';
import {
Button,
Card,
Col,
Dropdown,
MenuProps,
@@ -152,95 +151,100 @@ function ExplorerCard({
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
const showSaveView = false;
return (
<>
<ExplorerCardHeadContainer size="small">
<Row align="middle">
<Col span={6}>
<Space>
<Typography>Query Builder</Typography>
<TextToolTip
url={ExploreHeaderToolTip.url}
text={ExploreHeaderToolTip.text}
useFilledIcon={false}
/>
</Space>
</Col>
<OffSetCol span={18}>
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
<Select
getPopupContainer={popupContainer}
loading={isLoading || isRefetching}
showSearch
placeholder="Select a view"
dropdownStyle={DropDownOverlay}
dropdownMatchSelectWidth={false}
optionLabelProp="value"
value={viewName || undefined}
{showSaveView && (
<ExplorerCardHeadContainer size="small">
<Row align="middle">
<Col span={6}>
<Space>
<Typography>Query Builder</Typography>
<TextToolTip
url={ExploreHeaderToolTip.url}
text={ExploreHeaderToolTip.text}
useFilledIcon={false}
/>
</Space>
</Col>
<OffSetCol span={18}>
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
<Select
getPopupContainer={popupContainer}
loading={isLoading || isRefetching}
showSearch
placeholder="Select a view"
dropdownStyle={DropDownOverlay}
dropdownMatchSelectWidth={false}
optionLabelProp="value"
value={viewName || undefined}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.uuid} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.uuid}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}
/>
</Select.Option>
))}
</Select>
</Space>
)}
{isQueryUpdated && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={onUpdateQueryHandler}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.uuid} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.uuid}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}
/>
</Select.Option>
))}
</Select>
</Space>
)}
{isQueryUpdated && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={onUpdateQueryHandler}
Save changes
</Button>
)}
<Popover
getPopupContainer={popupContainer}
placement="bottomLeft"
trigger="click"
content={
<SaveViewWithName
sourcePage={sourcepage}
handlePopOverClose={handleOpenChange}
refetchAllView={refetchAllView}
/>
}
showArrow={false}
open={isOpen}
onOpenChange={handleOpenChange}
>
Save changes
</Button>
)}
<Popover
getPopupContainer={popupContainer}
placement="bottomLeft"
trigger="click"
content={
<SaveViewWithName
sourcePage={sourcepage}
handlePopOverClose={handleOpenChange}
refetchAllView={refetchAllView}
/>
}
showArrow={false}
open={isOpen}
onOpenChange={handleOpenChange}
>
<Button
type={saveButtonType}
icon={saveButtonIcon}
data-testid="traces-save-view-action"
>
{isQueryUpdated
? SaveButtonText.SAVE_AS_NEW_VIEW
: SaveButtonText.SAVE_VIEW}
</Button>
</Popover>
<ShareAltOutlined onClick={onCopyUrlHandler} />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<MoreOutlined />
</Dropdown>
)}
</Space>
</OffSetCol>
</Row>
</ExplorerCardHeadContainer>
<Card>{children}</Card>
<Button
type={saveButtonType}
icon={saveButtonIcon}
data-testid="traces-save-view-action"
>
{isQueryUpdated
? SaveButtonText.SAVE_AS_NEW_VIEW
: SaveButtonText.SAVE_VIEW}
</Button>
</Popover>
<ShareAltOutlined onClick={onCopyUrlHandler} />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<MoreOutlined />
</Dropdown>
)}
</Space>
</OffSetCol>
</Row>
</ExplorerCardHeadContainer>
)}
<div>{children}</div>
</>
);
}

View File

@@ -3,6 +3,7 @@ import styled, { CSSProperties } from 'styled-components';
export const ExplorerCardHeadContainer = styled(Card)`
margin: 1rem 0;
padding: 0;
`;
export const OffSetCol = styled(Col)`

View File

@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
@@ -46,7 +46,7 @@ describe('ExplorerCard', () => {
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</MockQueryClientProvider>,
);
expect(screen.getByText('Query Builder')).toBeInTheDocument();
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
});
it('renders a save view button', () => {
@@ -55,19 +55,6 @@ describe('ExplorerCard', () => {
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</MockQueryClientProvider>,
);
expect(screen.getByText('Save view')).toBeInTheDocument();
});
it('should see all the view listed in dropdown', async () => {
const screen = render(
<ExplorerCard sourcepage={DataSource.TRACES}>Mock Children</ExplorerCard>,
);
const selectPlaceholder = screen.getByText('Select a view');
fireEvent.mouseDown(selectPlaceholder);
const viewNameText = await screen.getAllByText('View 1');
viewNameText.forEach((element) => {
expect(element).toBeInTheDocument();
});
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
});
});

View File

@@ -14,7 +14,7 @@ import {
SaveViewHandlerProps,
} from './types';
const showErrorNotification = (
export const showErrorNotification = (
notifications: NotificationInstance,
err: Error,
): void => {
@@ -90,6 +90,14 @@ export const isQueryUpdatedInView = ({
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
if (
updatedCurrentQuery?.builder === undefined ||
updatedCurrentQuery.clickhouse_sql === undefined ||
updatedCurrentQuery.promql === undefined
) {
return false;
}
return (
panelType !== currentPanelType ||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||

View File

@@ -3,8 +3,11 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { ILog } from 'types/api/logs/log';
import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Pick<ActionItemProps, 'onClickActionItem'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -0,0 +1,224 @@
.log-detail-drawer {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
padding: 16px;
}
.title {
color: var(--text-vanilla-400);
font-family: Inter;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.log-detail-drawer__log {
width: 100%;
display: flex;
align-items: center;
gap: 4px;
position: relative;
.log-body {
font-family: 'SF Mono';
font-family: 'Space Mono', monospace;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
line-height: 18px;
letter-spacing: -0.07px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
color: var(--text-vanilla-400);
opacity: 0.6;
}
.log-type-indicator {
height: 24px;
border: 2px solid var(--bg-slate-400);
border-radius: 5px;
margin-left: 0;
&.INFO {
border-color: #1d212d;
}
&.WARNING {
border-color: #ffcd56;
}
&.ERROR {
border-color: #e5484d;
}
}
.log-overflow-shadow {
background: linear-gradient(270deg, #121317 10.4%, rgba(18, 19, 23, 0) 100%);
width: 196px;
position: absolute;
right: 0;
}
}
.tabs-and-search {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.json-action-btn {
display: flex;
gap: 8px;
}
}
.views-tabs {
color: var(--text-vanilla-400);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.tab {
border: 1px solid var(--bg-slate-400);
width: 114px;
}
.tab::before {
background: var(--bg-slate-400);
}
.selected_view {
background: var(--bg-slate-300);
color: var(--text-vanilla-100);
border: 1px solid var(--bg-slate-400);
}
.selected_view::before {
background: var(--bg-slate-400);
}
}
.search-input {
margin-top: var(--margin-2);
border: 1px solid var(--bg-slate-400);
height: 46px;
padding: var(--padding-1) var(--padding-2);
}
}
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
.log-detail-drawer {
.title {
color: var(--text-ink-300);
}
.log-detail-drawer__log {
.log-overflow-shadow {
background: linear-gradient(
270deg,
var(--bg-vanilla-100) 10.4%,
rgba(255, 255, 255, 0) 100%
);
}
.log-type-indicator {
border: 2px solid var(--bg-vanilla-400);
}
.ant-typography {
color: var(--text-ink-300);
background: transparent;
}
}
.radio-button {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.views-tabs {
.tab {
background: var(--bg-vanilla-100);
}
.selected_view {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--text-ink-400);
}
.selected_view::before {
background: var(--bg-vanilla-300);
border-left: 1px solid var(--bg-slate-300);
}
}
.tabs-and-search {
.action-btn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
.search-input {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
}

View File

@@ -0,0 +1,10 @@
.query-builder-search-wrapper {
margin-top: 10px;
height: 46px;
border: 1px solid var(--bg-slate-400);
border-bottom: none;
.ant-select-selector {
border: none !important;
}
}

View File

@@ -0,0 +1,77 @@
import './QueryBuilderSearchWrapper.styles.scss';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { Dispatch, SetStateAction, useEffect } from 'react';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
function QueryBuilderSearchWrapper({
log,
filters,
contextQuery,
isEdit,
suffixIcon,
setFilters,
setContextQuery,
}: QueryBuilderSearchWraperProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
useEffect(() => {
setContextQuery(initialContextQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleSearch = (tagFilters: TagFilter): void => {
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || !filters.items.length)) ||
tagFiltersLength === filters?.items.length ||
!contextQuery
)
return;
const nextQuery: Query = {
...contextQuery,
builder: {
...contextQuery.builder,
queryData: contextQuery.builder.queryData.map((item) => ({
...item,
filters: tagFilters,
})),
},
};
setFilters({ ...tagFilters });
setContextQuery({ ...nextQuery });
};
// eslint-disable-next-line react/jsx-no-useless-fragment
if (!contextQuery || !isEdit) return <></>;
return (
<QueryBuilderSearch
query={contextQuery?.builder.queryData[0]}
onChange={handleSearch}
className="query-builder-search-wrapper"
suffixIcon={suffixIcon}
/>
);
}
interface QueryBuilderSearchWraperProps {
log: ILog;
isEdit: boolean;
contextQuery: Query | undefined;
setContextQuery: Dispatch<SetStateAction<Query | undefined>>;
filters: TagFilter | null;
setFilters: Dispatch<SetStateAction<TagFilter | null>>;
suffixIcon?: React.ReactNode;
}
QueryBuilderSearchWrapper.defaultProps = {
suffixIcon: undefined,
};
export default QueryBuilderSearchWrapper;

View File

@@ -0,0 +1,7 @@
export const VIEW_TYPES = {
OVERVIEW: 'OVERVIEW',
JSON: 'JSON',
CONTEXT: 'CONTEXT',
} as const;
export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];

View File

@@ -1,50 +1,207 @@
import { Drawer, Tabs } from 'antd';
import JSONView from 'container/LogDetailedView/JsonView';
import TableView from 'container/LogDetailedView/TableView';
import { useMemo } from 'react';
/* eslint-disable sonarjs/cognitive-complexity */
import './LogDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import {
Braces,
Copy,
Filter,
HardHat,
Table,
TextSelect,
X,
} from 'lucide-react';
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
function LogDetail({
log,
onClose,
onAddToQuery,
onClickActionItem,
selectedTab,
}: LogDetailProps): JSX.Element {
const items = useMemo(
() => [
{
label: 'Table',
key: '1',
children: log && (
<TableView
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
/>
),
},
{
label: 'JSON',
key: '2',
children: log && <JSONView logData={log} />,
},
],
[log, onAddToQuery, onClickActionItem],
);
const [, copyToClipboard] = useCopyToClipboard();
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
const [isFilterVisibile, setIsFilterVisible] = useState<boolean>(false);
const [contextQuery, setContextQuery] = useState<Query | undefined>();
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const { notifications } = useNotifications();
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setIsEdit(false);
setIsFilterVisible(false);
};
const handleFilterVisible = (): void => {
setIsFilterVisible(!isFilterVisibile);
setIsEdit(!isEdit);
};
const drawerCloseHandler = (
e: React.MouseEvent | React.KeyboardEvent,
): void => {
if (onClose) {
onClose(e);
}
};
const handleJSONCopy = (): void => {
copyToClipboard(LogJsonData);
notifications.success({
message: 'Copied to clipboard',
});
};
if (!log) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
const logType = log?.attributes_string?.log_level || LogType.INFO;
return (
<Drawer
width="60%"
title="Log Details"
title={
<>
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</>
}
placement="right"
closable
onClose={onClose}
// closable
onClose={drawerCloseHandler}
open={log !== null}
style={{ overscrollBehavior: 'contain' }}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="log-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
<Tabs defaultActiveKey="1" items={items} />
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={log?.body} placement="left">
<Typography.Text className="log-body">{log?.body}</Typography.Text>
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
</Radio.Group>
{selectedView === VIEW_TYPES.JSON && (
<div className="json-action-btn">
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={handleJSONCopy}
/>
</div>
)}
{selectedView === VIEW_TYPES.CONTEXT && (
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
)}
</div>
<QueryBuilderSearchWrapper
isEdit={isEdit}
log={log}
filters={filters}
setContextQuery={setContextQuery}
setFilters={setFilters}
contextQuery={contextQuery}
suffixIcon={
<HardHat size={12} style={{ paddingRight: Spacing.PADDING_2 }} />
}
/>
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
</Drawer>
);
}

View File

@@ -0,0 +1,103 @@
.log-field-key {
padding-right: 5px;
color: var(--text-vanilla-400, #c0c1c3);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.log-value {
color: var(--text-vanilla-400, #c0c1c3);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.log-line {
display: flex;
overflow: hidden;
.log-state-indicator {
padding-left: 0;
}
transition: background-color 0.2s ease-in;
&:hover {
background-color: rgba(171, 189, 255, 0.04) !important;
}
}
.log-selected-fields {
display: flex;
width: 100%;
overflow: hidden;
align-items: center;
.selected-log-field-key {
color: var(--bg-robin-400) !important;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.selected-log-value {
color: var(--bg-sienna-500);
border-radius: 2px;
background: rgba(173, 127, 88, 0.08);
padding: 0px 2px;
margin-left: 7px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
font-size: 14px;
}
.selected-log-kv {
min-height: 24px;
display: flex;
align-items: center;
}
}
.log-action-buttons {
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
right: 0;
cursor: pointer;
height: 32px;
width: 68px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: var(--bg-ink-400, #121317);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
.context-btn {
width: 50% !important;
}
.copy-link-btn {
width: 50% !important;
border-left: 1px solid var(--bg-slate-400, #1d212d) !important;
}
.ant-btn-default {
border: none;
box-shadow: none;
}
}
.lightMode {
.log-field-key {
color: var(--text-slate-400);
}
.log-value {
color: var(--text-slate-400);
}
.log-line {
&:hover {
background-color: var(--text-vanilla-200) !important;
}
}
}

View File

@@ -1,35 +1,32 @@
import { blue, grey, orange } from '@ant-design/colors';
import {
CopyFilled,
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import './ListLogView.styles.scss';
import { blue } from '@ant-design/colors';
import Convert from 'ansi-to-html';
import { Button, Divider, Row, Typography } from 'antd';
import LogsExplorerContext from 'container/LogsExplorerContext';
import { Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useNotifications } from 'hooks/useNotifications';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import { useCallback, useMemo } from 'react';
import { useCopyToClipboard } from 'react-use';
import { useCallback, useMemo, useState } from 'react';
// interfaces
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
// components
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
// styles
import {
Container,
LogContainer,
LogText,
SelectedLog,
Text,
TextContainer,
} from './styles';
@@ -55,12 +52,10 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
return (
<TextContainer>
<Text ellipsis type="secondary">
{`${fieldKey}: `}
<Text ellipsis type="secondary" className="log-field-key">
{`${fieldKey} : `}
</Text>
<CopyClipboardHOC textToCopy={fieldValue}>
<LogText dangerouslySetInnerHTML={html} />
</CopyClipboardHOC>
<LogText dangerouslySetInnerHTML={html} className="log-value" />
</TextContainer>
);
}
@@ -71,23 +66,23 @@ function LogSelectedField({
onAddToQuery,
}: LogSelectedFieldProps): JSX.Element {
return (
<SelectedLog>
<div className="log-selected-fields">
<AddToQueryHOC
fieldKey={fieldKey}
fieldValue={fieldValue}
onAddToQuery={onAddToQuery}
>
<Typography.Text>
<span style={{ color: blue[4] }}>{fieldKey}</span>
<span style={{ color: blue[4] }} className="selected-log-field-key">
{fieldKey}
</span>
</Typography.Text>
</AddToQueryHOC>
<CopyClipboardHOC textToCopy={fieldValue}>
<Typography.Text ellipsis>
<span>{': '}</span>
<span style={{ color: orange[6] }}>{fieldValue || "''"}</span>
</Typography.Text>
</CopyClipboardHOC>
</SelectedLog>
<Typography.Text ellipsis className="selected-log-kv">
<span className="selected-log-field-key">{': '}</span>
<span className="selected-log-value">{fieldValue || "''"}</span>
</Typography.Text>
</div>
);
}
@@ -96,6 +91,7 @@ type ListLogViewProps = {
selectedFields: IField[];
onSetActiveLog: (log: ILog) => void;
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null;
};
function ListLogView({
@@ -103,34 +99,42 @@ function ListLogView({
selectedFields,
onSetActiveLog,
onAddToQuery,
activeLog,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
logData.id,
);
const {
activeLog: activeContextLog,
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const handlerClearActiveContextLog = useCallback(
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
handleClearActiveContextLog();
},
[handleClearActiveContextLog],
);
const handleDetailedView = useCallback(() => {
onSetActiveLog(logData);
}, [logData, onSetActiveLog]);
const handleShowContext = useCallback(() => {
handleSetActiveContextLog(logData);
}, [logData, handleSetActiveContextLog]);
const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2));
notifications.success({
message: 'Copied to clipboard',
});
};
const handleShowContext = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
handleSetActiveContextLog(logData);
},
[logData, handleSetActiveContextLog],
);
const updatedSelecedFields = useMemo(
() => selectedFields.filter((e) => e.name !== 'id'),
@@ -145,84 +149,74 @@ function ListLogView({
[flattenLogData.timestamp],
);
const logType = logData?.attributes_string?.log_level || LogType.INFO;
const handleMouseEnter = (): void => {
setHasActionButtons(true);
};
const handleMouseLeave = (): void => {
setHasActionButtons(false);
};
return (
<Container $isActiveLog={isHighlighted}>
<div>
<LogContainer>
<>
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
{flattenLogData.stream && (
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
)}
<LogGeneralField fieldKey="timestamp" fieldValue={timestampValue} />
</>
</LogContainer>
<div>
{updatedSelecedFields.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
<LogSelectedField
key={field.name}
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
onAddToQuery={onAddToQuery}
/>
) : null,
)}
<>
<Container
$isActiveLog={isHighlighted}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}
>
<div className="log-line">
<LogStateIndicator
type={logType}
isActive={
activeLog?.id === logData.id || activeContextLog?.id === logData.id
}
/>
<div>
<LogContainer>
<LogGeneralField fieldKey="Log" fieldValue={flattenLogData.body} />
{flattenLogData.stream && (
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
)}
<LogGeneralField fieldKey="Timestamp" fieldValue={timestampValue} />
{updatedSelecedFields.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
<LogSelectedField
key={field.name}
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
onAddToQuery={onAddToQuery}
/>
) : null,
)}
</LogContainer>
</div>
</div>
</div>
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />
<Row>
<Button
size="small"
type="text"
onClick={handleDetailedView}
style={{ color: blue[5] }}
icon={<ExpandAltOutlined />}
>
View Details
</Button>
<Button
size="small"
type="text"
onClick={handleCopyJSON}
style={{ color: grey[1] }}
icon={<CopyFilled />}
>
Copy JSON
</Button>
{isLogsExplorerPage && (
<>
<Button
size="small"
type="text"
onClick={handleShowContext}
style={{ color: grey[1] }}
icon={<MonitorOutlined />}
>
Show in Context
</Button>
<Button
size="small"
type="text"
onClick={onLogCopy}
style={{ color: grey[1] }}
icon={<LinkOutlined />}
>
Copy Link
</Button>
</>
)}
{activeContextLog && (
<LogsExplorerContext
log={activeContextLog}
onClose={handleClearActiveContextLog}
{hasActionButtons && isLogsExplorerPage && (
<LogLinesActionButtons
handleShowContext={handleShowContext}
onLogCopy={onLogCopy}
/>
)}
</Row>
</Container>
</Container>
{activeContextLog && (
<LogDetail
log={activeContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
/>
)}
</>
);
}
ListLogView.defaultProps = {
activeLog: null,
};
export default ListLogView;

View File

@@ -7,6 +7,7 @@ export const Container = styled(Card)<{
}>`
width: 100% !important;
margin-bottom: 0.3rem;
cursor: pointer;
.ant-card-body {
padding: 0.3rem 0.6rem;
}
@@ -29,11 +30,13 @@ export const TextContainer = styled.div`
export const LogContainer = styled.div`
margin-left: 0.5rem;
display: flex;
flex-direction: column;
gap: 6px;
`;
export const LogText = styled.div`
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;

View File

@@ -0,0 +1,44 @@
.log-line-action-buttons {
display: flex;
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
right: 0;
cursor: pointer;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.ant-btn-default {
border: none;
box-shadow: none;
padding: 9px;
justify-content: center;
align-items: center;
display: flex;
&.active-tab {
background-color: var(--bg-slate-400);
}
}
.copy-log-btn {
border-left: 1px solid var(--bg-slate-400);
border-color: var(--bg-slate-400) !important;
}
}
.lightMode {
.log-line-action-buttons {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-400);
.ant-btn-default {
}
.copy-log-btn {
border-left: 1px solid var(--bg-vanilla-400);
border-color: var(--bg-vanilla-400) !important;
}
}
}

View File

@@ -0,0 +1,42 @@
import './LogLinesActionButtons.styles.scss';
import { LinkOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { TextSelect } from 'lucide-react';
import { MouseEventHandler } from 'react';
export interface LogLinesActionButtonsProps {
handleShowContext: MouseEventHandler<HTMLElement>;
onLogCopy: MouseEventHandler<HTMLElement>;
customClassName?: string;
}
export default function LogLinesActionButtons({
handleShowContext,
onLogCopy,
customClassName = '',
}: LogLinesActionButtonsProps): JSX.Element {
return (
<div className={`log-line-action-buttons ${customClassName}`}>
<Tooltip title="Show Context">
<Button
size="small"
icon={<TextSelect size={14} />}
className="show-context-btn"
onClick={handleShowContext}
/>
</Tooltip>
<Tooltip title="Copy Link">
<Button
size="small"
icon={<LinkOutlined size={14} />}
onClick={onLogCopy}
className="copy-log-btn"
/>
</Tooltip>
</div>
);
}
LogLinesActionButtons.defaultProps = {
customClassName: '',
};

View File

@@ -0,0 +1,30 @@
.log-state-indicator {
padding-left: 8px;
.line {
margin: 0 8px;
min-height: 24px;
height: 100%;
width: 3px;
border-radius: 50px;
background-color: transparent;
&.INFO {
background-color: #1d212d;
}
&.WARNING {
background-color: #ffcd56;
}
&.ERROR {
background-color: #e5484d;
}
}
&.isActive {
.line {
background-color: var(--bg-robin-400, #7190f9);
}
}
}

View File

@@ -0,0 +1,28 @@
import './LogStateIndicator.styles.scss';
import cx from 'classnames';
export const LogType = {
INFO: 'INFO',
WARNING: 'WARNING',
ERROR: 'ERROR',
};
function LogStateIndicator({
type,
isActive,
}: {
type: string;
isActive?: boolean;
}): JSX.Element {
return (
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
<div className={cx('line', type)}> </div>
</div>
);
}
LogStateIndicator.defaultProps = {
isActive: false,
};
export default LogStateIndicator;

View File

@@ -1,11 +1,9 @@
import {
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import './RawLogView.styles.scss';
import Convert from 'ansi-to-html';
import { Button, DrawerProps, Tooltip } from 'antd';
import { DrawerProps } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
@@ -13,6 +11,8 @@ import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
// hooks
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { isEmpty, isUndefined } from 'lodash-es';
import {
KeyboardEvent,
MouseEvent,
@@ -22,13 +22,12 @@ import {
useState,
} from 'react';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
// styles
import {
ActionButtonsWrapper,
ExpandIconWrapper,
RawLogContent,
RawLogViewContainer,
} from './styles';
import { RawLogContent, RawLogViewContainer } from './styles';
import { RawLogViewProps } from './types';
const convert = new Convert();
@@ -39,13 +38,15 @@ function RawLogView({
data,
linesPerRow,
isTextOverflowEllipsisDisabled,
selectedFields = [],
}: RawLogViewProps): JSX.Element {
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
data.id,
);
const flattenLogData = useMemo(() => FlatLogData(data), [data]);
const {
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const {
@@ -56,24 +57,47 @@ function RawLogView({
} = useActiveLog();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
const isDarkMode = useIsDarkMode();
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
const severityText = data.severity_text ? `${data.severity_text} |` : '';
const logType = data?.attributes_string?.log_level || LogType.INFO;
const updatedSelecedFields = useMemo(
() => selectedFields.filter((e) => e.name !== 'id'),
[selectedFields],
);
const attributesValues = updatedSelecedFields
.map((field) => flattenLogData[field.name])
.filter((attribute) => !isUndefined(attribute) && !isEmpty(attribute));
let attributesText = attributesValues.join(' | ');
if (attributesText.length > 0) {
attributesText += ' | ';
}
const text = useMemo(
() =>
typeof data.timestamp === 'string'
? `${dayjs(data.timestamp).format()} | ${severityText} ${data.body}`
: `${dayjs(data.timestamp / 1e6).format()} | ${severityText} ${data.body}`,
[data.timestamp, data.body, severityText],
? `${dayjs(data.timestamp).format()} | ${attributesText} ${severityText} ${
data.body
}`
: `${dayjs(
data.timestamp / 1e6,
).format()} | ${attributesText} ${severityText} ${data.body}`,
[data.timestamp, data.body, severityText, attributesText],
);
const handleClickExpand = useCallback(() => {
if (activeContextLog || isReadOnly) return;
onSetActiveLog(data);
setSelectedTab(VIEW_TYPES.OVERVIEW);
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
@@ -84,6 +108,7 @@ function RawLogView({
event.stopPropagation();
onClearActiveLog();
setSelectedTab(undefined);
},
[onClearActiveLog],
);
@@ -104,9 +129,11 @@ function RawLogView({
(event) => {
event.preventDefault();
event.stopPropagation();
handleSetActiveContextLog(data);
// handleSetActiveContextLog(data);
setSelectedTab(VIEW_TYPES.CONTEXT);
onSetActiveLog(data);
},
[data, handleSetActiveContextLog],
[data, onSetActiveLog],
);
const html = useMemo(
@@ -123,37 +150,30 @@ function RawLogView({
align="middle"
$isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isActiveLog={isHighlighted}
$isHightlightedLog={isHighlighted}
$isActiveLog={isActiveLog}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{!isReadOnly && (
<ExpandIconWrapper flex="30px">
<ExpandAltOutlined />
</ExpandIconWrapper>
)}
<LogStateIndicator
type={logType}
isActive={activeLog?.id === data.id || activeContextLog?.id === data.id}
/>
<RawLogContent
$isReadOnly={isReadOnly}
$isActiveLog={isActiveLog}
$isDarkMode={isDarkMode}
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
linesPerRow={linesPerRow}
dangerouslySetInnerHTML={html}
/>
{hasActionButtons && (
<ActionButtonsWrapper>
<Tooltip title="Show Context">
<Button
size="small"
icon={<MonitorOutlined />}
onClick={handleShowContext}
/>
</Tooltip>
<Tooltip title="Copy Link">
<Button size="small" icon={<LinkOutlined />} onClick={onLogCopy} />
</Tooltip>
</ActionButtonsWrapper>
<LogLinesActionButtons
handleShowContext={handleShowContext}
onLogCopy={onLogCopy}
/>
)}
{activeContextLog && (
@@ -162,12 +182,15 @@ function RawLogView({
onClose={handleClearActiveContextLog}
/>
)}
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
{selectedTab && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
)}
</RawLogViewContainer>
);
}

View File

@@ -1,4 +1,5 @@
import { blue } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens';
import { Col, Row, Space } from 'antd';
import styled from 'styled-components';
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
@@ -9,20 +10,25 @@ export const RawLogViewContainer = styled(Row)<{
$isDarkMode: boolean;
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isHightlightedLog: boolean;
}>`
position: relative;
width: 100%;
font-weight: 700;
font-size: 0.625rem;
line-height: 1.25rem;
display: flex;
align-items: stretch;
transition: background-color 0.2s ease-in;
.log-state-indicator {
margin: 4px 0;
}
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
${({ $isReadOnly, $isDarkMode, $isActiveLog }): string =>
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? getActiveLogBackground()
? getActiveLogBackground($isActiveLog, $isDarkMode)
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
`;
@@ -30,13 +36,17 @@ export const ExpandIconWrapper = styled(Col)`
color: ${blue[6]};
padding: 0.25rem 0.375rem;
cursor: pointer;
font-size: 12px;
`;
export const RawLogContent = styled.div<RawLogContentProps>`
margin-bottom: 0;
font-family: Fira Code, monospace;
font-weight: 300;
font-family: 'SF Mono', monospace;
font-family: 'Space Mono', monospace;
font-size: 13px;
font-weight: 400;
text-align: left;
color: ${({ $isDarkMode }): string =>
$isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400};
${({ $isTextOverflowEllipsisDisabled, linesPerRow }): string =>
$isTextOverflowEllipsisDisabled
@@ -48,15 +58,12 @@ export const RawLogContent = styled.div<RawLogContentProps>`
line-clamp: ${linesPerRow};
-webkit-box-orient: vertical;`};
font-size: 12px;
line-height: 24px;
letter-spacing: -0.07px;
padding: 4px;
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
${({ $isActiveLog, $isReadOnly }): string =>
$isReadOnly && $isActiveLog ? 'padding: 0 1.5rem;' : ''}
`;
export const ActionButtonsWrapper = styled(Space)`

View File

@@ -1,3 +1,4 @@
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
export interface RawLogViewProps {
@@ -6,11 +7,13 @@ export interface RawLogViewProps {
isTextOverflowEllipsisDisabled?: boolean;
data: ILog;
linesPerRow: number;
selectedFields?: IField[];
}
export interface RawLogContentProps {
linesPerRow: number;
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isDarkMode?: boolean;
$isTextOverflowEllipsisDisabled?: boolean;
}

View File

@@ -1,12 +1,21 @@
import { TableProps } from 'antd';
import { CSSProperties } from 'react';
export const defaultCellStyle: CSSProperties = {
paddingTop: 4,
paddingBottom: 6,
paddingRight: 8,
paddingLeft: 8,
};
export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
return {
paddingTop: 4,
paddingBottom: 6,
paddingRight: 8,
paddingLeft: 8,
color: isDarkMode ? 'var(--bg-vanilla-400)' : 'var(--bg-slate-400)',
fontSize: '14px',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '18px',
letterSpacing: '-0.07px',
marginBottom: '0px',
};
}
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',

View File

@@ -2,18 +2,22 @@ import styled from 'styled-components';
interface TableBodyContentProps {
linesPerRow: number;
isDarkMode?: boolean;
}
export const TableBodyContent = styled.div<TableBodyContentProps>`
margin-bottom: 0;
color: ${(props): string =>
props.isDarkMode ? 'var(--bg-vanilla-400, #c0c1c3)' : 'var(--bg-slate-400)'};
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: ${(props): number => props.linesPerRow};
line-clamp: ${(props): number => props.linesPerRow};
-webkit-box-orient: vertical;
font-size: 0.875rem;
line-height: 2rem;
`;

View File

@@ -22,6 +22,8 @@ export type UseTableViewProps = {
appendTo?: 'center' | 'end';
onOpenLogsContext?: (log: ILog) => void;
onClickExpand?: (log: ILog) => void;
activeLog?: ILog | null;
activeContextLog?: ILog | null;
} & LogsTableViewProps;
export type ActionsColumnProps = {

View File

@@ -0,0 +1,27 @@
.text {
color: var(--bg-vanilla-400);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.table-timestamp {
display: flex;
align-items: center;
.ant-typography {
margin-bottom: 0;
}
.log-state-indicator {
padding: 0px;
}
}
.lightMode {
.text {
color: var(--bg-slate-400);
}
}

View File

@@ -1,22 +1,21 @@
import {
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import './useTableView.styles.scss';
import Convert from 'ansi-to-html';
import { Button, Space, Typography } from 'antd';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useCallback, useMemo } from 'react';
import { defaultTo } from 'lodash-es';
import { useMemo } from 'react';
import { ExpandIconWrapper } from '../RawLogView/styles';
import { defaultCellStyle, defaultTableStyle } from './config';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
import { defaultTableStyle, getDefaultCellStyle } from './config';
import { TableBodyContent } from './styles';
import {
ActionsColumnProps,
ColumnTypeRender,
UseTableViewProps,
UseTableViewResult,
@@ -24,60 +23,22 @@ import {
const convert = new Convert();
function ActionsColumn({
logId,
logs,
onOpenLogsContext,
}: ActionsColumnProps): JSX.Element {
const currentLog = useMemo(() => logs.find(({ id }) => id === logId), [
logs,
logId,
]);
const { onLogCopy } = useCopyLogLink(currentLog?.id);
const handleShowContext = useCallback(() => {
if (!onOpenLogsContext || !currentLog) return;
onOpenLogsContext(currentLog);
}, [currentLog, onOpenLogsContext]);
return (
<Space>
<Button
size="small"
onClick={handleShowContext}
icon={<MonitorOutlined />}
/>
<Button size="small" onClick={onLogCopy} icon={<LinkOutlined />} />
</Space>
);
}
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const {
logs,
fields,
linesPerRow,
appendTo = 'center',
onOpenLogsContext,
onClickExpand,
activeContextLog,
activeLog,
} = props;
const { isLogsExplorerPage } = useCopyLogLink();
const isDarkMode = useIsDarkMode();
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs,
]);
const handleClickExpand = useCallback(
(index: number): void => {
if (!onClickExpand) return;
onClickExpand(logs[index]);
},
[logs, onClickExpand],
);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
@@ -87,7 +48,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
style: getDefaultCellStyle(isDarkMode),
},
children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
@@ -98,38 +59,30 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
}));
return [
{
title: '',
dataIndex: 'id',
key: 'expand',
// https://github.com/ant-design/ant-design/discussions/36886
render: (_, item, index): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<ExpandIconWrapper
onClick={(): void => {
handleClickExpand(index);
}}
>
<ExpandAltOutlined />
</ExpandIconWrapper>
),
}),
},
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => {
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? dayjs(field).format()
: dayjs(field / 1e6).format();
return {
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
children: (
<div className="table-timestamp">
<LogStateIndicator
type={defaultTo(item.log_level, LogType.INFO) as string}
isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id
}
/>
<Typography.Paragraph ellipsis className="text">
{date}
</Typography.Paragraph>
</div>
),
};
},
},
@@ -148,38 +101,20 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
__html: convert.toHtml(dompurify.sanitize(field)),
}}
linesPerRow={linesPerRow}
isDarkMode={isDarkMode}
/>
),
}),
},
...(appendTo === 'end' ? fieldColumns : []),
...(isLogsExplorerPage
? ([
{
title: 'actions',
dataIndex: 'actions',
key: 'actions',
render: (_, log): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<ActionsColumn
logId={(log.id as unknown) as string}
logs={logs}
onOpenLogsContext={onOpenLogsContext}
/>
),
}),
},
] as ColumnsType<Record<string, unknown>>)
: []),
];
}, [
logs,
fields,
appendTo,
isDarkMode,
linesPerRow,
isLogsExplorerPage,
handleClickExpand,
onOpenLogsContext,
activeLog?.id,
activeContextLog?.id,
]);
return { columns, dataSource: flattenLogData };

View File

@@ -0,0 +1,394 @@
.nested-menu-container {
z-index: 2;
position: absolute;
right: -2px;
margin: 6px 0;
width: 160px;
border-radius: 4px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.menu-container {
padding: 12px;
.title {
font-family: Inter;
font-size: 11px;
font-weight: 600;
line-height: 18px;
letter-spacing: 0.08em;
text-align: left;
color: var(--bg-slate-200, #52575c);
}
.menu-items {
display: flex;
gap: 12px;
flex-direction: column;
margin-top: 12px;
}
.item {
font-family: Inter;
font-size: 13px;
font-weight: 400;
line-height: 17px;
letter-spacing: 0.01em;
text-align: left;
.item-label {
display: flex;
color: var(--bg-vanilla-400, #c0c1c3);
justify-content: space-between;
}
cursor: pointer;
}
}
.horizontal-line {
height: 1px;
background: #1d212d;
}
.max-lines-per-row {
padding: 12px;
.title {
color: var(--bg-slate-200, #52575c);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
.lucide {
color: var(--bg-vanilla-400, #c0c1c3);
cursor: pointer;
}
}
.max-lines-per-row-input {
display: flex;
border: 1px solid var(--bg-slate-400, #1d212d);
.ant-input-number-handler-wrap {
display: none;
}
.ant-input-number {
min-width: 36px;
width: auto;
border: 0px;
text-align: center;
height: 26px;
border-radius: 0;
&:active,
&:focus {
border: none;
box-shadow: none;
}
}
.ant-input-number-focused {
box-shadow: none !important;
}
.ant-input-number-input-wrap {
input {
text-align: center;
font-size: 13px;
&:active,
&:focus {
border: none;
}
}
&:active,
&:focus {
border: none;
}
}
.periscope-btn {
box-shadow: none;
padding: 6px 12px;
height: 26px;
border-radius: 0px 1px 1px 0px;
background: var(--bg-ink-300, #16181d);
}
}
}
.selected-item-content-container {
.add-new-column-header {
padding: 8px;
}
.title {
color: var(--bg-slate-200, #52575c);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
.lucide {
color: var(--bg-vanilla-400, #c0c1c3);
cursor: pointer;
}
}
.horizontal-line {
height: 1px;
background: #1d212d;
}
.loading-container {
margin: 12px 0;
}
.item-content {
padding: 12px;
.column-format,
.column-format-new-options {
display: flex;
gap: 12px;
flex-direction: column;
margin-top: 12px;
.column-name {
color: var(--bg-vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
cursor: pointer;
.name {
flex: 1;
overflow: hidden;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.delete-btn {
display: none;
flex: 0 0 16px;
cursor: pointer;
}
&:hover {
.delete-btn {
display: block;
}
}
}
overflow-x: hidden;
&::-webkit-scrollbar {
height: 1rem;
width: 0.2rem;
}
}
.column-format {
max-height: 150px;
overflow: auto;
overflow-x: hidden;
}
.column-format-new-options {
max-height: 150px;
overflow-y: auto;
overflow-x: hidden;
}
.column-divider {
margin: 12px 0;
border-top: 2px solid var(--bg-slate-400);
}
}
}
&.active {
.nested-menu-container {
backdrop-filter: blur(18px);
.item {
.item-label {
color: var(--bg-vanilla-400);
}
}
}
.selected-item-content-container {
width: 110%;
margin-left: -5%;
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.column-format {
margin-top: 0px;
}
}
}
}
.lightMode {
.nested-menu-container {
border: 1px solid var(--bg-vanilla-300);
background: linear-gradient(
139deg,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
.horizontal-line {
background: var(--bg-vanilla-300);
}
.item-content {
.column-divider {
border-top: 2px solid var(--bg-vanilla-300);
}
}
.max-lines-per-row {
.title {
color: var(--bg-ink-200);
.lucide {
color: var(--bg-ink-300);
}
}
.max-lines-per-row-input {
border: 1px solid var(--bg-vanilla-300);
.periscope-btn {
background: var(--bg-vanilla-300);
}
}
}
.menu-container {
.title {
color: var(--bg-ink-200);
}
.item {
.item-label {
color: var(--bg-ink-400);
}
}
}
.selected-item-content-container {
.title {
color: var(--bg-ink-200);
.lucide {
color: var(--bg-ink-300);
}
}
.horizontal-line {
background: var(--bg-vanilla-300);
}
.item-content {
.max-lines-per-row-input {
border: 1px solid var(--bg-vanilla-300);
.periscope-btn {
background: var(--bg-vanilla-300);
}
}
.column-format,
.column-format-new-options {
.column-name {
color: var(--bg-ink-300);
}
}
}
}
&.active {
.nested-menu-container {
backdrop-filter: blur(18px);
.item {
.item-label {
color: var(--bg-ink-300);
}
}
}
.selected-item-content-container {
border: 1px solid var(--bg-vanilla-300);
background: linear-gradient(
139deg,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
}
}
}
}

View File

@@ -0,0 +1,248 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './LogsFormatOptionsMenu.styles.scss';
import { Divider, Input, InputNumber, Tooltip } from 'antd';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, Minus, Plus, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
interface LogsFormatOptionsMenuProps {
title: string;
items: any;
selectedOptionFormat: any;
config: OptionsMenuConfig;
}
export default function LogsFormatOptionsMenu({
title,
items,
selectedOptionFormat,
config,
}: LogsFormatOptionsMenuProps): JSX.Element {
const { maxLines, format, addColumn } = config;
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
const maxLinesNumber = (maxLines?.value as number) || 1;
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
const [addNewColumn, setAddNewColumn] = useState(false);
const onChange = useCallback(
(key: LogViewMode) => {
if (!format) return;
format.onChange(key);
},
[format],
);
const handleMenuItemClick = (key: LogViewMode): void => {
setSelectedItem(key);
onChange(key);
setAddNewColumn(false);
};
const incrementMaxLinesPerRow = (): void => {
if (maxLinesPerRow < 10) {
setMaxLinesPerRow(maxLinesPerRow + 1);
}
};
const decrementMaxLinesPerRow = (): void => {
if (maxLinesPerRow > 1) {
setMaxLinesPerRow(maxLinesPerRow - 1);
}
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const value = event?.target?.value || '';
if (addColumn && addColumn?.onSearch) {
addColumn?.onSearch(value);
}
}, 300);
const handleToggleAddNewColumn = (): void => {
setAddNewColumn(!addNewColumn);
};
// console.log('optionsMenuConfig', config);
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
if (
maxLinesPerRow &&
Number.isInteger(maxLinesNumber) &&
maxLinesPerRow > 1
) {
setMaxLinesPerRow(maxLinesPerRow);
}
};
useEffect(() => {
if (maxLinesPerRow && config && config.maxLines?.onChange) {
config.maxLines.onChange(maxLinesPerRow);
}
}, [maxLinesPerRow]);
return (
<div
className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
onClick={(event): void => {
// this is to restrict click events to propogate to parent
event.stopPropagation();
}}
>
<div className="menu-container">
<div className="title"> {title} </div>
<div className="menu-items">
{items.map(
(item: any): JSX.Element => (
<div
className="item"
key={item.label}
onClick={(): void => handleMenuItemClick(item.key)}
>
<div className={cx('item-label')}>
{item.label}
{selectedItem === item.key && <Check size={12} />}
</div>
</div>
),
)}
</div>
</div>
{selectedItem && (
<>
{selectedItem === 'raw' && (
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</div>
</>
)}
<div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />}
{addNewColumn && (
<div className="add-new-column-header">
<div className="title">
{' '}
columns
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
)}
<div className="item-content">
{!addNewColumn && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
)}
<div className="column-format">
{addColumn?.value?.map(({ key, id }) => (
<div className="column-name" key={id}>
<div className="name">
<Tooltip placement="left" title={key}>
{key}
</Tooltip>
</div>
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(id as string)}
/>
</div>
))}
</div>
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
{addNewColumn &&
addColumn &&
addColumn.value.length > 0 &&
addColumn.options &&
addColumn?.options?.length > 0 && (
<Divider className="column-divider" />
)}
{addNewColumn && (
<div className="column-format-new-options">
{addColumn?.options?.map(({ label, value }) => (
<div
className="column-name"
key={value}
onClick={(eve): void => {
console.log('coluimn name', label, value);
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(value, { label, disabled: false });
}
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
)}
</div>
</div>
</>
)}
</div>
);
}

View File

@@ -25,11 +25,12 @@ const allComponentMap: ComponentMapType[] = [
if (!path) {
return false;
}
const allowedPaths = [
const allowedPaths: string[] = [
ROUTES.LIST_ALL_ALERT,
ROUTES.APPLICATION,
ROUTES.ALL_DASHBOARD,
];
return (
userFlags?.ReleaseNote0120Hide !== 'Y' &&
allowedPaths.includes(path) &&

View File

@@ -73,12 +73,21 @@ function ResizeTable({
}
}, [columns]);
const paginationConfig = tableParams.pagination
? {
hideOnSinglePage: true,
showTotal: (total: number, range: number[]): string =>
`${range[0]}-${range[1]} of ${total} items`,
...tableParams.pagination,
}
: tableParams.pagination;
return onDragColumn ? (
<ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}>
<Table {...tableParams} />
<Table {...tableParams} pagination={paginationConfig} />
</ReactDragListView.DragColumn>
) : (
<Table {...tableParams} />
<Table {...tableParams} pagination={paginationConfig} />
);
}

View File

@@ -1,13 +1,15 @@
import { TabsProps } from 'antd';
import { History } from 'history';
export type TabRoutes = {
name: React.ReactNode;
route: string;
Component: () => JSX.Element;
key: string;
};
export interface RouteTabProps {
routes: {
name: React.ReactNode;
route: string;
Component: () => JSX.Element;
key: string;
}[];
routes: TabRoutes[];
activeKey: TabsProps['activeKey'];
onChangeHandler?: VoidFunction;
history: History<unknown>;

View File

@@ -24,6 +24,7 @@ const ROUTES = {
MY_SETTINGS: '/my-settings',
SETTINGS: '/settings',
ORG_SETTINGS: '/settings/org-settings',
API_KEYS: '/settings/api-keys',
INGESTION_SETTINGS: '/settings/ingestion-settings',
SOMETHING_WENT_WRONG: '/something-went-wrong',
UN_AUTHORIZED: '/un-authorized',
@@ -41,7 +42,9 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/billing',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs-save-views',
TRACES_SAVE_VIEWS: '/traces-save-views',
WORKSPACE_LOCKED: '/workspace-locked',
};
} as const;
export default ROUTES;

View File

@@ -0,0 +1,447 @@
.api-key-container {
margin-top: 32px;
display: flex;
justify-content: center;
width: 100%;
.api-key-content {
width: calc(100% - 30px);
max-width: 736px;
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
}
.subtitle {
color: var(---bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.api-keys-search-add-new {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 0;
.add-new-api-key-btn {
display: flex;
align-items: center;
gap: 8px;
}
}
.ant-table-row {
.ant-table-cell {
padding: 0;
border: none;
background: var(--bg-ink-500);
}
.column-render {
margin: 8px 0 !important;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.title-with-action {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
.api-key-data {
display: flex;
gap: 8px;
align-items: center;
.api-key-title {
display: flex;
align-items: center;
gap: 6px;
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
}
}
.api-key-value {
display: flex;
align-items: center;
gap: 12px;
border-radius: 20px;
padding: 0px 12px;
background: var(--bg-ink-200, #23262e);
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-xs);
font-family: 'Space Mono', monospace;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
}
.copy-key-btn {
cursor: pointer;
}
}
}
.action-btn {
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
}
.visibility-btn {
border: 1px solid rgba(113, 144, 249, 0.2);
background: rgba(113, 144, 249, 0.1);
}
}
.api-key-details {
display: flex;
align-items: center;
border-top: 1px solid var(--bg-slate-500, #161922);
padding: 8px;
.api-key-tag {
width: 14px;
height: 14px;
border-radius: 50px;
background: var(--bg-slate-300);
display: flex;
justify-content: center;
align-items: center;
.tag-text {
color: var(--bg-vanilla-400);
leading-trim: both;
text-edge: cap;
font-size: 10px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: normal;
letter-spacing: -0.05px;
}
}
.api-key-created-by {
margin-left: 8px;
}
.api-key-created-at {
display: flex;
align-items: center;
gap: 8px;
.ant-typography {
margin-left: 6px;
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
}
}
}
}
}
.ant-pagination-item {
display: flex;
justify-content: center;
align-items: center;
> a {
color: var(--bg-vanilla-400);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
}
}
.ant-pagination-item-active {
background-color: var(--bg-robin-500);
> a {
color: var(--bg-ink-500) !important;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
}
}
}
}
.api-key-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0;
.ant-modal-header {
background: none;
border-bottom: 1px solid var(--Slate-500, #161922);
padding: 16px;
}
.ant-modal-close-x {
font-size: 12px;
}
.ant-modal-body {
padding: 12px 16px;
}
.ant-modal-footer {
padding: 16px;
margin-top: 0;
display: flex;
justify-content: flex-end;
}
}
}
.api-key-access-role {
.ant-radio-button-wrapper {
font-size: 12px;
text-transform: capitalize;
}
.tab {
border: 1px solid var(--bg-slate-400);
&::before {
background: var(--bg-slate-400);
}
&.selected {
background: var(--Slate-400, #1d212d);
}
}
.role {
display: flex;
align-items: center;
gap: 8px;
}
}
.delete-api-key-modal {
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--bg-ink-400);
}
.ant-modal-body {
padding: 0px 16px 28px 16px;
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}
.api-key-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
.cancel-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-slate-500);
}
.delete-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-cherry-500);
margin-left: 12px;
}
.delete-btn:hover {
color: var(--bg-vanilla-100);
background: var(--bg-cherry-600);
}
}
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
}
}
.lightMode {
.api-key-container {
.api-key-content {
.title {
color: var(--bg-ink-500);
}
.ant-table-row {
.ant-table-cell {
background: var(--bg-vanilla-200);
}
&:hover {
.ant-table-cell {
background: var(--bg-vanilla-200) !important;
}
}
.column-render {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.title-with-action {
.api-key-title {
.ant-typography {
color: var(--bg-ink-500);
}
}
.action-btn {
.ant-typography {
color: var(--bg-ink-500);
}
}
}
.api-key-details {
.api-key-tag {
background: var(--bg-vanilla-200);
.tag-text {
color: var(--bg-ink-500);
}
}
.api-key-created-by {
color: var(--bg-ink-500);
}
.api-key-created-at {
.ant-typography {
color: var(--bg-ink-500);
}
}
}
}
}
}
}
.delete-api-key-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
.title {
color: var(--bg-ink-500);
}
}
.ant-modal-body {
.ant-typography {
color: var(--bg-ink-500);
}
.api-key-input {
.ant-input {
background: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
}
.ant-modal-footer {
.cancel-btn {
background: var(--bg-vanilla-300);
color: var(--bg-ink-400);
}
}
}
}
}

View File

@@ -0,0 +1,412 @@
import './APIKeys.styles.scss';
import { Color } from '@signozhq/design-tokens';
import {
Button,
Flex,
Form,
Input,
InputNumber,
Modal,
Radio,
Table,
TableProps,
Typography,
} from 'antd';
import cx from 'classnames';
import { getRandomColor } from 'container/ExplorerOptions/utils';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
import useErrorNotification from 'hooks/useErrorNotification';
import {
CalendarClock,
Check,
ClipboardEdit,
Contact2,
Copy,
Eye,
PenLine,
Plus,
Search,
Trash2,
X,
} from 'lucide-react';
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
function APIKeys(): JSX.Element {
const sourcepage = 'traces';
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [activeViewKey, setActiveViewKey] = useState<string>('');
const [newViewName, setNewViewName] = useState<string>('');
const [color, setColor] = useState(Color.BG_SIENNA_500);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [activeViewName, setActiveViewName] = useState<string>('');
const [
activeCompositeQuery,
setActiveCompositeQuery,
] = useState<ICompositeMetricQuery | null>(null);
const [searchValue, setSearchValue] = useState<string>('');
const [dataSource, setDataSource] = useState<ViewProps[]>([]);
const { t } = useTranslation(['apiKeys']);
const [form] = Form.useForm();
const hideDeleteViewModal = (): void => {
setIsDeleteModalOpen(false);
};
const handleDeleteModelOpen = (uuid: string, name: string): void => {
setActiveViewKey(uuid);
setActiveViewName(name);
setIsDeleteModalOpen(true);
};
const hideEditViewModal = (): void => {
setIsEditModalOpen(false);
};
const hideAddViewModal = (): void => {
setIsAddModalOpen(false);
};
const handleEditModelOpen = (view: ViewProps, color: string): void => {
setActiveViewKey(view.uuid);
setColor(color);
setActiveViewName(view.name);
setNewViewName(view.name);
setActiveCompositeQuery(view.compositeQuery);
setIsEditModalOpen(true);
};
const handleAddModelOpen = (): void => {
setIsAddModalOpen(true);
};
const { data: viewsData, isLoading, error, isRefetching } = useGetAllViews(
sourcepage as DataSource,
);
useEffect(() => {
setDataSource(viewsData?.data.data || []);
}, [viewsData?.data.data]);
useErrorNotification(error);
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
setSearchValue(e.target.value);
const filteredData = viewsData?.data.data.filter((view) =>
view.name.toLowerCase().includes(e.target.value.toLowerCase()),
);
setDataSource(filteredData || []);
};
const clearSearch = (): void => {
setSearchValue('');
};
const onDeleteHandler = (): void => {
console.log('on delete handler');
clearSearch();
};
const onUpdateApiKey = (): void => {
console.log('update key');
};
const columns: TableProps<ViewProps>['columns'] = [
{
title: 'API Key',
key: 'api-key',
render: (view: ViewProps): JSX.Element => {
const extraData = view.extraData !== '' ? JSON.parse(view.extraData) : '';
let bgColor = getRandomColor();
if (extraData !== '') {
bgColor = extraData.color;
}
const timeOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
};
const formattedTime = new Date(view.createdAt).toLocaleTimeString(
'en-US',
timeOptions,
);
const dateOptions: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
const formattedDate = new Date(view.createdAt).toLocaleDateString(
'en-US',
dateOptions,
);
// Combine time and date
const formattedDateAndTime = `${formattedDate} ${formattedTime} `;
return (
<div className="column-render">
<div className="title-with-action">
<div className="api-key-data">
<div className="api-key-title">
<Typography.Text>{view.name}</Typography.Text>
</div>
<div className="api-key-value">
<Typography.Text>
{view.name.substring(0, 2)}********
{view.name.substring(view.name.length - 2).trim()}
</Typography.Text>
<Copy className="copy-key-btn" size={12} />
</div>
<Button
size="small"
className="periscope-btn primary visibility-btn"
shape="circle"
icon={<Eye size={12} color={Color.BG_ROBIN_400} />}
/>
</div>
<div className="action-btn">
<PenLine
size={14}
onClick={(): void => handleEditModelOpen(view, bgColor)}
/>
<Trash2
size={14}
color={Color.BG_CHERRY_500}
onClick={(): void => handleDeleteModelOpen(view.uuid, view.name)}
/>
</div>
</div>
<div className="api-key-details">
<div className="api-key-created-at">
<CalendarClock size={14} />
Last used
<Typography.Text>{formattedDateAndTime}</Typography.Text>
</div>
</div>
</div>
);
},
},
];
return (
<div className="api-key-container">
<div className="api-key-content">
<header>
<Typography.Title className="title">API Keys</Typography.Title>
<Typography.Text className="subtitle">
Create and manage access keys for the SigNoz API
</Typography.Text>
</header>
<div className="api-keys-search-add-new">
<Input
placeholder="Search for keys..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue}
onChange={handleSearch}
/>
<Button
className="add-new-api-key-btn"
type="primary"
onClick={handleAddModelOpen}
>
{' '}
<Plus size={14} /> New Key{' '}
</Button>
</div>
<Table
columns={columns}
dataSource={dataSource}
loading={isLoading || isRefetching}
showHeader={false}
pagination={{ pageSize: 5 }}
/>
</div>
<Modal
className="delete-api-key-modal"
title={<span className="title">Delete key</span>}
open={isDeleteModalOpen}
closable={false}
onCancel={hideDeleteViewModal}
footer={[
<Button
key="cancel"
onClick={hideDeleteViewModal}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
icon={<Trash2 size={16} />}
onClick={onDeleteHandler}
className="delete-btn"
>
Delete key
</Button>,
]}
>
<Typography.Text className="delete-text">
{t('delete_confirm_message', {
keyName: activeViewName,
})}
</Typography.Text>
</Modal>
<Modal
className="api-key-modal"
title="Edit key"
open={isEditModalOpen}
closable
onCancel={hideEditViewModal}
footer={[
<Button
className="periscope-btn primary"
key="submit"
type="primary"
icon={<Check size={14} />}
onClick={onUpdateApiKey}
>
Update key
</Button>,
]}
>
<Form form={form} layout="vertical" autoComplete="off">
<Form.Item
name="label"
label="Label"
rules={[
{ required: true },
{ type: 'url', warningOnly: true },
{ type: 'string', min: 6 },
]}
>
<Input placeholder="Top Secret" />
</Form.Item>
<Form.Item name="role" label="Role">
<Flex vertical gap="middle">
<Radio.Group buttonStyle="solid" className="api-key-access-role">
<Radio.Button value={USER_ROLES.ADMIN} className="tab">
<div className="role">
<Contact2 size={14} /> Admin
</div>
</Radio.Button>
<Radio.Button value={USER_ROLES.EDITOR} className="tab selected">
<div className="role">
{' '}
<ClipboardEdit size={14} /> Editor
</div>
</Radio.Button>
<Radio.Button value={USER_ROLES.EDITOR} className="tab">
<div className="role">
{' '}
<Eye size={14} /> Viewer
</div>
</Radio.Button>
</Radio.Group>
</Flex>
</Form.Item>
</Form>
</Modal>
<Modal
className="api-key-modal"
title="Create new key"
open={isAddModalOpen}
closable
onCancel={hideAddViewModal}
footer={[
<Button
className="periscope-btn primary"
key="submit"
type="primary"
icon={<Check size={14} />}
onClick={onUpdateApiKey}
>
Create new key
</Button>,
]}
>
<Form
form={form}
initialValues={{
role: USER_ROLES.ADMIN,
}}
layout="vertical"
autoComplete="off"
>
<Form.Item
name="label"
label="Label"
rules={[{ required: true }, { type: 'string', min: 6 }]}
>
<Input placeholder="Top Secret" />
</Form.Item>
<Form.Item name="role" label="Role">
<Flex vertical gap="middle">
<Radio.Group buttonStyle="solid" className="api-key-access-role">
<Radio.Button
value={USER_ROLES.ADMIN}
className={cx(
'tab',
form.getFieldValue('role') === USER_ROLES.ADMIN ? 'selected' : '',
)}
>
<div className="role">
<Contact2 size={14} /> Admin
</div>
</Radio.Button>
<Radio.Button value={USER_ROLES.VIEWER} className="tab">
<div className="role">
{' '}
<ClipboardEdit size={14} /> Editor
</div>
</Radio.Button>
<Radio.Button value={USER_ROLES.EDITOR} className="tab">
<div className="role">
{' '}
<Eye size={14} /> Viewer
</div>
</Radio.Button>
</Radio.Group>
</Flex>
</Form.Item>
<Form.Item
name="expiration"
label="Expiration"
rules={[{ required: true }]}
>
<InputNumber min={1} max={100} defaultValue={30} />
</Form.Item>
</Form>
</Modal>
</div>
);
}
export default APIKeys;

View File

@@ -1,12 +1,20 @@
@import '@signozhq/design-tokens';
.app-layout {
position: relative;
height: 100%;
width: 100%;
.app-content {
width: 100%;
overflow: auto;
.content-container {
position: relative;
margin: 0 1rem;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
}
}

View File

@@ -4,10 +4,12 @@
import './AppLayout.styles.scss';
import { Flex } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import ROUTES from 'constants/routes';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
@@ -16,7 +18,15 @@ import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import {
ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
@@ -24,6 +34,7 @@ import { useQueries } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { sideBarCollapse } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
@@ -44,6 +55,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
(state) => state.app,
);
const [collapsed, setCollapsed] = useState<boolean>(
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
const isDarkMode = useIsDarkMode();
const { data: licenseData, isFetching } = useLicense();
@@ -92,7 +107,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const { children } = props;
const dispatch = useDispatch<Dispatch<AppActions>>();
const dispatch = useDispatch<Dispatch<AppActions | any>>();
const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0);
@@ -100,6 +115,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const { notifications } = useNotifications();
const onCollapse = useCallback(() => {
setCollapsed((collapsed) => !collapsed);
}, []);
useLayoutEffect(() => {
dispatch(sideBarCollapse(collapsed));
}, [collapsed, dispatch]);
useEffect(() => {
if (
getUserLatestVersionResponse.isFetched &&
@@ -230,8 +253,34 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
};
const isLogsView = (): boolean =>
routeKey === 'LOGS' ||
routeKey === 'LOGS_EXPLORER' ||
routeKey === 'LOGS_PIPELINES' ||
routeKey === 'LOGS_SAVE_VIEWS';
const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
useEffect(() => {
if (isDarkMode) {
document.body.classList.remove('lightMode');
document.body.classList.add('darkMode');
} else {
document.body.classList.add('lightMode');
document.body.classList.remove('darkMode');
}
}, [isDarkMode]);
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
return (
<Layout className={isDarkMode ? 'darkMode' : 'lightMode'}>
<Layout
className={cx(
isDarkMode ? 'darkMode' : 'lightMode',
isSideNavCollapsed ? 'sidebarCollapsed' : '',
)}
>
<Helmet>
<title>{pageTitle}</title>
</Helmet>
@@ -259,12 +308,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
{isToDisplayLayout && !renderFullScreen && (
<SideNav licenseData={licenseData} isFetching={isFetching} />
<SideNav
licenseData={licenseData}
isFetching={isFetching}
onCollapse={onCollapse}
collapsed={collapsed}
/>
)}
<div className="app-content">
<div className={cx('app-content', collapsed ? 'collapsed' : '')}>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<LayoutContent>
<ChildrenContainer>
<ChildrenContainer
style={{
margin: isLogsView() || isTracesView() ? 0 : ' 0 1rem',
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>

View File

@@ -8,6 +8,7 @@ export const Layout = styled(LayoutComponent)`
min-height: calc(100vh - 8rem);
overflow: hidden;
height: 100%;
flex-direction: column !important;
}
`;
@@ -17,7 +18,6 @@ export const LayoutContent = styled(LayoutComponent.Content)`
`;
export const ChildrenContainer = styled.div`
margin: 0 1rem;
display: flex;
flex-direction: column;
height: 100%;

View File

@@ -1,4 +1,4 @@
import { Row } from 'antd';
import { Row, Typography } from 'antd';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -33,7 +33,14 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
return (
<SelectTypeContainer>
<h3> {t('choose_alert_type')} </h3>
<Typography.Title
level={4}
style={{
padding: '0 8px',
}}
>
{t('choose_alert_type')}
</Typography.Title>
<Row>{renderOptions}</Row>
</SelectTypeContainer>
);

View File

@@ -7,16 +7,18 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
const [formInstance] = Form.useForm();
return (
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
<div style={{ marginTop: '1rem' }}>
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
</div>
);
}

View File

@@ -0,0 +1,30 @@
.empty-logs-search-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 240px;
.empty-logs-search-container-content {
display: flex;
flex-direction: column;
gap: 4px;
color: var(--text-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
.empty-state-svg {
height: 50px;
width: 50px;
}
.sub-text {
font-weight: 600;
}
}
}

View File

@@ -0,0 +1,21 @@
import './EmptyLogsSearch.styles.scss';
import { Typography } from 'antd';
export default function EmptyLogsSearch(): JSX.Element {
return (
<div className="empty-logs-search-container">
<div className="empty-logs-search-container-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text>
<span className="sub-text">This query had no results. </span>
Edit your query and try again!
</Typography.Text>
</div>
</div>
);
}

View File

@@ -0,0 +1,307 @@
.explorer-update {
position: fixed;
bottom: 16px;
left: calc(50% - 225px);
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6);
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
.action-icon {
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
cursor: pointer;
}
.ant-divider {
margin: 0;
height: 28px;
border: 1px solid var(--bg-slate-400);
}
}
.explorer-options {
display: flex;
gap: 16px;
padding: 10px 12px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6);
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
position: fixed;
bottom: 16px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
.ant-select-selector {
padding: 0 !important;
}
hr {
border-color: #1d212d;
}
.view-options,
.actions {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
button {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
border: 1px solid #1d2023;
color: #c0c1c3;
background-color: #161922;
box-shadow: none !important;
&.ant-btn-round {
padding-inline-start: 10px;
padding-inline-end: 8px;
font-weight: 500;
}
&.ant-btn-round:disabled {
background-color: rgba(209, 209, 209, 0.074);
color: #5f5f5f;
}
}
.ant-select-focused {
border-color: transparent !important;
.ant-select-selector {
border-color: transparent !important;
box-shadow: none !important;
}
}
.ant-select-selector {
border: transparent !important;
background-color: transparent !important;
.ant-select-selection-placeholder {
margin-left: 12px;
}
}
}
}
.app-content {
&.collapsed {
.explorer-options {
left: calc(50% + 72px);
}
}
}
.render-options {
display: flex;
align-items: center;
gap: 8px;
padding: 0 2px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.dot {
margin-left: 6px;
min-height: 6px;
min-width: 6px;
border-radius: 50%;
backdrop-filter: blur(20px);
}
}
.save-view-modal {
width: 384px !important;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-500);
}
.ant-modal-body {
padding: 12px 16px 0px 16px;
.ant-typography {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
.save-view-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
> button {
display: flex;
align-items: center;
border-radius: 2px;
background-color: var(--bg-robin-500) !important;
color: var(--bg-vanilla-100) !important;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 24px;
}
}
}
.title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
.lightMode {
.explorer-options {
border: 1px solid var(--bg-vanilla-300);
background: rgba(255, 255, 255, 0.8);
box-shadow: 4px 4px 16px 4px rgba(255, 255, 255, 0.55);
backdrop-filter: blur(20px);
hr {
border-color: var(--bg-vanilla-300);
}
.view-options,
.actions {
button {
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-200);
background-color: var(--bg-vanilla-300);
}
}
}
.render-options {
color: var(--bg-ink-200);
}
.explorer-update {
border: 1px solid var(--bg-vanilla-300);
background: transparent;
box-shadow: 4px 4px 16px 4px rgba(255, 255, 255, 0.55);
backdrop-filter: blur(20px);
.action-icon {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
}
.ant-divider {
border-color: var(--bg-vanilla-300);
}
}
.ant-tooltip-arrow {
border-top-color: var(--bg-vanilla-300) !important;
}
.ant-tooltip-inner {
background-color: var(--bg-vanilla-300);
color: var(--bg-ink-200);
}
.save-view-modal {
.ant-modal-content {
background: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-300);
.ant-modal-header {
background: var(--bg-vanilla-200);
border-bottom: 1px solid var(--bg-vanilla-300);
}
.ant-modal-body {
.ant-typography {
color: var(--bg-ink-200);
}
.ant-color-picker-trigger {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-color-picker-color-block {
.ant-color-picker-color-block-inner {
svg {
fill: var(--bg-ink-200);
}
}
}
}
}
}
.title {
color: var(--bg-ink-200);
}
}
}

View File

@@ -0,0 +1,403 @@
import './ExplorerOptions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import {
Button,
ColorPicker,
Divider,
Input,
Modal,
RefSelectProps,
Select,
Tooltip,
Typography,
} from 'antd';
import axios from 'axios';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
import { useSaveView } from 'hooks/saveViews/useSaveView';
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useErrorNotification from 'hooks/useErrorNotification';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { Check, ConciergeBell, Disc3, Plus, X } from 'lucide-react';
import { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
DATASOURCE_VS_ROUTES,
generateRGBAFromHex,
getRandomColor,
saveNewViewHandler,
} from './utils';
function ExplorerOptions({
disabled,
isLoading,
onExport,
query,
sourcepage,
}: ExplorerOptionsProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
const [newViewName, setNewViewName] = useState<string>('');
const [color, setColor] = useState(Color.BG_SIENNA_500);
const { notifications } = useNotifications();
const history = useHistory();
const ref = useRef<RefSelectProps>(null);
const isDarkMode = useIsDarkMode();
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
}, []);
const handleSaveViewModalToggle = (): void => {
setIsSaveModalOpen(!isSaveModalOpen);
};
const hideSaveViewModal = (): void => {
setIsSaveModalOpen(false);
};
const onCreateAlertsHandler = useCallback(() => {
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(query),
)}`,
);
}, [history, query]);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);
};
const onAddToDashboard = (): void => {
setIsExport(true);
};
const {
data: viewsData,
isLoading: viewsIsLoading,
error,
isRefetching,
refetch: refetchAllView,
} = useGetAllViews(sourcepage);
const {
currentQuery,
panelType,
isStagedQueryUpdated,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
const extraData = viewsData?.data.data.find((view) => view.uuid === viewKey)
?.extraData;
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
const rgbaColor = generateRGBAFromHex(
extraDataColor || Color.BG_SIENNA_500,
0.08,
);
const {
mutateAsync: updateViewAsync,
isLoading: isViewUpdating,
} = useUpdateView({
compositeQuery,
viewKey,
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
sourcePage: sourcepage,
viewName,
});
const showErrorNotification = (err: Error): void => {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
};
const onUpdateQueryHandler = (): void => {
const extraData = viewsData?.data.data.find((view) => view.uuid === viewKey)
?.extraData;
updateViewAsync(
{
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
sourcePage: sourcepage,
viewName,
},
{
onSuccess: () => {
notifications.success({
message: 'View Updated Successfully',
});
refetchAllView();
},
onError: (err) => {
showErrorNotification(err);
},
},
);
};
useErrorNotification(error);
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
const currentViewDetails = getViewDetailsUsingViewKey(
key,
viewsData?.data.data,
);
if (!currentViewDetails) return;
const {
query,
name,
uuid,
panelType: currentPanelType,
} = currentViewDetails;
handleExplorerTabChange(currentPanelType, {
query,
name,
uuid,
});
},
[viewsData, handleExplorerTabChange],
);
const handleSelect = (
value: string,
option: { key: string; value: string },
): void => {
onMenuItemSelectHandler({
key: option.key,
});
if (ref.current) {
ref.current.blur();
}
};
const handleClearSelect = (): void => {
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
};
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
const {
isLoading: isSaveViewLoading,
mutateAsync: saveViewAsync,
} = useSaveView({
viewName: newViewName || '',
compositeQuery,
sourcePage: sourcepage,
extraData: JSON.stringify({ color }),
});
const onSaveHandler = (): void => {
saveNewViewHandler({
compositeQuery,
handlePopOverClose: hideSaveViewModal,
extraData: JSON.stringify({ color }),
notifications,
panelType: panelType || PANEL_TYPES.LIST,
redirectWithQueryBuilderData,
refetchAllView,
saveViewAsync,
sourcePage: sourcepage,
viewName: newViewName,
setNewViewName,
});
};
// TODO: Remove this and move this to scss file
const dropdownStyle: CSSProperties = useMemo(
() => ({
borderRadius: '4px',
border: isDarkMode
? `1px solid ${Color.BG_SLATE_400}`
: `1px solid ${Color.BG_VANILLA_300}`,
background: isDarkMode
? 'linear-gradient(139deg, rgba(18, 19, 23, 0.80) 0%, rgba(18, 19, 23, 0.90) 98.68%)'
: 'linear-gradient(139deg, rgba(241, 241, 241, 0.8) 0%, rgba(241, 241, 241, 0.9) 98.68%)',
boxShadow: '4px 10px 16px 2px rgba(0, 0, 0, 0.20)',
backdropFilter: 'blur(20px)',
bottom: '74px',
width: '191px',
}),
[isDarkMode],
);
return (
<>
{isQueryUpdated && (
<div className="explorer-update">
<Tooltip title="Clear this view" placement="top">
<Button
className="action-icon"
onClick={handleClearSelect}
icon={<X size={14} />}
/>
</Tooltip>
<Divider type="vertical" />
<Tooltip title="Update this view" placement="top">
<Button
className="action-icon"
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />}
/>
</Tooltip>
</div>
)}
<div
className="explorer-options"
style={{
background: extraData
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
: 'transparent',
backdropFilter: 'blur(20px)',
}}
>
<div className="view-options">
<Select<string, { key: string; value: string }>
showSearch
placeholder="Select a view"
loading={viewsIsLoading || isRefetching}
value={viewName || undefined}
onSelect={handleSelect}
style={{
minWidth: 170,
}}
dropdownStyle={dropdownStyle}
className="views-dropdown"
allowClear
onClear={handleClearSelect}
ref={ref}
>
{viewsData?.data.data.map((view) => {
const extraData =
view.extraData !== '' ? JSON.parse(view.extraData) : '';
let bgColor = getRandomColor();
if (extraData !== '') {
bgColor = extraData.color;
}
return (
<Select.Option key={view.uuid} value={view.name}>
<div className="render-options">
<span
className="dot"
style={{
background: bgColor,
boxShadow: `0px 0px 6px 0px ${bgColor}`,
}}
/>{' '}
{view.name}
</div>
</Select.Option>
);
})}
</Select>
<Button
shape="round"
onClick={handleSaveViewModalToggle}
disabled={viewsIsLoading || isRefetching}
>
<Disc3 size={16} /> Save this view
</Button>
</div>
<hr />
<div className="actions">
<Button disabled={disabled} shape="circle" onClick={onCreateAlertsHandler}>
<ConciergeBell size={16} />
</Button>
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
<Plus size={16} />
</Button>
</div>
</div>
<Modal
className="save-view-modal"
title={<span className="title">Save this view</span>}
open={isSaveModalOpen}
closable
onCancel={hideSaveViewModal}
footer={[
<Button
key="submit"
type="primary"
icon={<Check size={16} />}
onClick={onSaveHandler}
disabled={isSaveViewLoading}
>
Save this view
</Button>,
]}
>
<Typography.Text>Label</Typography.Text>
<div className="save-view-input">
<ColorPicker
value={color}
onChange={(value, hex): void => setColor(hex)}
/>
<Input
placeholder="e.g. External http method view"
value={newViewName}
onChange={(e): void => setNewViewName(e.target.value)}
/>
</div>
</Modal>
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
destroyOnClose
>
<ExportPanelContainer
query={query}
isLoading={isLoading}
onExport={onExport}
/>
</Modal>
</>
);
}
export interface ExplorerOptionsProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null) => void;
query: Query | null;
disabled: boolean;
sourcepage: DataSource;
}
ExplorerOptions.defaultProps = { isLoading: false };
export default ExplorerOptions;

View File

@@ -0,0 +1,28 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { SaveViewWithNameProps } from 'components/ExplorerCard/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Dispatch, SetStateAction } from 'react';
import { UseMutateAsyncFunction } from 'react-query';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
export interface SaveNewViewHandlerProps {
viewName: string;
compositeQuery: ICompositeMetricQuery;
sourcePage: DataSource;
extraData: SaveViewProps['extraData'];
panelType: PANEL_TYPES | null;
notifications: NotificationInstance;
refetchAllView: SaveViewWithNameProps['refetchAllView'];
saveViewAsync: UseMutateAsyncFunction<
AxiosResponse<SaveViewPayloadProps>,
Error,
SaveViewProps,
SaveViewPayloadProps
>;
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
setNewViewName: Dispatch<SetStateAction<string>>;
}

View File

@@ -0,0 +1,69 @@
import { Color } from '@signozhq/design-tokens';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { DataSource } from 'types/common/queryBuilder';
import { SaveNewViewHandlerProps } from './types';
export const getRandomColor = (): Color => {
const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
return Color[randomKey];
};
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
[DataSource.METRICS]: '',
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
};
export const saveNewViewHandler = ({
saveViewAsync,
refetchAllView,
notifications,
handlePopOverClose,
viewName,
compositeQuery,
sourcePage,
extraData,
redirectWithQueryBuilderData,
panelType,
setNewViewName,
}: SaveNewViewHandlerProps): void => {
saveViewAsync(
{
viewName,
compositeQuery,
sourcePage,
extraData,
},
{
onSuccess: (data) => {
refetchAllView();
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
[QueryParams.panelTypes]: panelType,
[QueryParams.viewName]: viewName,
[QueryParams.viewKey]: data.data.data,
});
notifications.success({
message: 'View Saved Successfully',
});
},
onError: (err) => {
showErrorNotification(notifications, err);
},
onSettled: () => {
handlePopOverClose();
setNewViewName('');
},
},
);
};
export const generateRGBAFromHex = (hex: string, opacity: number): string =>
`rgba(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(
hex.slice(3, 5),
16,
)}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`;

View File

@@ -16,7 +16,10 @@ import {
} from './styles';
import { filterOptions, getSelectOptions } from './utils';
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
function ExportPanelContainer({
isLoading,
onExport,
}: ExportPanelProps): JSX.Element {
const { t } = useTranslation(['dashboard']);
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
@@ -118,4 +121,4 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
);
}
export default ExportPanel;
export default ExportPanelContainer;

View File

@@ -1,13 +1,9 @@
import { AlertOutlined, AreaChartOutlined } from '@ant-design/icons';
import { Button, Modal, Space } from 'antd';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { Modal } from 'antd';
import { useCallback, useState } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import ExportPanelContainer from './ExportPanel';
import ExportPanelContainer from './ExportPanelContainer';
function ExportPanel({
isLoading,
@@ -20,53 +16,25 @@ function ExportPanel({
setIsExport(value);
}, []);
const onCreateAlertsHandler = useCallback(() => {
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(query),
)}`,
);
}, [query]);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);
};
const onAddToDashboard = (): void => {
setIsExport(true);
};
return (
<>
<Space size={24}>
<Button
icon={<AreaChartOutlined />}
onClick={onAddToDashboard}
type="primary"
>
Add to Dashboard
</Button>
<Button onClick={onCreateAlertsHandler} icon={<AlertOutlined />}>
Setup Alerts
</Button>
</Space>
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
destroyOnClose
>
<ExportPanelContainer
query={query}
isLoading={isLoading}
onExport={onExport}
/>
</Modal>
</>
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
destroyOnClose
>
<ExportPanelContainer
query={query}
isLoading={isLoading}
onExport={onExport}
/>
</Modal>
);
}

View File

@@ -1,4 +1,5 @@
import { Form, Select } from 'antd';
import { Form, Select, Switch } from 'antd';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertDef, Labels } from 'types/api/alerts/def';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
@@ -7,7 +8,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
import ChannelSelect from './ChannelSelect';
import LabelSelect from './labels';
import {
ChannelSelectTip,
FormContainer,
FormItemMedium,
InputSmall,
@@ -19,14 +19,41 @@ import {
const { Option } = Select;
interface BasicInfoProps {
isNewRule: boolean;
alertDef: AlertDef;
setAlertDef: (a: AlertDef) => void;
}
function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
// init namespace for translations
function BasicInfo({
isNewRule,
alertDef,
setAlertDef,
}: BasicInfoProps): JSX.Element {
const { t } = useTranslation('alerts');
const [
shouldBroadCastToAllChannels,
setShouldBroadCastToAllChannels,
] = useState(false);
useEffect(() => {
const hasPreferredChannels =
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0) ||
isNewRule;
setShouldBroadCastToAllChannels(!hasPreferredChannels);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleBroadcastToAllChannels = (shouldBroadcast: boolean): void => {
setShouldBroadCastToAllChannels(shouldBroadcast);
setAlertDef({
...alertDef,
broadcastToAll: shouldBroadcast,
});
};
return (
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>
@@ -105,18 +132,38 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
initialValues={alertDef.labels}
/>
</FormItemMedium>
<FormItemMedium label="Notification Channels">
<ChannelSelect
currentValue={alertDef.preferredChannels}
onSelectChannels={(preferredChannels): void => {
setAlertDef({
...alertDef,
preferredChannels,
});
}}
<FormItemMedium
name="alert_all_configured_channels"
label="Alert all the configured channels"
>
<Switch
checked={shouldBroadCastToAllChannels}
onChange={handleBroadcastToAllChannels}
/>
<ChannelSelectTip> {t('channel_select_tooltip')}</ChannelSelectTip>
</FormItemMedium>
{!shouldBroadCastToAllChannels && (
<FormItemMedium
label="Notification Channels"
name="notification_channels"
required
rules={[
{ required: true, message: requireErrorMessage(t('field_alert_name')) },
]}
>
<ChannelSelect
disabled={shouldBroadCastToAllChannels}
currentValue={alertDef.preferredChannels}
onSelectChannels={(preferredChannels): void => {
setAlertDef({
...alertDef,
preferredChannels,
});
}}
/>
</FormItemMedium>
)}
</FormContainer>
</>
);

View File

@@ -8,11 +8,13 @@ import { useTranslation } from 'react-i18next';
import { StyledSelect } from './styles';
export interface ChannelSelectProps {
disabled?: boolean;
currentValue?: string[];
onSelectChannels: (s: string[]) => void;
}
function ChannelSelect({
disabled,
currentValue,
onSelectChannels,
}: ChannelSelectProps): JSX.Element | null {
@@ -52,6 +54,7 @@ function ChannelSelect({
};
return (
<StyledSelect
disabled={disabled}
status={error ? 'error' : ''}
mode="multiple"
style={{ width: '100%' }}
@@ -68,6 +71,7 @@ function ChannelSelect({
}
ChannelSelect.defaultProps = {
disabled: false,
currentValue: [],
};
export default ChannelSelect;

View File

@@ -5,6 +5,7 @@ import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -28,7 +29,7 @@ export interface ChartPreviewProps {
query: Query | null;
graphType?: PANEL_TYPES;
selectedTime?: timePreferenceType;
selectedInterval?: Time;
selectedInterval?: Time | TimeV2;
headline?: JSX.Element;
alertDef?: AlertDef;
userQueryKey?: string;

View File

@@ -0,0 +1,56 @@
.alert-tabs {
.ant-tabs-tab {
border: none !important;
margin-left: 0px !important;
padding: 0px !important;
.nav-btns {
display: flex;
align-items: center;
justify-content: center;
}
.ant-btn-default {
border-color: transparent;
}
}
.ant-tabs-tab-active {
.nav-btns {
background: var(--bg-slate-400) !important;
}
}
.ant-tabs-nav {
margin: 0px;
margin-bottom: 0.5rem;
}
.ant-tabs-nav::before {
border-bottom: none !important;
}
.ant-tabs-nav-list {
border: 1px solid var(--bg-slate-200);
}
.ant-tabs-tab + .ant-tabs-tab {
border-left: 1px solid var(--bg-slate-200) !important;
}
.stage-run-query {
display: flex;
align-items: center;
}
}
.lightMode {
.alert-tabs {
.ant-tabs-nav-list {
border: 1px solid var(--bg-vanilla-300);
}
.ant-tabs-tab + .ant-tabs-tab {
border-left: 1px solid var(--bg-vanilla-200) !important;
}
.ant-tabs-tab-active {
.nav-btns {
background: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -1,8 +1,11 @@
import './QuerySection.styles.scss';
import { Button, Tabs } from 'antd';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { useMemo } from 'react';
import { Atom, LucideAccessibility, Play, Terminal } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -22,6 +25,7 @@ function QuerySection({
}: QuerySectionProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const [currentTab, setCurrentTab] = useState(queryCategory);
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
@@ -31,6 +35,7 @@ function QuerySection({
featureResponse.refetch().then(() => {
setQueryCategory(queryType as EQueryType);
});
setCurrentTab(queryType as EQueryType);
};
const renderPromqlUI = (): JSX.Element => <PromqlSection />;
@@ -49,22 +54,51 @@ function QuerySection({
const tabs = [
{
label: t('tab_qb'),
label: (
<Button className="nav-btns">
<Atom size={14} />
</Button>
),
key: EQueryType.QUERY_BUILDER,
},
{
label: t('tab_chquery'),
label: (
<Button className="nav-btns">
<Terminal size={14} />
</Button>
),
key: EQueryType.CLICKHOUSE,
},
];
const items = useMemo(
() => [
{ label: t('tab_qb'), key: EQueryType.QUERY_BUILDER },
{ label: t('tab_chquery'), key: EQueryType.CLICKHOUSE },
{ label: t('tab_promql'), key: EQueryType.PROM },
{
label: (
<Button className="nav-btns">
<Atom size={14} />
</Button>
),
key: EQueryType.QUERY_BUILDER,
},
{
label: (
<Button className="nav-btns">
<Terminal size={14} />
</Button>
),
key: EQueryType.CLICKHOUSE,
},
{
label: (
<Button className="nav-btns">
<LucideAccessibility size={14} />
</Button>
),
key: EQueryType.PROM,
},
],
[t],
[],
);
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
@@ -73,40 +107,54 @@ function QuerySection({
case AlertTypes.LOGS_BASED_ALERT:
case AlertTypes.EXCEPTIONS_BASED_ALERT:
return (
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER}
activeKey={queryCategory}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button type="primary" onClick={runQuery}>
Run Query
</Button>
</span>
}
items={tabs}
/>
<div className="alert-tabs">
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={currentTab}
activeKey={currentTab}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={runQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
</span>
}
items={tabs}
/>
</div>
);
case AlertTypes.METRICS_BASED_ALERT:
default:
return (
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER}
activeKey={queryCategory}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button type="primary" onClick={runQuery}>
Run Query
</Button>
</span>
}
items={items}
/>
<div className="alert-tabs">
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={currentTab}
activeKey={currentTab}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={runQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
</span>
}
items={items}
/>
</div>
);
}
};
@@ -126,8 +174,8 @@ function QuerySection({
<>
<StepHeading> {t('alert_form_step1')}</StepHeading>
<FormContainer>
<div style={{ display: 'flex' }}>{renderTabs(alertType)}</div>
{renderQuerySection(queryCategory)}
<div>{renderTabs(alertType)}</div>
{renderQuerySection(currentTab)}
</FormContainer>
</>
);

View File

@@ -53,6 +53,7 @@ import {
import UserGuide from './UserGuide';
import { getSelectedQueryOptions } from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function FormAlertRules({
alertType,
formInstance,
@@ -78,6 +79,8 @@ function FormAlertRules({
// use query client
const ruleCache = useQueryClient();
const isNewRule = ruleId === 0;
const [loading, setLoading] = useState(false);
// alertDef holds the form values to be posted
@@ -108,8 +111,17 @@ function FormAlertRules({
useShareBuilderUrl(sq);
useEffect(() => {
setAlertDef(initialValue);
}, [initialValue]);
const broadcastToSpecificChannels =
(initialValue &&
initialValue.preferredChannels &&
initialValue.preferredChannels.length > 0) ||
isNewRule;
setAlertDef({
...initialValue,
broadcastToAll: !broadcastToSpecificChannels,
});
}, [initialValue, isNewRule]);
useEffect(() => {
// Set selectedQueryName based on the length of queryOptions
@@ -130,6 +142,10 @@ function FormAlertRules({
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults
const onQueryCategoryChange = (val: EQueryType): void => {
const element = document.getElementById('top');
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
if (val === EQueryType.PROM) {
setAlertDef({
...alertDef,
@@ -243,6 +259,7 @@ function FormAlertRules({
const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = {
...alertDef,
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
alertType,
source: window?.location.toString(),
ruleType:
@@ -386,7 +403,11 @@ function FormAlertRules({
}, [t, isFormValid, memoizedPreparePostData, notifications]);
const renderBasicInfo = (): JSX.Element => (
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
<BasicInfo
alertDef={alertDef}
setAlertDef={setAlertDef}
isNewRule={isNewRule}
/>
);
const renderQBChartPreview = (): JSX.Element => (
@@ -421,8 +442,6 @@ function FormAlertRules({
/>
);
const isNewRule = ruleId === 0;
const isAlertNameMissing = !formInstance.getFieldValue('alert');
const isAlertAvialableToSave =
@@ -442,11 +461,15 @@ function FormAlertRules({
}));
};
const isChannelConfigurationValid =
alertDef?.broadcastToAll ||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
return (
<>
{Element}
<PanelContainer>
<PanelContainer id="top">
<StyledLeftContainer flex="5 1 600px" md={18}>
<MainFormContainer
initialValues={initialValue}
@@ -489,7 +512,11 @@ function FormAlertRules({
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={isAlertNameMissing || isAlertAvialableToSave}
disabled={
isAlertNameMissing ||
isAlertAvialableToSave ||
!isChannelConfigurationValid
}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>
@@ -497,6 +524,7 @@ function FormAlertRules({
<ActionButton
loading={loading || false}
disabled={isAlertNameMissing || !isChannelConfigurationValid}
type="default"
onClick={onTestRuleHandler}
>

View File

@@ -76,6 +76,10 @@ export const FormContainer = styled(Card)`
display: flex;
flex-direction: column;
border-radius: 4px;
.ant-card-body {
padding: 12px;
}
`;
export const TextareaMedium = styled(TextArea)`

View File

@@ -1,6 +1,6 @@
.full-view-header-container {
.full-screen-header-container {
display: flex;
justify-content: flex-end;
justify-content: center;
align-items: center;
padding: 24px 0;

View File

@@ -1,10 +1,10 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './FullViewHeader.styles.scss';
import './FullScreenHeader.styles.scss';
import history from 'lib/history';
export default function FullViewHeader({
export default function FullScreenHeader({
overrideRoute,
}: {
overrideRoute?: string;
@@ -13,7 +13,7 @@ export default function FullViewHeader({
history.push(overrideRoute || '/');
};
return (
<div className="full-view-header-container">
<div className="full-screen-header-container">
<div className="brand-logo" onClick={handleLogoClick}>
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
@@ -23,6 +23,6 @@ export default function FullViewHeader({
);
}
FullViewHeader.defaultProps = {
FullScreenHeader.defaultProps = {
overrideRoute: '/',
};

View File

@@ -18,13 +18,14 @@ import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariab
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { getGraphVisibilityStateOnDataChange } from '../utils';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager';
// import GraphManager from './GraphManager';
@@ -36,13 +37,14 @@ function FullView({
fullViewOptions = true,
onClickHandler,
name,
originalName,
yAxisUnit,
options,
onDragSelect,
isDependedDataLoaded = false,
graphsVisibilityStates,
onToggleModelHandler,
parentChartRef,
setGraphsVisibilityStates,
parentGraphVisibilityState,
}: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
@@ -55,6 +57,20 @@ function FullView({
const { selectedDashboard, isDashboardLocked } = useDashboard();
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
() =>
getGraphVisibilityStateOnDataChange({
options,
isExpandedName: false,
name: originalName,
}),
[options, originalName],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState(
localStoredVisibilityStates,
);
const getSelectedTime = useCallback(
() =>
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
@@ -89,7 +105,7 @@ function FullView({
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
});
const chartData = getUPlotChartData(response?.data?.payload);
const chartData = getUPlotChartData(response?.data?.payload, widget.fillSpans);
const isDarkMode = useIsDarkMode();
@@ -144,9 +160,9 @@ function FullView({
useEffect(() => {
graphsVisibilityStates?.forEach((e, i) => {
fullViewChartRef?.current?.toggleGraph(i, e);
parentChartRef?.current?.toggleGraph(i, e);
});
}, [graphsVisibilityStates, parentChartRef]);
parentGraphVisibilityState(graphsVisibilityStates);
}, [graphsVisibilityStates, parentGraphVisibilityState]);
if (response.isFetching) {
return <Spinner height="100%" size="large" tip="Loading..." />;
@@ -206,7 +222,7 @@ function FullView({
{canModifyChart && chartOptions && !isDashboardLocked && (
<GraphManager
data={chartData}
name={name}
name={originalName}
options={chartOptions}
yAxisUnit={yAxisUnit}
onToggleModelHandler={onToggleModelHandler}

View File

@@ -50,13 +50,14 @@ export interface FullViewProps {
fullViewOptions?: boolean;
onClickHandler?: OnClickPluginOpts['onClick'];
name: string;
originalName: string;
options: uPlot.Options;
yAxisUnit?: string;
onDragSelect: (start: number, end: number) => void;
isDependedDataLoaded?: boolean;
graphsVisibilityStates?: boolean[];
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
setGraphsVisibilityStates: Dispatch<SetStateAction<boolean[]>>;
parentChartRef: GraphManagerProps['lineChartRef'];
parentGraphVisibilityState: Dispatch<SetStateAction<boolean[]>>;
}
export interface GraphManagerProps extends UplotProps {
@@ -64,8 +65,8 @@ export interface GraphManagerProps extends UplotProps {
yAxisUnit?: string;
onToggleModelHandler?: () => void;
options: uPlot.Options;
setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates'];
graphsVisibilityStates: FullViewProps['graphsVisibilityStates'];
graphsVisibilityStates?: boolean[];
setGraphsVisibilityStates: Dispatch<SetStateAction<boolean[]>>;
lineChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
parentChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
}

View File

@@ -17,7 +17,6 @@ import {
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
@@ -39,14 +38,15 @@ function WidgetGraphComponent({
queryResponse,
errorMessage,
name,
onClickHandler,
threshold,
headerMenuList,
isWarning,
data,
options,
graphVisibiltyState,
onClickHandler,
onDragSelect,
graphVisibility,
setGraphVisibility,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@@ -60,33 +60,28 @@ function WidgetGraphComponent({
const lineChartRef = useRef<ToggleGraphProps>();
const graphRef = useRef<HTMLDivElement>(null);
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
() =>
getGraphVisibilityStateOnDataChange({
useEffect(() => {
if (queryResponse.isSuccess) {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getGraphVisibilityStateOnDataChange({
options,
isExpandedName: true,
isExpandedName: false,
name,
}),
[options, name],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
boolean[]
>(localStoredVisibilityStates);
});
setGraphVisibility(localStoredVisibilityState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryResponse.isSuccess]);
useEffect(() => {
setGraphsVisibilityStates(localStoredVisibilityStates);
if (!lineChartRef.current) return;
localStoredVisibilityStates.forEach((state, index) => {
graphVisibiltyState.forEach((state, index) => {
lineChartRef.current?.toggleGraph(index, state);
});
setGraphsVisibilityStates(localStoredVisibilityStates);
}, [localStoredVisibilityStates]);
graphVisibility?.forEach((state, index) => {
lineChartRef.current?.toggleGraph(index, state);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
@@ -129,6 +124,7 @@ function WidgetGraphComponent({
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
setDeleteModal(false);
featureResponse.refetch();
},
onError: () => {
@@ -260,6 +256,7 @@ function WidgetGraphComponent({
destroyOnClose
onCancel={onDeleteModelHandler}
open={deleteModal}
confirmLoading={updateDashboardMutation.isLoading}
title="Delete"
height="10vh"
onOk={onDeleteHandler}
@@ -279,13 +276,14 @@ function WidgetGraphComponent({
>
<FullView
name={`${name}expanded`}
originalName={name}
widget={widget}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
parentChartRef={lineChartRef}
parentGraphVisibilityState={setGraphVisibility}
onDragSelect={onDragSelect}
setGraphsVisibilityStates={setGraphsVisibilityStates}
graphsVisibilityStates={graphsVisibilityStates}
options={options}
/>
</Modal>

View File

@@ -167,13 +167,6 @@ function GridCardGraph({
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
useEffect(() => {
setGraphVisibility([
true,
...Array(queryResponse.data?.payload?.data.result.length).fill(true),
]);
}, [queryResponse.data?.payload?.data.result.length]);
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -227,7 +220,8 @@ function GridCardGraph({
threshold={threshold}
headerMenuList={menuList}
onClickHandler={onClickHandler}
graphVisibility={graphVisibility}
graphVisibiltyState={graphVisibility}
setGraphVisibility={setGraphVisibility}
/>
)}
</div>

View File

@@ -1,7 +1,7 @@
import { ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { MutableRefObject, ReactNode } from 'react';
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
@@ -28,7 +28,8 @@ export interface WidgetGraphComponentProps extends UplotProps {
threshold?: ReactNode;
headerMenuList: MenuItemKeys[];
isWarning: boolean;
graphVisibility?: boolean[];
graphVisibiltyState: boolean[];
setGraphVisibility: Dispatch<SetStateAction<boolean[]>>;
}
export interface GridCardGraphProps {

View File

@@ -1,8 +1,10 @@
.fullscreen-grid-container {
overflow: auto;
margin-top: 1rem;
.react-grid-layout {
border: none !important;
margin-top: 0;
}
}
@@ -13,3 +15,9 @@
height: calc(100% - 30px);
}
}
.lightMode {
.fullscreen-grid-container {
background-color: rgb(250, 250, 250);
}
}

View File

@@ -55,7 +55,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [dashboardLayout, setDashboardLayout] = useState(layouts);
const [dashboardLayout, setDashboardLayout] = useState<Layout[]>([]);
const updateDashboardMutation = useUpdateDashboard();
@@ -77,6 +77,10 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
userRole,
);
useEffect(() => {
setDashboardLayout(layouts);
}, [layouts]);
const onSaveHandler = (): void => {
if (!selectedDashboard) return;
@@ -196,6 +200,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
name={currentWidget?.id || ''}
headerMenuList={widgetActions}
variables={variables}
fillSpans={currentWidget?.fillSpans}
/>
</Card>
</CardContainer>

View File

@@ -1,21 +1,14 @@
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback } from 'react';
import { Layout } from 'react-grid-layout';
import { EMPTY_WIDGET_LAYOUT } from './config';
import GraphLayoutContainer from './GridCardLayout';
function GridGraph(): JSX.Element {
const { handleToggleDashboardSlider, setLayouts } = useDashboard();
const { handleToggleDashboardSlider } = useDashboard();
const onEmptyWidgetHandler = useCallback(() => {
handleToggleDashboardSlider(true);
setLayouts((preLayout: Layout[]) => [
EMPTY_WIDGET_LAYOUT,
...(preLayout || []),
]);
}, [handleToggleDashboardSlider, setLayouts]);
}, [handleToggleDashboardSlider]);
return <GraphLayoutContainer onAddPanelHandler={onEmptyWidgetHandler} />;
}

View File

@@ -51,7 +51,7 @@ function HeaderContainer(): JSX.Element {
const isDarkMode = useIsDarkMode();
const { toggleTheme } = useThemeMode();
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
const [homeRoute, setHomeRoute] = useState(ROUTES.APPLICATION);
const [homeRoute, setHomeRoute] = useState<string>(ROUTES.APPLICATION);
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);

View File

@@ -0,0 +1,5 @@
.delete-modal {
.ant-modal-confirm-body {
align-items: center;
}
}

View File

@@ -1,3 +1,5 @@
import './DeleteButton.styles.scss';
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { Modal, Tooltip, Typography } from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@@ -64,6 +66,7 @@ function DeleteButton({
okText: 'Delete',
okButtonProps: { danger: true },
centered: true,
className: 'delete-modal',
});
}, [modal, name, deleteDashboardMutation, notifications, t, queryClient]);

View File

@@ -3,18 +3,25 @@ import { Dashboard } from 'types/api/dashboard/getAll';
export const filterDashboard = (
searchValue: string,
dashboardList: Dashboard[],
): any[] => {
// Convert the searchValue to lowercase for case-insensitive search
const searchValueLowerCase = searchValue.toLowerCase();
): Dashboard[] => {
const searchValueLowerCase = searchValue?.toLowerCase();
// Use the filter method to find matching objects
// Filter by title, description, tags
return dashboardList.filter((item: Dashboard) => {
// Convert each property value to lowercase for case-insensitive search
const itemValues = Object.values(item?.data).map((value) =>
value.toString().toLowerCase(),
);
const { title, description, tags } = item.data;
const itemValuesNew = [title, description];
if (tags && tags.length > 0) {
itemValuesNew.push(...tags);
}
// Check if any property value contains the searchValue
return itemValues.some((value) => value.includes(searchValueLowerCase));
return itemValuesNew.some((value) => {
if (value) {
return value.toLowerCase().includes(searchValueLowerCase);
}
return false;
});
});
};

View File

@@ -1,5 +1,6 @@
import { Card, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import Spinner from 'components/Spinner';
@@ -13,7 +14,6 @@ import { Heading } from 'container/LogsTable/styles';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useFontFaceObserver from 'hooks/useFontObserver';
import { useEventSource } from 'providers/EventSource';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -51,26 +51,18 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
[logs, activeLogId],
);
useFontFaceObserver(
[
{
family: 'Fira Code',
weight: '300',
},
],
options.format === 'raw',
{
timeout: 5000,
},
);
const selectedFields = convertKeysToColumnFields(options.selectColumns);
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
return (
<RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
<RawLogView
key={log.id}
data={log}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
/>
);
}
@@ -145,6 +137,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
</InfinityWrapperStyled>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}

View File

@@ -29,11 +29,11 @@ function ActionItem({
() => (
<Col>
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
<PlusCircleOutlined /> Filter for value
<PlusCircleOutlined size={12} /> Filter for value
</Button>
<br />
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
<MinusCircleOutlined /> Filter out value
<MinusCircleOutlined size={12} /> Filter out value
</Button>
</Col>
),

View File

@@ -0,0 +1,3 @@
.log-context-container {
border: 1px solid var(--bg-slate-400);
}

View File

@@ -0,0 +1,54 @@
import './ContextView.styles.scss';
import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
interface LogContextProps {
log: ILog;
contextQuery: Query | undefined;
filters: TagFilter | null;
isEdit: boolean;
}
function ContextView({
log,
filters,
contextQuery,
isEdit,
}: LogContextProps): JSX.Element {
// eslint-disable-next-line react/jsx-no-useless-fragment
if (!contextQuery) return <></>;
return (
<div className="log-context-container">
<LogsContextList
className="logs-context-list-asc"
order={ORDERBY_FILTERS.ASC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
<RawLogView
isActiveLog
isReadOnly
isTextOverflowEllipsisDisabled={false}
data={log}
linesPerRow={1}
/>
<LogsContextList
className="logs-context-list-desc"
order={ORDERBY_FILTERS.DESC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
</div>
);
}
export default ContextView;

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