Compare commits
11 Commits
v0.143.0
...
bottom-str
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef3e09c0b1 | ||
|
|
720810d424 | ||
|
|
7424885a14 | ||
|
|
2c09fedde1 | ||
|
|
5a1be60745 | ||
|
|
cad93a8063 | ||
|
|
aed096bf27 | ||
|
|
6b66ab64c8 | ||
|
|
362d3a4fdf | ||
|
|
ff80758e1c | ||
|
|
f9d425bf5c |
11
.github/CODEOWNERS
vendored
@@ -200,6 +200,15 @@ go.mod @therealpandey
|
||||
/frontend/src/container/ListAlertRules/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/AnomalyAlertEvaluationView/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/RoutingPolicies/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/AlertBreadcrumb/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/EditRules/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/AlertDetailsFilters/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/Alerts/ @SigNoz/pulse-frontend
|
||||
/frontend/src/hooks/routingPolicies/ @SigNoz/pulse-frontend
|
||||
/frontend/src/types/api/alerts/ @SigNoz/pulse-frontend
|
||||
/frontend/src/providers/Alert.tsx @SigNoz/pulse-frontend
|
||||
/frontend/src/constants/alerts.ts @SigNoz/pulse-frontend
|
||||
|
||||
## Notification Channels
|
||||
/frontend/src/pages/ChannelsEdit/ @SigNoz/pulse-frontend
|
||||
@@ -207,6 +216,8 @@ go.mod @therealpandey
|
||||
/frontend/src/container/AllAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/CreateAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/EditAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/FormAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/hooks/notificationChannels/ @SigNoz/pulse-frontend
|
||||
|
||||
## OpenAPI Schema - Generated
|
||||
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Preview } from '@storybook/react-vite';
|
||||
import type { SetupWorker } from 'msw';
|
||||
import { setupWorker } from 'msw';
|
||||
import { configure } from 'storybook/test';
|
||||
|
||||
import { settleForCapture } from '../src/storybook/visual/settleForCapture';
|
||||
import PageDocs from '../src/storybook/docs/PageDocs';
|
||||
@@ -83,6 +84,10 @@ const translationsReady = i18n.loadNamespaces(
|
||||
),
|
||||
);
|
||||
|
||||
// testing-library's 1s default for `findBy*` and `waitFor` is shorter than a
|
||||
// popup or a query takes to land on a loaded CI runner.
|
||||
configure({ asyncUtilTimeout: 10_000 });
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
|
||||
@@ -30,6 +30,8 @@ interface CapturedMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const PREPARE_TIMEOUT_MS = 180_000;
|
||||
|
||||
const messagesByPage = new WeakMap<Page, CapturedMessage[]>();
|
||||
|
||||
/**
|
||||
@@ -52,6 +54,15 @@ const config: TestRunnerConfig = {
|
||||
// msw logs every mocked request at `log`; keep it out of the failure dump
|
||||
// unless the job is re-run with debug logging (GitHub sets RUNNER_DEBUG=1).
|
||||
logLevel: process.env.RUNNER_DEBUG === '1' ? 'info' : 'warn',
|
||||
// The runner's default prepare, minus Playwright's 30s navigation timeout: on
|
||||
// a cold dev server the first load compiles the whole preview, which outlasts
|
||||
// it on a shared CI runner.
|
||||
async prepare({ page }): Promise<void> {
|
||||
await page.goto(new URL('iframe.html', process.env.TARGET_URL).toString(), {
|
||||
waitUntil: 'load',
|
||||
timeout: PREPARE_TIMEOUT_MS,
|
||||
});
|
||||
},
|
||||
async preVisit(page): Promise<void> {
|
||||
const existing = messagesByPage.get(page);
|
||||
if (existing) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 92.2 65" style="enable-background:new 0 0 92.2 65;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st0{fill:#7A8291;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
|
||||
|
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 714 B |
3
frontend/src/assets/Logos/antigravity.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 16 15" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.0777 13.984C14.945 14.6345 16.2458 14.2008 15.0533 13.0084C11.476 9.53949 12.2349 0 7.79033 0C3.34579 0 4.10461 9.53949 0.527295 13.0084C-0.773543 14.3092 0.635692 14.6345 1.50293 13.984C4.86344 11.7076 4.64663 7.69664 7.79033 7.69664C10.934 7.69664 10.7172 11.7076 14.0777 13.984Z" fill="#4285F4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 394 B |
1
frontend/src/assets/Logos/aurora.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><g fill="none" fill-rule="evenodd"><path fill="#c925d1" d="M0 0h64v64H0z"/><path fill="#fff" d="M36.213 16.011h-2.016v-2.005h2.016V12h2.017v2.006h2.016v2.005H38.23v2.006h-2.017zm9.074 8.023h-2.016v-2.006h2.016v-2.005h2.016v2.005h2.017v2.006h-2.017v2.006h-2.016v-2.006m-3.434 22.64c-1.495-3.292-4.482-6.263-7.792-7.75 3.31-1.487 6.297-4.459 7.792-7.75 1.494 3.291 4.482 6.263 7.791 7.75-3.31 1.487-6.297 4.458-7.79 7.75m12.139-8.753c-5.202 0-11.13-5.898-11.13-11.071 0-.555-.452-1.003-1.009-1.003s-1.008.448-1.008 1.003c0 5.173-5.93 11.071-11.13 11.071-.558 0-1.009.448-1.009 1.003s.45 1.003 1.008 1.003c5.202 0 11.13 5.898 11.13 11.07 0 .554.452 1.003 1.01 1.003.556 0 1.007-.45 1.007-1.003 0-5.172 5.93-11.07 11.13-11.07.558 0 1.009-.45 1.009-1.003 0-.555-.45-1.003-1.008-1.003m-31.39-19.904c6.85 0 10.587 1.988 10.587 3.008s-3.737 3.009-10.586 3.009-10.587-1.988-10.587-3.009 3.737-3.008 10.587-3.008m-.35 15.15c-5.012 0-8.62-1.063-10.236-2.19v-7.113c2.367 1.433 6.487 2.176 10.587 2.176s8.22-.743 10.586-2.176v6.484c-1.016 1.406-5.247 2.819-10.936 2.819M33.19 45.975c0 1.435-4.126 3.52-10.59 3.52-6.46 0-10.583-2.085-10.583-3.52v-4.407c2.258 1.354 6.014 2.192 10.276 2.192 2.942 0 5.786-.413 8.01-1.166l-.651-1.898c-2.019.682-4.633 1.058-7.359 1.058-5.39 0-9.252-1.402-10.276-2.76v-5.707c2.411 1.176 6.113 1.885 10.237 1.885 3.92 0 8.34-.725 10.936-2.269v2.162h2.016v-14.04c0-3.292-6.34-5.014-12.602-5.014S10 17.733 10 21.025v24.95c0 3.59 6.49 5.526 12.598 5.526 6.112 0 12.607-1.937 12.607-5.526v-2.887h-2.016z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,6 +1,6 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 50.6 50.6" style="enable-background:new 0 0 50.6 50.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st0{fill:#7A8291;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
3
frontend/src/assets/Logos/cline.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m23.365 13.556-1.442-2.895V8.994c0-2.764-2.218-5.002-4.954-5.002h-2.464c.178-.367.276-.779.276-1.213A2.77 2.77 0 0 0 12.018 0a2.77 2.77 0 0 0-2.763 2.779c0 .434.098.846.276 1.213H7.067c-2.736 0-4.954 2.238-4.954 5.002v1.667L.64 13.549c-.149.29-.149.636 0 .927l1.472 2.855v1.667C2.113 21.762 4.33 24 7.067 24h9.902c2.736 0 4.954-2.238 4.954-5.002V17.33l1.44-2.865c.143-.286.143-.622.002-.91m-12.854 2.36a2.27 2.27 0 0 1-2.261 2.273 2.27 2.27 0 0 1-2.261-2.273v-4.042A2.27 2.27 0 0 1 8.249 9.6a2.267 2.267 0 0 1 2.262 2.274zm7.285 0a2.27 2.27 0 0 1-2.26 2.273 2.27 2.27 0 0 1-2.262-2.273v-4.042A2.267 2.267 0 0 1 15.535 9.6a2.267 2.267 0 0 1 2.261 2.274z" fill="#7A8291" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 761 B |
@@ -1,6 +1,6 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 43 43" style="enable-background:new 0 0 43 43;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#0074A2;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
3
frontend/src/assets/Logos/cursor.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23" fill="#7A8291" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 453 B |
@@ -3,7 +3,7 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 800.5 907.77" style="enable-background:new 0 0 800.5 907.77;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st0{fill:#8B5CC7;}
|
||||
</style>
|
||||
<path class="st0" d="M303.36,238.61c31.36-21.37,71.76-12.97,65-6.53c-12.89,12.28,4.26,8.65,6.11,31.31
|
||||
c1.36,16.69-4.09,25.88-8.78,31.11c-9.79,1.28-21.69,3.67-36.02,8.33c-8.48,2.76-15.85,5.82-22.31,8.9
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
10
frontend/src/assets/Logos/daytona.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg role="img" viewBox="0 0 275 287" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.5584 193.736H114.275V227.925H14.5584V193.736Z" fill="#7A8291" />
|
||||
<path d="M148.464 74.076H262.426V108.265H148.464V74.076Z" fill="#7A8291" />
|
||||
<path d="M88.6338 84.6127L173.246 0L197.422 24.175L112.809 108.788L88.6338 84.6127Z" fill="#7A8291" />
|
||||
<path d="M89.157 170.084L24.175 105.102L0 129.277L64.9819 194.259L89.157 170.084Z" fill="#7A8291" />
|
||||
<path d="M174.629 217.911L106.133 286.407L81.9577 262.232L150.454 193.736L174.629 217.911Z" fill="#7A8291" />
|
||||
<path d="M174.106 132.44L250.66 208.994L274.835 184.819L198.281 108.265L174.106 132.44Z" fill="#7A8291" />
|
||||
<path d="M88.6338 48.434V131.057H54.4451L54.4451 48.434H88.6338Z" fill="#7A8291" />
|
||||
<path d="M208.294 168.094V270.66H174.106V168.094H208.294Z" fill="#7A8291" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 825 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Deno</title><path d="M1.105 18.02A11.9 11.9 0 0 1 0 12.985q0-.698.078-1.376a12 12 0 0 1 .231-1.34A12 12 0 0 1 4.025 4.02a12 12 0 0 1 5.46-2.771 12 12 0 0 1 3.428-.23c1.452.112 2.825.477 4.077 1.05a12 12 0 0 1 2.78 1.774 12.02 12.02 0 0 1 4.053 7.078A12 12 0 0 1 24 12.985q0 .454-.036.914a12 12 0 0 1-.728 3.305 12 12 0 0 1-2.38 3.875c-1.33 1.357-3.02 1.962-4.43 1.936a4.4 4.4 0 0 1-2.724-1.024c-.99-.853-1.391-1.83-1.53-2.919a5 5 0 0 1 .128-1.518c.105-.38.37-1.116.76-1.437-.455-.197-1.04-.624-1.226-.829-.045-.05-.04-.13 0-.183a.155.155 0 0 1 .177-.053c.392.134.869.267 1.372.35.66.111 1.484.25 2.317.292 2.03.1 4.153-.813 4.812-2.627s.403-3.609-1.96-4.685-3.454-2.356-5.363-3.128c-1.247-.505-2.636-.205-4.06.582-3.838 2.121-7.277 8.822-5.69 15.032a.191.191 0 0 1-.315.19 12 12 0 0 1-1.25-1.634 12 12 0 0 1-.769-1.404M11.57 6.087c.649-.051 1.214.501 1.31 1.236.13.979-.228 1.99-1.41 2.013-1.01.02-1.315-.997-1.248-1.614.066-.616.574-1.575 1.35-1.635"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#7A8291" viewBox="0 0 24 24"><title>Deno</title><path d="M1.105 18.02A11.9 11.9 0 0 1 0 12.985q0-.698.078-1.376a12 12 0 0 1 .231-1.34A12 12 0 0 1 4.025 4.02a12 12 0 0 1 5.46-2.771 12 12 0 0 1 3.428-.23c1.452.112 2.825.477 4.077 1.05a12 12 0 0 1 2.78 1.774 12.02 12.02 0 0 1 4.053 7.078A12 12 0 0 1 24 12.985q0 .454-.036.914a12 12 0 0 1-.728 3.305 12 12 0 0 1-2.38 3.875c-1.33 1.357-3.02 1.962-4.43 1.936a4.4 4.4 0 0 1-2.724-1.024c-.99-.853-1.391-1.83-1.53-2.919a5 5 0 0 1 .128-1.518c.105-.38.37-1.116.76-1.437-.455-.197-1.04-.624-1.226-.829-.045-.05-.04-.13 0-.183a.155.155 0 0 1 .177-.053c.392.134.869.267 1.372.35.66.111 1.484.25 2.317.292 2.03.1 4.153-.813 4.812-2.627s.403-3.609-1.96-4.685-3.454-2.356-5.363-3.128c-1.247-.505-2.636-.205-4.06.582-3.838 2.121-7.277 8.822-5.69 15.032a.191.191 0 0 1-.315.19 12 12 0 0 1-1.25-1.634 12 12 0 0 1-.769-1.404M11.57 6.087c.649-.051 1.214.501 1.31 1.236.13.979-.228 1.99-1.41 2.013-1.01.02-1.315-.997-1.248-1.614.066-.616.574-1.575 1.35-1.635"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="#FFF" viewBox="0 0 50 50"><path d="M7 2v46h36V14.594l-.281-.313-12-12L30.406 2Zm2 2h20v12h12v30H9Zm22 1.438L39.563 14H31ZM15 22v2h20v-2Zm0 6v2h16v-2Zm0 6v2h20v-2Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="#7A8291" viewBox="0 0 50 50"><path d="M7 2v46h36V14.594l-.281-.313-12-12L30.406 2Zm2 2h20v12h12v30H9Zm22 1.438L39.563 14H31ZM15 22v2h20v-2Zm0 6v2h16v-2Zm0 6v2h20v-2Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 242 B After Width: | Height: | Size: 245 B |
3
frontend/src/assets/Logos/e2b.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 6h10v12H9a4 4 0 0 1-4-4v-4a4 4 0 0 1 4-4zm1.2 4.6h8.8v.9h-8.8zm0 3.6h8.8v.9h-8.8z" fill="#7A8291" fill-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 213 B |
5
frontend/src/assets/Logos/emqx.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 5.5h13a1.5 1.5 0 0 1 0 3h-13a1.5 1.5 0 0 1 0-3z" fill="#00B173" />
|
||||
<path d="M4 10.5h10a1.5 1.5 0 0 1 0 3H4a1.5 1.5 0 0 1 0-3z" fill="#00B173" />
|
||||
<path d="M6.5 15.5h13a1.5 1.5 0 0 1 0 3h-13a1.5 1.5 0 0 1 0-3z" fill="#00B173" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 115.28 122.88"><path d="M25.38 57h64.88V37.34H69.59c-2.17 0-5.19-1.17-6.62-2.6s-2.3-4.01-2.3-6.17V7.64H8.15c-.18 0-.32.09-.41.18-.15.1-.19.23-.19.42v106.45c0 .14.09.32.18.41.09.14.28.18.41.18h81.51c.18 0 .17-.09.27-.18.14-.09.33-.28.33-.41v-11.16H25.38c-4.14 0-7.56-3.4-7.56-7.56V64.55c0-4.15 3.4-7.55 7.56-7.55m4.11 11.38h7.43v18.15h11.63v5.92H29.49zm20.4 12.05c0-3.93 1.09-6.99 3.28-9.17 2.19-2.19 5.24-3.28 9.15-3.28 4.01 0 7.09 1.08 9.26 3.22 2.17 2.15 3.25 5.16 3.25 9.04 0 2.81-.47 5.11-1.42 6.91q-1.425 2.7-4.11 4.2t-6.69 1.5c-2.71 0-4.96-.43-6.74-1.29-1.78-.87-3.22-2.23-4.32-4.11-1.11-1.87-1.66-4.21-1.66-7.02m7.42.01c0 2.43.45 4.17 1.36 5.23s2.14 1.59 3.7 1.59c1.6 0 2.84-.52 3.71-1.56.88-1.04 1.32-2.9 1.32-5.6 0-2.26-.46-3.92-1.37-4.96-.92-1.05-2.16-1.57-3.73-1.57-1.5 0-2.71.53-3.62 1.59-.91 1.08-1.37 2.83-1.37 5.28m33.11 3.3v-5.01h11.49v10.23c-2.2 1.5-4.15 2.53-5.83 3.07-1.69.54-3.7.81-6.02.81-2.86 0-5.19-.49-6.99-1.46s-3.19-2.42-4.18-4.35c-.99-1.92-1.48-4.13-1.48-6.63 0-2.63.54-4.91 1.62-6.85s2.67-3.41 4.76-4.42c1.63-.78 3.83-1.17 6.58-1.17 2.66 0 4.64.24 5.96.72s2.41 1.23 3.28 2.24 1.52 2.3 1.96 3.85l-7.16 1.29c-.3-.91-.8-1.61-1.5-2.09-.71-.49-1.6-.73-2.7-.73-1.62 0-2.92.57-3.89 1.7s-1.45 2.92-1.45 5.37c0 2.6.49 4.46 1.47 5.57.97 1.11 2.34 1.68 4.09 1.68q1.245 0 2.37-.36c.75-.24 1.61-.65 2.59-1.22v-2.25h-4.97zM97.79 57h9.93c4.16 0 7.56 3.41 7.56 7.56v31.42c0 4.15-3.41 7.56-7.56 7.56h-9.93v13.55c0 1.61-.65 3.04-1.7 4.1a5.74 5.74 0 0 1-4.1 1.7H5.81a5.74 5.74 0 0 1-4.1-1.7 5.74 5.74 0 0 1-1.7-4.1V5.85c0-1.61.65-3.04 1.7-4.1a5.8 5.8 0 0 1 4.1-1.7h58.72c.13-.05.27-.05.41-.05.64 0 1.29.28 1.75.69h.09c.09.05.14.09.23.18L97 31.23c.51.51.88 1.2.88 1.98 0 .23-.05.41-.09.65zM67.52 27.97V8.94l21.43 21.7H70.19c-.74 0-1.38-.32-1.89-.78-.46-.46-.78-1.15-.78-1.89" style="fill:#D1D5DB;fill-rule:evenodd;clip-rule:evenodd"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 115.28 122.88"><path d="M25.38 57h64.88V37.34H69.59c-2.17 0-5.19-1.17-6.62-2.6s-2.3-4.01-2.3-6.17V7.64H8.15c-.18 0-.32.09-.41.18-.15.1-.19.23-.19.42v106.45c0 .14.09.32.18.41.09.14.28.18.41.18h81.51c.18 0 .17-.09.27-.18.14-.09.33-.28.33-.41v-11.16H25.38c-4.14 0-7.56-3.4-7.56-7.56V64.55c0-4.15 3.4-7.55 7.56-7.55m4.11 11.38h7.43v18.15h11.63v5.92H29.49zm20.4 12.05c0-3.93 1.09-6.99 3.28-9.17 2.19-2.19 5.24-3.28 9.15-3.28 4.01 0 7.09 1.08 9.26 3.22 2.17 2.15 3.25 5.16 3.25 9.04 0 2.81-.47 5.11-1.42 6.91q-1.425 2.7-4.11 4.2t-6.69 1.5c-2.71 0-4.96-.43-6.74-1.29-1.78-.87-3.22-2.23-4.32-4.11-1.11-1.87-1.66-4.21-1.66-7.02m7.42.01c0 2.43.45 4.17 1.36 5.23s2.14 1.59 3.7 1.59c1.6 0 2.84-.52 3.71-1.56.88-1.04 1.32-2.9 1.32-5.6 0-2.26-.46-3.92-1.37-4.96-.92-1.05-2.16-1.57-3.73-1.57-1.5 0-2.71.53-3.62 1.59-.91 1.08-1.37 2.83-1.37 5.28m33.11 3.3v-5.01h11.49v10.23c-2.2 1.5-4.15 2.53-5.83 3.07-1.69.54-3.7.81-6.02.81-2.86 0-5.19-.49-6.99-1.46s-3.19-2.42-4.18-4.35c-.99-1.92-1.48-4.13-1.48-6.63 0-2.63.54-4.91 1.62-6.85s2.67-3.41 4.76-4.42c1.63-.78 3.83-1.17 6.58-1.17 2.66 0 4.64.24 5.96.72s2.41 1.23 3.28 2.24 1.52 2.3 1.96 3.85l-7.16 1.29c-.3-.91-.8-1.61-1.5-2.09-.71-.49-1.6-.73-2.7-.73-1.62 0-2.92.57-3.89 1.7s-1.45 2.92-1.45 5.37c0 2.6.49 4.46 1.47 5.57.97 1.11 2.34 1.68 4.09 1.68q1.245 0 2.37-.36c.75-.24 1.61-.65 2.59-1.22v-2.25h-4.97zM97.79 57h9.93c4.16 0 7.56 3.41 7.56 7.56v31.42c0 4.15-3.41 7.56-7.56 7.56h-9.93v13.55c0 1.61-.65 3.04-1.7 4.1a5.74 5.74 0 0 1-4.1 1.7H5.81a5.74 5.74 0 0 1-4.1-1.7 5.74 5.74 0 0 1-1.7-4.1V5.85c0-1.61.65-3.04 1.7-4.1a5.8 5.8 0 0 1 4.1-1.7h58.72c.13-.05.27-.05.41-.05.64 0 1.29.28 1.75.69h.09c.09.05.14.09.23.18L97 31.23c.51.51.88 1.2.88 1.98 0 .23-.05.41-.09.65zM67.52 27.97V8.94l21.43 21.7H70.19c-.74 0-1.38-.32-1.89-.78-.46-.46-.78-1.15-.78-1.89" style="fill:#7A8291;fill-rule:evenodd;clip-rule:evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Haystack</title><path d="M2.008 0C.9 0 0 .9 0 2.008v19.984C0 23.1.9 24 2.008 24h19.984C23.1 24 24 23.1 24 21.992V2.008C24 .9 23.1 0 21.992 0Zm9.963 3.84c3.43 0 6.21 2.763 6.21 6.17v6.488a.27.27 0 0 1-.27.268 2.423 2.423 0 0 1-2.43-2.414V10.01c0-1.927-1.572-3.488-3.51-3.488S8.547 8.085 8.547 10.01v1.608a.263.263 0 0 0 .259.268h1.54a.27.27 0 0 0 .275-.263V9.945c0-.74.604-1.341 1.35-1.341s1.35.6 1.35 1.341V20.03a.275.275 0 0 1-.28.268 2.41 2.41 0 0 1-2.42-2.404v-3.23a.275.275 0 0 0-.276-.269H8.811a.264.264 0 0 0-.264.263v1.08c0 1.333-1.175 2.414-2.517 2.414a.27.27 0 0 1-.27-.268v-7.872c0-3.408 2.78-6.171 6.21-6.171"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#0E9A89" viewBox="0 0 24 24"><title>Haystack</title><path d="M2.008 0C.9 0 0 .9 0 2.008v19.984C0 23.1.9 24 2.008 24h19.984C23.1 24 24 23.1 24 21.992V2.008C24 .9 23.1 0 21.992 0Zm9.963 3.84c3.43 0 6.21 2.763 6.21 6.17v6.488a.27.27 0 0 1-.27.268 2.423 2.423 0 0 1-2.43-2.414V10.01c0-1.927-1.572-3.488-3.51-3.488S8.547 8.085 8.547 10.01v1.608a.263.263 0 0 0 .259.268h1.54a.27.27 0 0 0 .275-.263V9.945c0-.74.604-1.341 1.35-1.341s1.35.6 1.35 1.341V20.03a.275.275 0 0 1-.28.268 2.41 2.41 0 0 1-2.42-2.404v-3.23a.275.275 0 0 0-.276-.269H8.811a.264.264 0 0 0-.264.263v1.08c0 1.333-1.175 2.414-2.517 2.414a.27.27 0 0 1-.27-.268v-7.872c0-3.408 2.78-6.171 6.21-6.171"/></svg>
|
||||
|
Before Width: | Height: | Size: 707 B After Width: | Height: | Size: 710 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="154" height="250" preserveAspectRatio="xMidYMid" viewBox="0 0 256 416"><path d="M201.816 230.216c-16.186 0-30.697 7.171-40.634 18.461l-25.463-18.026c2.703-7.442 4.255-15.433 4.255-23.797 0-8.219-1.498-16.076-4.112-23.408l25.406-17.835c9.936 11.233 24.409 18.365 40.548 18.365 29.875 0 54.184-24.305 54.184-54.184s-24.309-54.184-54.184-54.184-54.184 24.305-54.184 54.184c0 5.348.808 10.505 2.258 15.389l-25.423 17.844c-10.62-13.175-25.911-22.374-43.333-25.182v-30.64c24.544-5.155 43.037-26.962 43.037-53.019C124.171 24.305 99.862 0 69.987 0S15.803 24.305 15.803 54.184c0 25.708 18.014 47.246 42.067 52.769v31.038C25.044 143.753 0 172.401 0 206.854c0 34.621 25.292 63.374 58.355 68.94v32.774c-24.299 5.341-42.552 27.011-42.552 52.894 0 29.879 24.309 54.184 54.184 54.184s54.184-24.305 54.184-54.184c0-25.883-18.253-47.553-42.552-52.894v-32.775a69.97 69.97 0 0 0 42.6-24.776l25.633 18.143c-1.423 4.84-2.22 9.946-2.22 15.24 0 29.879 24.309 54.184 54.184 54.184S256 314.279 256 284.4s-24.309-54.184-54.184-54.184m0-126.695c14.487 0 26.27 11.788 26.27 26.271s-11.783 26.27-26.27 26.27-26.27-11.787-26.27-26.27 11.783-26.271 26.27-26.271m-158.1-49.337c0-14.483 11.784-26.27 26.271-26.27s26.27 11.787 26.27 26.27-11.783 26.27-26.27 26.27-26.271-11.787-26.271-26.27m52.541 307.278c0 14.483-11.783 26.27-26.27 26.27s-26.271-11.787-26.271-26.27 11.784-26.27 26.271-26.27 26.27 11.787 26.27 26.27m-26.272-117.97c-20.205 0-36.642-16.434-36.642-36.638 0-20.205 16.437-36.642 36.642-36.642 20.204 0 36.641 16.437 36.641 36.642 0 20.204-16.437 36.638-36.641 36.638m131.831 67.179c-14.487 0-26.27-11.788-26.27-26.271s11.783-26.27 26.27-26.27 26.27 11.787 26.27 26.27-11.783 26.271-26.27 26.271" style="fill:#fff"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="154" height="250" preserveAspectRatio="xMidYMid" viewBox="0 0 256 416"><path d="M201.816 230.216c-16.186 0-30.697 7.171-40.634 18.461l-25.463-18.026c2.703-7.442 4.255-15.433 4.255-23.797 0-8.219-1.498-16.076-4.112-23.408l25.406-17.835c9.936 11.233 24.409 18.365 40.548 18.365 29.875 0 54.184-24.305 54.184-54.184s-24.309-54.184-54.184-54.184-54.184 24.305-54.184 54.184c0 5.348.808 10.505 2.258 15.389l-25.423 17.844c-10.62-13.175-25.911-22.374-43.333-25.182v-30.64c24.544-5.155 43.037-26.962 43.037-53.019C124.171 24.305 99.862 0 69.987 0S15.803 24.305 15.803 54.184c0 25.708 18.014 47.246 42.067 52.769v31.038C25.044 143.753 0 172.401 0 206.854c0 34.621 25.292 63.374 58.355 68.94v32.774c-24.299 5.341-42.552 27.011-42.552 52.894 0 29.879 24.309 54.184 54.184 54.184s54.184-24.305 54.184-54.184c0-25.883-18.253-47.553-42.552-52.894v-32.775a69.97 69.97 0 0 0 42.6-24.776l25.633 18.143c-1.423 4.84-2.22 9.946-2.22 15.24 0 29.879 24.309 54.184 54.184 54.184S256 314.279 256 284.4s-24.309-54.184-54.184-54.184m0-126.695c14.487 0 26.27 11.788 26.27 26.271s-11.783 26.27-26.27 26.27-26.27-11.787-26.27-26.27 11.783-26.271 26.27-26.271m-158.1-49.337c0-14.483 11.784-26.27 26.271-26.27s26.27 11.787 26.27 26.27-11.783 26.27-26.27 26.27-26.271-11.787-26.271-26.27m52.541 307.278c0 14.483-11.783 26.27-26.27 26.27s-26.271-11.787-26.271-26.27 11.784-26.27 26.271-26.27 26.27 11.787 26.27 26.27m-26.272-117.97c-20.205 0-36.642-16.434-36.642-36.638 0-20.205 16.437-36.642 36.642-36.642 20.204 0 36.641 16.437 36.641 36.642 0 20.204-16.437 36.638-36.641 36.638m131.831 67.179c-14.487 0-26.27-11.788-26.27-26.271s11.783-26.27 26.27-26.27 26.27 11.787 26.27 26.27-11.783 26.271-26.27 26.271" style="fill:#7A8291"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
5
frontend/src/assets/Logos/karpenter.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg role="img" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(2.1186441,37.661017)">
|
||||
<path d="m 237.70492,0 c 76.95771,0 139.34426,62.386551 139.34426,139.34426 v 0 l -1.8e-4,197.00774 65.57395,-1.5e-4 c 31.68847,0 57.37705,25.68858 57.37705,57.37705 v 0 V 443 h -7.54607 c -18.22527,0 -33.09496,-14.59276 -33.43754,-32.81482 v 0 -8.22465 l -0.003,-0.40664 c -0.21715,-13.39321 -11.14191,-24.18352 -24.58687,-24.18352 v 0 H 106.55738 C 47.707363,377.37037 0,329.66301 0,270.81299 v 0 -131.46873 C 0,62.386551 62.386551,0 139.34426,0 v 0 z m -8.19672,41.018518 h -81.96722 l -1.76212,0.01428 C 87.741593,41.97378 40.983607,89.314381 40.983607,147.5759 v 0 123.20218 l 0.0088,1.08438 c 0.579068,35.71525 29.711746,64.48939 65.564993,64.48939 v 0 h 163.9344 c 36.2154,0 65.57377,-29.35838 65.57377,-65.57377 v 0 -123.20218 l -0.0143,-1.76213 C 335.11031,87.776505 287.76971,41.018518 229.5082,41.018518 Z m -81.63935,65.629632 c 9.34426,0 16.72131,7.62944 16.72131,17.22778 v 0 49.96055 l 56.80328,-58.57444 c 7.37705,-6.89111 17.45902,-7.38334 24.59017,-0.73834 v 0 0.24611 c 6.88524,6.39889 6.39344,16.98167 -1.22951,23.87278 v 0 l -42.04918,43.31556 48.93442,69.89555 c 5.90164,8.61389 4.18033,19.44278 -3.68852,24.61111 -8.60656,4.92223 -18.44262,2.21501 -24.34426,-6.39888 v 0 l -44.0164,-64.48112 -15,15.75112 v 40.36222 c 0,9.84444 -7.37705,17.22778 -16.72131,17.22778 -9.34426,0 -16.72131,-7.38334 -16.72131,-17.22778 v 0 -137.82222 c 0,-9.59834 7.37705,-17.22778 16.72131,-17.22778 z" fill="#5C62B0" fill-rule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LangChain</title><path d="M8.373 14.502c.013-.06.024-.118.038-.17l.061.145c.115.28.229.557.506.714-.012.254-.334.357-.552.326-.048-.114-.115-.228-.255-.164-.143.056-.3-.01-.266-.185.333-.012.407-.371.468-.666zM18.385 9.245c-.318 0-.616.122-.839.342l-.902.887c-.243.24-.368.572-.343.913l.006.056c.032.262.149.498.337.682.13.128.273.21.447.266a.866.866 0 01-.247.777l-.056.055a2.022 2.022 0 01-1.355-1.555l-.01-.057-.046.037c-.03.024-.06.05-.088.078l-.902.887a1.156 1.156 0 000 1.65c.231.228.535.342.84.342.304 0 .607-.114.838-.341l.902-.888a1.156 1.156 0 00-.436-1.921.953.953 0 01.276-.842 2.062 2.062 0 011.371 1.57l.01.057.047-.037c.03-.024.06-.05.088-.078l.902-.888a1.155 1.155 0 000-1.65 1.188 1.188 0 00-.84-.342z" fill="#1C3C3C"></path><path clip-rule="evenodd" d="M17.901 6H6.1C2.736 6 0 8.692 0 12s2.736 6 6.099 6H17.9C21.264 18 24 15.308 24 12s-2.736-6-6.099-6zm-5.821 9.407c-.195.04-.414.047-.562-.106-.045.1-.136.077-.221.056a.797.797 0 00-.061-.014c-.01.025-.017.048-.026.073-.329.021-.575-.309-.732-.558a4.991 4.991 0 00-.473-.21c-.172-.07-.345-.14-.509-.23a2.218 2.218 0 00-.004.173c-.002.244-.004.503-.227.651-.007.295.236.292.476.29.207-.003.41-.005.447.184a.485.485 0 01-.05.003c-.046 0-.092 0-.127.034-.117.111-.242.063-.372.013-.12-.046-.243-.094-.367-.02a2.318 2.318 0 00-.262.154.97.97 0 01-.548.194c-.024-.036-.014-.059.006-.08a.562.562 0 00.043-.056c.019-.028.035-.057.051-.084.054-.095.103-.18.242-.22-.185-.029-.344.055-.5.137l-.004.002a4.21 4.21 0 01-.065.034c-.097.04-.154.009-.212-.023-.082-.045-.168-.092-.376.04-.04-.032-.02-.061.002-.086.091-.109.21-.125.345-.119-.351-.193-.604-.056-.81.055-.182.098-.327.176-.471-.012-.065.017-.102.063-.138.108-.015.02-.03.038-.047.055-.035-.039-.027-.083-.018-.128l.005-.026a.242.242 0 00.003-.03l-.027-.01c-.053-.022-.105-.044-.09-.124-.117-.04-.2.03-.286.094-.054-.041-.01-.095.032-.145a.279.279 0 00.045-.065c.038-.065.103-.067.166-.069.054-.001.108-.003.145-.042.133-.075.297-.036.462.003.121.028.242.057.354.042.203.025.454-.18.352-.385-.186-.233-.184-.528-.183-.813v-.143c-.016-.108-.172-.233-.328-.358-.12-.095-.24-.191-.298-.28-.16-.177-.285-.382-.409-.585l-.015-.024c-.212-.404-.297-.86-.382-1.315-.103-.546-.205-1.09-.526-1.54-.266.144-.612.075-.841-.118-.12.107-.13.247-.138.396l-.001.014c-.297-.292-.26-.844-.023-1.17.097-.128.213-.233.342-.326.03-.021.04-.042.039-.074.235-1.04 1.836-.839 2.342-.103.167.206.281.442.395.678.137.283.273.566.5.795.22.237.452.463.684.689.359.35.718.699 1.032 1.089.49.587.839 1.276 1.144 1.97.05.092.08.193.11.293.044.15.089.299.2.417.026.035.084.088.149.148.156.143.357.328.289.409.009.019.027.04.05.06.032.028.074.058.116.088.122.087.25.178.16.25zm7.778-3.545l-.902.887c-.24.237-.537.413-.859.51l-.017.005-.006.015A2.021 2.021 0 0117.6 14l-.902.888c-.393.387-.916.6-1.474.6-.557 0-1.08-.213-1.474-.6a2.03 2.03 0 010-2.9l.902-.888c.242-.238.531-.409.859-.508l.016-.004.006-.016c.105-.272.265-.516.475-.724l.902-.887c.393-.387.917-.6 1.474-.6.558 0 1.08.213 1.474.6.394.387.61.902.61 1.45 0 .549-.216 1.064-.61 1.45v.001z" fill="#1C3C3C" fill-rule="evenodd"></path></svg>
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LangChain</title><path d="M8.373 14.502c.013-.06.024-.118.038-.17l.061.145c.115.28.229.557.506.714-.012.254-.334.357-.552.326-.048-.114-.115-.228-.255-.164-.143.056-.3-.01-.266-.185.333-.012.407-.371.468-.666zM18.385 9.245c-.318 0-.616.122-.839.342l-.902.887c-.243.24-.368.572-.343.913l.006.056c.032.262.149.498.337.682.13.128.273.21.447.266a.866.866 0 01-.247.777l-.056.055a2.022 2.022 0 01-1.355-1.555l-.01-.057-.046.037c-.03.024-.06.05-.088.078l-.902.887a1.156 1.156 0 000 1.65c.231.228.535.342.84.342.304 0 .607-.114.838-.341l.902-.888a1.156 1.156 0 00-.436-1.921.953.953 0 01.276-.842 2.062 2.062 0 011.371 1.57l.01.057.047-.037c.03-.024.06-.05.088-.078l.902-.888a1.155 1.155 0 000-1.65 1.188 1.188 0 00-.84-.342z" fill="#3B8686"></path><path clip-rule="evenodd" d="M17.901 6H6.1C2.736 6 0 8.692 0 12s2.736 6 6.099 6H17.9C21.264 18 24 15.308 24 12s-2.736-6-6.099-6zm-5.821 9.407c-.195.04-.414.047-.562-.106-.045.1-.136.077-.221.056a.797.797 0 00-.061-.014c-.01.025-.017.048-.026.073-.329.021-.575-.309-.732-.558a4.991 4.991 0 00-.473-.21c-.172-.07-.345-.14-.509-.23a2.218 2.218 0 00-.004.173c-.002.244-.004.503-.227.651-.007.295.236.292.476.29.207-.003.41-.005.447.184a.485.485 0 01-.05.003c-.046 0-.092 0-.127.034-.117.111-.242.063-.372.013-.12-.046-.243-.094-.367-.02a2.318 2.318 0 00-.262.154.97.97 0 01-.548.194c-.024-.036-.014-.059.006-.08a.562.562 0 00.043-.056c.019-.028.035-.057.051-.084.054-.095.103-.18.242-.22-.185-.029-.344.055-.5.137l-.004.002a4.21 4.21 0 01-.065.034c-.097.04-.154.009-.212-.023-.082-.045-.168-.092-.376.04-.04-.032-.02-.061.002-.086.091-.109.21-.125.345-.119-.351-.193-.604-.056-.81.055-.182.098-.327.176-.471-.012-.065.017-.102.063-.138.108-.015.02-.03.038-.047.055-.035-.039-.027-.083-.018-.128l.005-.026a.242.242 0 00.003-.03l-.027-.01c-.053-.022-.105-.044-.09-.124-.117-.04-.2.03-.286.094-.054-.041-.01-.095.032-.145a.279.279 0 00.045-.065c.038-.065.103-.067.166-.069.054-.001.108-.003.145-.042.133-.075.297-.036.462.003.121.028.242.057.354.042.203.025.454-.18.352-.385-.186-.233-.184-.528-.183-.813v-.143c-.016-.108-.172-.233-.328-.358-.12-.095-.24-.191-.298-.28-.16-.177-.285-.382-.409-.585l-.015-.024c-.212-.404-.297-.86-.382-1.315-.103-.546-.205-1.09-.526-1.54-.266.144-.612.075-.841-.118-.12.107-.13.247-.138.396l-.001.014c-.297-.292-.26-.844-.023-1.17.097-.128.213-.233.342-.326.03-.021.04-.042.039-.074.235-1.04 1.836-.839 2.342-.103.167.206.281.442.395.678.137.283.273.566.5.795.22.237.452.463.684.689.359.35.718.699 1.032 1.089.49.587.839 1.276 1.144 1.97.05.092.08.193.11.293.044.15.089.299.2.417.026.035.084.088.149.148.156.143.357.328.289.409.009.019.027.04.05.06.032.028.074.058.116.088.122.087.25.178.16.25zm7.778-3.545l-.902.887c-.24.237-.537.413-.859.51l-.017.005-.006.015A2.021 2.021 0 0117.6 14l-.902.888c-.393.387-.916.6-1.474.6-.557 0-1.08-.213-1.474-.6a2.03 2.03 0 010-2.9l.902-.888c.242-.238.531-.409.859-.508l.016-.004.006-.016c.105-.272.265-.516.475-.724l.902-.887c.393-.387.917-.6 1.474-.6.558 0 1.08.213 1.474.6.394.387.61.902.61 1.45 0 .549-.216 1.064-.61 1.45v.001z" fill="#3B8686" fill-rule="evenodd"></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
3
frontend/src/assets/Logos/meta.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z" fill="#0467DF" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
frontend/src/assets/Logos/modal.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.89 5.57 0 14.002l2.521 4.4h5.05l4.396-7.718 4.512 7.709 4.996.037L24 14.057l-4.857-8.452-5.073-.015-2.076 3.598L9.94 5.57Zm.837.729h3.787l1.845 3.252H7.572Zm9.189.021 3.803.012 4.228 7.355-3.736-.027zm-9.82.346L6.94 9.914l-4.209 7.389-1.892-3.3Zm9.187.014 4.297 7.343-1.892 3.282-4.3-7.344zm-6.713 3.6h3.79l-4.212 7.394H3.361Zm11.64 4.109 3.74.027-1.893 3.281-3.74-.027z" fill="#3E9C2A" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 482 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Ollama</title><path d="M16.361 10.26a.9.9 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.75.75 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.1 1.1 0 0 0-.466 0m-9.203.005c-.305.096-.533.32-.65.639a1.2 1.2 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.9.9 0 0 0-.565-.472 1 1 0 0 0-.474.007m4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.4.4 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.4.4 0 0 0-.193.032m.39-2.195c-.373.036-.475.05-.654.086a4.5 4.5 0 0 0-.951.328c-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a7 7 0 0 0-.855-.031zm.645.937a3.3 3.3 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3 3 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042q-.856-.16-1.35-.68c-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002m-5.503-11a1.65 1.65 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208l-.187.152a5.3 5.3 0 0 0-.949 1.02 5.5 5.5 0 0 0-.94 2.339 6.6 6.6 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.4 7.4 0 0 0-.548 1.873 5 5 0 0 0-.071.991c0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.9.9 0 0 0-.194-.25 1.7 1.7 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.8.8 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.14 3.14 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04s.398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2 2 0 0 1-.224.258.9.9 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a1 1 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516a9 9 0 0 0 0-1.258c-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141a1.4 1.4 0 0 0 .108-.152c.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6 6 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.6 6.6 0 0 0-.024-1.358 5.5 5.5 0 0 0-.939-2.339 5.3 5.3 0 0 0-.95-1.02l-.186-.152a.7.7 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a7 7 0 0 0-.097.726c0 .036-.007.066-.015.066a1 1 0 0 1-.149-.078A4.86 4.86 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a1 1 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a7 7 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2 2 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.4 8.4 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013m9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.7 3.7 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#7A8291" viewBox="0 0 24 24"><title>Ollama</title><path d="M16.361 10.26a.9.9 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.75.75 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.1 1.1 0 0 0-.466 0m-9.203.005c-.305.096-.533.32-.65.639a1.2 1.2 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.9.9 0 0 0-.565-.472 1 1 0 0 0-.474.007m4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.4.4 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.4.4 0 0 0-.193.032m.39-2.195c-.373.036-.475.05-.654.086a4.5 4.5 0 0 0-.951.328c-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a7 7 0 0 0-.855-.031zm.645.937a3.3 3.3 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3 3 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042q-.856-.16-1.35-.68c-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002m-5.503-11a1.65 1.65 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208l-.187.152a5.3 5.3 0 0 0-.949 1.02 5.5 5.5 0 0 0-.94 2.339 6.6 6.6 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.4 7.4 0 0 0-.548 1.873 5 5 0 0 0-.071.991c0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.9.9 0 0 0-.194-.25 1.7 1.7 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.8.8 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.14 3.14 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04s.398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2 2 0 0 1-.224.258.9.9 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a1 1 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516a9 9 0 0 0 0-1.258c-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141a1.4 1.4 0 0 0 .108-.152c.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6 6 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.6 6.6 0 0 0-.024-1.358 5.5 5.5 0 0 0-.939-2.339 5.3 5.3 0 0 0-.95-1.02l-.186-.152a.7.7 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a7 7 0 0 0-.097.726c0 .036-.007.066-.015.066a1 1 0 0 1-.149-.078A4.86 4.86 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a1 1 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a7 7 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2 2 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.4 8.4 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013m9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.7 3.7 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#fff" d="M44.559 19.646a11.957 11.957 0 0 0-1.028-9.822 12.094 12.094 0 0 0-13.026-5.802A11.962 11.962 0 0 0 21.485 0 12.097 12.097 0 0 0 9.95 8.373a11.964 11.964 0 0 0-7.997 5.8A12.097 12.097 0 0 0 3.44 28.356a11.957 11.957 0 0 0 1.028 9.822 12.094 12.094 0 0 0 13.026 5.802 11.953 11.953 0 0 0 9.02 4.02 12.096 12.096 0 0 0 11.54-8.379 11.964 11.964 0 0 0 7.997-5.8 12.099 12.099 0 0 0-1.491-14.177zM26.517 44.863a8.966 8.966 0 0 1-5.759-2.082 6.85 6.85 0 0 0 .284-.16L30.6 37.1c.49-.278.79-.799.786-1.361V22.265l4.04 2.332a.141.141 0 0 1 .078.111v11.16a9.006 9.006 0 0 1-8.987 8.995zM7.191 36.608a8.957 8.957 0 0 1-1.073-6.027c.071.042.195.119.284.17l9.558 5.52a1.556 1.556 0 0 0 1.57 0l11.67-6.738v4.665a.15.15 0 0 1-.057.124l-9.662 5.579a9.006 9.006 0 0 1-12.288-3.293zM4.675 15.744a8.966 8.966 0 0 1 4.682-3.943c0 .082-.005.228-.005.33v11.042a1.555 1.555 0 0 0 .785 1.359l11.669 6.736-4.04 2.333a.143.143 0 0 1-.136.012L7.967 28.03a9.006 9.006 0 0 1-3.293-12.284zm33.19 7.724L26.196 16.73l4.04-2.331a.143.143 0 0 1 .136-.012l9.664 5.579c4.302 2.485 5.776 7.989 3.29 12.29a8.991 8.991 0 0 1-4.68 3.943V24.827a1.553 1.553 0 0 0-.78-1.36zm4.02-6.051c-.07-.044-.195-.119-.283-.17l-9.558-5.52a1.556 1.556 0 0 0-1.57 0l-11.67 6.738V13.8a.15.15 0 0 1 .057-.124l9.662-5.574a8.995 8.995 0 0 1 13.36 9.315zm-25.277 8.315-4.04-2.333a.141.141 0 0 1-.079-.11v-11.16a8.997 8.997 0 0 1 14.753-6.91c-.073.04-.2.11-.283.161L17.4 10.9a1.552 1.552 0 0 0-.786 1.36l-.006 13.469zM18.803 21l5.198-3.002 5.197 3V27l-5.197 3-5.198-3z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#7A8291" d="M44.559 19.646a11.957 11.957 0 0 0-1.028-9.822 12.094 12.094 0 0 0-13.026-5.802A11.962 11.962 0 0 0 21.485 0 12.097 12.097 0 0 0 9.95 8.373a11.964 11.964 0 0 0-7.997 5.8A12.097 12.097 0 0 0 3.44 28.356a11.957 11.957 0 0 0 1.028 9.822 12.094 12.094 0 0 0 13.026 5.802 11.953 11.953 0 0 0 9.02 4.02 12.096 12.096 0 0 0 11.54-8.379 11.964 11.964 0 0 0 7.997-5.8 12.099 12.099 0 0 0-1.491-14.177zM26.517 44.863a8.966 8.966 0 0 1-5.759-2.082 6.85 6.85 0 0 0 .284-.16L30.6 37.1c.49-.278.79-.799.786-1.361V22.265l4.04 2.332a.141.141 0 0 1 .078.111v11.16a9.006 9.006 0 0 1-8.987 8.995zM7.191 36.608a8.957 8.957 0 0 1-1.073-6.027c.071.042.195.119.284.17l9.558 5.52a1.556 1.556 0 0 0 1.57 0l11.67-6.738v4.665a.15.15 0 0 1-.057.124l-9.662 5.579a9.006 9.006 0 0 1-12.288-3.293zM4.675 15.744a8.966 8.966 0 0 1 4.682-3.943c0 .082-.005.228-.005.33v11.042a1.555 1.555 0 0 0 .785 1.359l11.669 6.736-4.04 2.333a.143.143 0 0 1-.136.012L7.967 28.03a9.006 9.006 0 0 1-3.293-12.284zm33.19 7.724L26.196 16.73l4.04-2.331a.143.143 0 0 1 .136-.012l9.664 5.579c4.302 2.485 5.776 7.989 3.29 12.29a8.991 8.991 0 0 1-4.68 3.943V24.827a1.553 1.553 0 0 0-.78-1.36zm4.02-6.051c-.07-.044-.195-.119-.283-.17l-9.558-5.52a1.556 1.556 0 0 0-1.57 0l-11.67 6.738V13.8a.15.15 0 0 1 .057-.124l9.662-5.574a8.995 8.995 0 0 1 13.36 9.315zm-25.277 8.315-4.04-2.333a.141.141 0 0 1-.079-.11v-11.16a8.997 8.997 0 0 1 14.753-6.91c-.073.04-.2.11-.283.161L17.4 10.9a1.552 1.552 0 0 0-.786 1.36l-.006 13.469zM18.803 21l5.198-3.002 5.197 3V27l-5.197 3-5.198-3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>OpenRouter</title><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#7A8291" viewBox="0 0 24 24"><title>OpenRouter</title><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 685 B After Width: | Height: | Size: 688 B |
3
frontend/src/assets/Logos/podman.svg
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
10
frontend/src/assets/Logos/sglang.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="#A5300F" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17.4 7.7c0 4.6-4.7 4.9-4.7 8.6" />
|
||||
<path d="M8 10.7c3.3 0 4.2 2.4 4.2 5.6" />
|
||||
<circle cx="17.4" cy="4.6" r="3.1" fill="#FADDCD" />
|
||||
<circle cx="4.9" cy="10.7" r="3.1" fill="#FADDCD" />
|
||||
<rect x="7.9" y="16.3" width="9.6" height="6.6" rx="1.6" fill="#FADDCD" />
|
||||
<path d="M11.3 18.6 9.9 19.9l1.4 1.3M14.1 18.6l1.4 1.3-1.4 1.3M13.1 18.3l-1.2 3.3" stroke-width="1.1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 571 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1155 1000"><path fill="#fff" d="m577.3 0 577.4 1000H0z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1155 1000"><path fill="#7A8291" d="m577.3 0 577.4 1000H0z"/></svg>
|
||||
|
Before Width: | Height: | Size: 128 B After Width: | Height: | Size: 131 B |
4
frontend/src/assets/Logos/vllm.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m23.6 0-8.721 4.59L9.829 24h7.41z" fill="#30A2FF" />
|
||||
<path d="M9.83 24V5.142H.4z" fill="#FDB515" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 190 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" fill="#fff" stroke="#fff" viewBox="0 0 24 24"><path d="M20.98 11.802a1 1 0 0 0-.738-.771l-6.86-1.716 2.537-5.921a1 1 0 0 0-.317-1.192.996.996 0 0 0-1.234.024l-11 9a1 1 0 0 0 .39 1.744l6.719 1.681-3.345 5.854A1 1 0 0 0 8 22a1 1 0 0 0 .6-.2l12-9a1 1 0 0 0 .38-.998z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" fill="#7A8291" stroke="#7A8291" viewBox="0 0 24 24"><path d="M20.98 11.802a1 1 0 0 0-.738-.771l-6.86-1.716 2.537-5.921a1 1 0 0 0-.317-1.192.996.996 0 0 0-1.234.024l-11 9a1 1 0 0 0 .39 1.744l6.719 1.681-3.345 5.854A1 1 0 0 0 8 22a1 1 0 0 0 .6-.2l12-9a1 1 0 0 0 .38-.998z"/></svg>
|
||||
|
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 343 B |
@@ -2,6 +2,8 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.quick-filters-settings-container {
|
||||
flex: 0 0 0;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// The one `overflow: hidden` in the chain. Ancestors (RouteTab, AppLayout)
|
||||
// only hand height down; each pane below owns its own scroll.
|
||||
.layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Positioned so overlays (settings drawer) paint above the content pane
|
||||
// without changing this pane's layout width.
|
||||
.filters {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Bounded box for the OverlayScrollbar inside it (`.overlay-scrollbar` is
|
||||
// `height: 100%`), which owns the scrolling.
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
|
||||
import styles from './QuickFiltersLayout.module.scss';
|
||||
|
||||
// Same optionality as `<QuickFilters />` in JSX (honours its defaultProps).
|
||||
type QuickFiltersElementProps = JSX.LibraryManagedAttributes<
|
||||
typeof QuickFilters,
|
||||
ComponentProps<typeof QuickFilters>
|
||||
>;
|
||||
|
||||
export interface QuickFiltersLayoutProps {
|
||||
quickFilterProps: QuickFiltersElementProps;
|
||||
showFilters: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
testId?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function QuickFiltersLayout({
|
||||
quickFilterProps,
|
||||
showFilters,
|
||||
className,
|
||||
contentClassName,
|
||||
testId,
|
||||
children,
|
||||
}: QuickFiltersLayoutProps): JSX.Element {
|
||||
return (
|
||||
<div className={cx(styles.layout, className)} data-testid={testId}>
|
||||
{showFilters && (
|
||||
<aside
|
||||
className={styles.filters}
|
||||
data-testid="quick-filters-layout-filters"
|
||||
>
|
||||
<QuickFilters {...quickFilterProps} />
|
||||
</aside>
|
||||
)}
|
||||
<section
|
||||
className={cx(styles.content, contentClassName)}
|
||||
data-testid="quick-filters-layout-content"
|
||||
>
|
||||
<OverlayScrollbar>
|
||||
<div>{children}</div>
|
||||
</OverlayScrollbar>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuickFiltersLayout;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import { QuickFiltersSource } from '../../types';
|
||||
import QuickFiltersLayout from '../QuickFiltersLayout';
|
||||
|
||||
jest.mock('../QuickFiltersLayout.module.scss', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
layout: 'layout',
|
||||
filters: 'filters',
|
||||
content: 'content',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../QuickFilters', () => ({
|
||||
__esModule: true,
|
||||
default: ({ source }: { source: string }): JSX.Element => (
|
||||
<div data-testid="quick-filters">{source}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const quickFilterProps = {
|
||||
source: QuickFiltersSource.TRACES_EXPLORER,
|
||||
handleFilterVisibilityChange: jest.fn(),
|
||||
};
|
||||
|
||||
describe('QuickFiltersLayout', () => {
|
||||
it('renders QuickFilters with the given props inside the filters pane', () => {
|
||||
render(
|
||||
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
|
||||
const filtersPane = screen.getByTestId('quick-filters-layout-filters');
|
||||
expect(filtersPane).toContainElement(screen.getByTestId('quick-filters'));
|
||||
expect(screen.getByTestId('quick-filters')).toHaveTextContent(
|
||||
QuickFiltersSource.TRACES_EXPLORER,
|
||||
);
|
||||
expect(screen.getByTestId('quick-filters-layout-content')).toHaveTextContent(
|
||||
'content',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render the filters pane when showFilters is false', () => {
|
||||
render(
|
||||
<QuickFiltersLayout showFilters={false} quickFilterProps={quickFilterProps}>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('quick-filters-layout-filters'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('quick-filters')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('merges classNames onto the root and content panes', () => {
|
||||
render(
|
||||
<QuickFiltersLayout
|
||||
showFilters
|
||||
quickFilterProps={quickFilterProps}
|
||||
className="page-root"
|
||||
contentClassName="page-content"
|
||||
testId="page"
|
||||
>
|
||||
<div>content</div>
|
||||
</QuickFiltersLayout>,
|
||||
);
|
||||
|
||||
const root = screen.getByTestId('page');
|
||||
expect(root).toHaveClass('layout', 'page-root');
|
||||
expect(screen.getByTestId('quick-filters-layout-content')).toHaveClass(
|
||||
'content',
|
||||
'page-content',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6,27 +6,12 @@
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
width: 342px;
|
||||
height: 100%;
|
||||
background: var(--l1-background);
|
||||
transition: width 0.05s ease-in-out;
|
||||
overflow: hidden;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&.qf-logs-explorer {
|
||||
height: calc(100vh - 45px);
|
||||
}
|
||||
|
||||
&.qf-exceptions {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&.qf-api-monitoring {
|
||||
height: calc(100vh - 45px);
|
||||
}
|
||||
|
||||
&.qf-traces-explorer {
|
||||
height: calc(100vh - 45px);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
38
frontend/src/components/RouteTab/RouteTab.module.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
// Hands the parent's height down to the active pane and lets the pane scroll
|
||||
// its own content, so TopNav and the tab bar stay put. Child combinators only
|
||||
// (nested Tabs must not be caught).
|
||||
.routeTab {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.routeTab > :global(.ant-tabs-content-holder) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.routeTab > :global(.ant-tabs-content-holder) > :global(.ant-tabs-content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.routeTab
|
||||
> :global(.ant-tabs-content-holder)
|
||||
> :global(.ant-tabs-content)
|
||||
> :global(.ant-tabs-tabpane-active) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.routeTab
|
||||
> :global(.ant-tabs-content-holder)
|
||||
> :global(.ant-tabs-content)
|
||||
> :global(.ant-tabs-tabpane-active)
|
||||
> :global(.overlay-scrollbar) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -5,6 +5,11 @@ import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import RouteTab from './index';
|
||||
import { RouteTabProps } from './types';
|
||||
|
||||
jest.mock('./RouteTab.module.scss', () => ({
|
||||
__esModule: true,
|
||||
default: { routeTab: 'routeTab' },
|
||||
}));
|
||||
|
||||
function DummyComponent1(): JSX.Element {
|
||||
return <div>Dummy Component 1</div>;
|
||||
}
|
||||
@@ -74,6 +79,36 @@ describe('RouteTab component', () => {
|
||||
expect(history.location.pathname).toBe('/tab2');
|
||||
});
|
||||
|
||||
it('applies the layout class alongside a custom className', () => {
|
||||
const history = createMemoryHistory();
|
||||
const { container } = render(
|
||||
<Router history={history}>
|
||||
<RouteTab
|
||||
history={history}
|
||||
routes={testRoutes}
|
||||
activeKey="Tab1"
|
||||
className="custom-tabs"
|
||||
/>
|
||||
</Router>,
|
||||
);
|
||||
expect(container.querySelector('.ant-tabs')).toHaveClass(
|
||||
'routeTab',
|
||||
'custom-tabs',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the active tab content inside an overlay scrollbar', () => {
|
||||
const history = createMemoryHistory();
|
||||
const { container } = render(
|
||||
<Router history={history}>
|
||||
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
|
||||
</Router>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.ant-tabs-tabpane-active > .overlay-scrollbar'),
|
||||
).toHaveTextContent('Dummy Component 1');
|
||||
});
|
||||
|
||||
it('calls onChangeHandler on tab change', () => {
|
||||
const onChangeHandler = jest.fn();
|
||||
const history = createMemoryHistory();
|
||||
|
||||
@@ -5,20 +5,32 @@ import {
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
import { RouteTabProps } from './types';
|
||||
|
||||
import styles from './RouteTab.module.scss';
|
||||
|
||||
interface Params {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Each pane scrolls its own content inside an OverlayScrollbar, so the tab bar
|
||||
* stays put. Mounted as the page root the pane is bounded to the viewport; inside
|
||||
* a plain block wrapper the scroller is inert and the page scrolls as usual.
|
||||
* Pane content that needs a bounded box must size itself with `height: 100%`
|
||||
* (the scroller's viewport is block flow, so `flex: 1` has no effect there).
|
||||
*/
|
||||
function RouteTab({
|
||||
routes,
|
||||
activeKey,
|
||||
onChangeHandler,
|
||||
history,
|
||||
showRightSection,
|
||||
className,
|
||||
...rest
|
||||
}: RouteTabProps & TabsProps): JSX.Element {
|
||||
const params = useParams<Params>();
|
||||
@@ -50,11 +62,16 @@ function RouteTab({
|
||||
label: name,
|
||||
key,
|
||||
tabKey: route,
|
||||
children: <Component />,
|
||||
children: (
|
||||
<OverlayScrollbar>
|
||||
<Component />
|
||||
</OverlayScrollbar>
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className={cx(styles.routeTab, className)}
|
||||
onChange={onChange}
|
||||
destroyInactiveTabPane
|
||||
activeKey={currentRoute?.key || activeKey}
|
||||
|
||||
@@ -47,4 +47,5 @@ export enum LOCALSTORAGE {
|
||||
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
|
||||
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
|
||||
LLM_ATTRIBUTE_MAPPING_TEST_SPAN = 'LLM_ATTRIBUTE_MAPPING_TEST_SPAN',
|
||||
SAVED_VIEW_ENABLED = 'SAVED_VIEW_ENABLED',
|
||||
}
|
||||
|
||||
@@ -129,6 +129,10 @@ const themeColors = {
|
||||
salmon2: '#FFAB91',
|
||||
salmon3: '#E0876A',
|
||||
},
|
||||
/* Series palette (dark). Hues in the red band are deliberately absent: red is
|
||||
reserved for thresholds and error states, so an arbitrary series must never
|
||||
claim it. generateColor indexes by `hash % Object.keys(...).length`, so
|
||||
adding or removing an entry recolors every existing chart. */
|
||||
chartcolors: {
|
||||
// Blues (3)
|
||||
dodgerBlue: '#2F80ED',
|
||||
@@ -152,13 +156,13 @@ const themeColors = {
|
||||
|
||||
// Oranges (3)
|
||||
festivalOrange: '#F2994A',
|
||||
coralOrange: '#E17055',
|
||||
amber1: '#E1A155',
|
||||
pumpkin: '#FF7F50',
|
||||
|
||||
// Reds (3)
|
||||
radicalRed: '#FF1A66',
|
||||
crimsonRed: '#EB5757',
|
||||
fireRed: '#E10600',
|
||||
// Olives / Greens (3)
|
||||
olive1: '#DFC33A',
|
||||
olive2: '#D5E55D',
|
||||
green7: '#81C220',
|
||||
|
||||
// Pinks (3)
|
||||
hotPink: '#E84393',
|
||||
@@ -191,9 +195,9 @@ const themeColors = {
|
||||
orange1: '#D35400',
|
||||
orange2: '#E67E22',
|
||||
orange3: '#F5B041',
|
||||
red1: '#C0392B',
|
||||
red2: '#E74C3C',
|
||||
red3: '#EC7063',
|
||||
green8: '#5AC02B',
|
||||
green9: '#48E043',
|
||||
green10: '#68E788',
|
||||
pink1: '#D81B60',
|
||||
pink2: '#E91E63',
|
||||
pink3: '#F06292',
|
||||
@@ -212,9 +216,9 @@ const themeColors = {
|
||||
coral1: '#E67E22',
|
||||
coral2: '#F39C12',
|
||||
coral3: '#F5B041',
|
||||
crimson1: '#C0392B',
|
||||
crimson2: '#E74C3C',
|
||||
crimson3: '#EC7063',
|
||||
teal7: '#2BC07B',
|
||||
teal8: '#43E0C5',
|
||||
teal9: '#68D9E7',
|
||||
violet1: '#8E44AD',
|
||||
violet2: '#9B59B6',
|
||||
violet3: '#BB8FCE',
|
||||
@@ -224,18 +228,18 @@ const themeColors = {
|
||||
forest1: '#27AE60',
|
||||
forest2: '#2ECC71',
|
||||
forest3: '#58D68D',
|
||||
blush1: '#FF6F91',
|
||||
cyan4: '#83C2EB',
|
||||
blush2: '#FF85A2',
|
||||
blush3: '#FFA0B3',
|
||||
lavender1: '#9B59B6',
|
||||
lavender2: '#AF7AC5',
|
||||
lavender3: '#C39BD3',
|
||||
tomato1: '#E74C3C',
|
||||
tomato2: '#EC7063',
|
||||
tomato3: '#F1948A',
|
||||
salmon1: '#FF6B6B',
|
||||
salmon2: '#FF8787',
|
||||
salmon3: '#FFA1A1',
|
||||
blue7: '#4375E0',
|
||||
blue8: '#686DE7',
|
||||
indigo1: '#A68EED',
|
||||
indigo2: '#B980EA',
|
||||
purple6: '#EE98D9',
|
||||
olive3: '#F2F0AE',
|
||||
mustard1: '#F1C40F',
|
||||
mustard2: '#F7DC6F',
|
||||
mustard3: '#F9E79F',
|
||||
@@ -254,9 +258,9 @@ const themeColors = {
|
||||
blue4: '#2874A6',
|
||||
blue5: '#2E86C1',
|
||||
blue6: '#3498DB',
|
||||
red4: '#C0392B',
|
||||
red5: '#E74C3C',
|
||||
red6: '#EC7063',
|
||||
purple4: '#A52BC0',
|
||||
purple5: '#E043D0',
|
||||
magenta4: '#E768B5',
|
||||
orange4: '#D35400',
|
||||
orange5: '#E67E22',
|
||||
orange6: '#EB984E',
|
||||
@@ -267,18 +271,19 @@ const themeColors = {
|
||||
gold5: '#F1C40F',
|
||||
gold6: '#F4D03F',
|
||||
},
|
||||
/* Series palette (light). Same red-free constraint as chartcolors above. */
|
||||
lightModeColor: {
|
||||
radicalRed: '#D81B60',
|
||||
magenta1: '#D81B60',
|
||||
|
||||
dodgerBlueDark: '#1E5BD9',
|
||||
steelgrey: '#344B6B',
|
||||
steelpurple: '#5E548E',
|
||||
steelindigo: '#8E4A7C',
|
||||
steelpink: '#B63A6F',
|
||||
steelcoral: '#E14B5A',
|
||||
amber1: '#E1A14B',
|
||||
steelorange: '#E76F2F',
|
||||
steelgold: '#E09B00',
|
||||
steelrust: '#C93A50',
|
||||
olive1: '#C9BD3A',
|
||||
steelgreen: '#2F7D69',
|
||||
|
||||
mediumOrchidDark: '#8E24AA',
|
||||
@@ -286,17 +291,17 @@ const themeColors = {
|
||||
seaGreen: '#1E7F5A',
|
||||
turquoiseBlueDark: '#007EA7',
|
||||
silverDark: '#5F5F5F',
|
||||
outrageousOrangeDark: '#E64A19',
|
||||
roseBudDark: '#D84315',
|
||||
green1: '#ACDB24',
|
||||
green2: '#66CC21',
|
||||
deepSkyBlueDark: '#0277BD',
|
||||
royalBlue: '#2A4FDB',
|
||||
|
||||
avocadoDark: '#6B6B1E',
|
||||
mintGreenDark: '#2E9E55',
|
||||
chestnut: '#8B3A3A',
|
||||
green3: '#3F8B3A',
|
||||
limaDark: '#5C7F00',
|
||||
olive: '#6E7F00',
|
||||
beautyBushDark: '#C93C3C',
|
||||
green4: '#3CC964',
|
||||
|
||||
danube: '#4F6FB3',
|
||||
oliveDrab: '#4F7F1A',
|
||||
@@ -304,13 +309,13 @@ const themeColors = {
|
||||
electricLimeDark: '#6B8F00',
|
||||
robin: '#2F4FCC',
|
||||
|
||||
harleyOrange: '#CC2E12',
|
||||
teal1: '#1FBF83',
|
||||
gladeGreen: '#4F7F46',
|
||||
hemlock: '#5C5C45',
|
||||
vidaLoca: '#3D6B00',
|
||||
rust: '#993300',
|
||||
|
||||
red: '#C62828',
|
||||
teal2: '#28C6C1',
|
||||
blue: '#1A237E',
|
||||
green: '#1B7F3A',
|
||||
purple: '#6A1B9A',
|
||||
@@ -320,7 +325,7 @@ const themeColors = {
|
||||
brown: '#7A3A1E',
|
||||
teal: '#006D6F',
|
||||
limeDark: '#4C8C2B',
|
||||
maroon: '#6D1B1B',
|
||||
cyan1: '#1B546D',
|
||||
navy: '#0D1B5E',
|
||||
gray: '#616161',
|
||||
|
||||
@@ -328,25 +333,25 @@ const themeColors = {
|
||||
indigo: '#303F9F',
|
||||
slateGray: '#556B7C',
|
||||
chocolate: '#9C4A1A',
|
||||
tomato: '#E53935',
|
||||
blue1: '#3B74DF',
|
||||
steelBlue: '#3A6EA5',
|
||||
|
||||
peruDark: '#B35E00',
|
||||
darkOliveGreen: '#445B1F',
|
||||
indianRed: '#B04040',
|
||||
blue2: '#4041B0',
|
||||
mediumSlateBlue: '#5C6BC0',
|
||||
rosyBrownDark: '#A94444',
|
||||
indigo1: '#6644A9',
|
||||
darkSlateGray: '#2E4A4A',
|
||||
|
||||
fuchsia: '#C511C5',
|
||||
salmonDark: '#E64A3C',
|
||||
darkSalmonDark: '#C85A3A',
|
||||
indigo2: '#AD42E0',
|
||||
purple1: '#C83AC5',
|
||||
paleVioletRedDark: '#C2186A',
|
||||
|
||||
mediumPurple: '#7E57C2',
|
||||
darkOrchid: '#7B1FA2',
|
||||
mediumSeaGreenDark: '#2E8B57',
|
||||
lightCoralDark: '#E57373',
|
||||
purple2: '#E573BC',
|
||||
|
||||
gold: '#D4AF37',
|
||||
sandyBrownDark: '#C76A15',
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
.api-monitoring-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
.api-monitoring-explorer {
|
||||
.api-quick-filters-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
.api-quick-filter-left-section {
|
||||
width: 0%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.api-quick-filters-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
@@ -161,16 +153,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
.api-quick-filter-left-section {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-filtered-domains-message-container {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
@@ -20,20 +19,21 @@ function Explorer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||
<section className="api-quick-filter-left-section">
|
||||
<QuickFilters
|
||||
className="qf-api-monitoring"
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
signal={SignalType.API_MONITORING}
|
||||
showFilterCollapse={false}
|
||||
showQueryName={false}
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
useFieldApis={quickFilterFieldApis}
|
||||
/>
|
||||
</section>
|
||||
<QuickFiltersLayout
|
||||
className="api-monitoring-explorer"
|
||||
showFilters
|
||||
quickFilterProps={{
|
||||
className: 'qf-api-monitoring',
|
||||
source: QuickFiltersSource.API_MONITORING,
|
||||
signal: SignalType.API_MONITORING,
|
||||
showFilterCollapse: false,
|
||||
showQueryName: false,
|
||||
handleFilterVisibilityChange: (): void => {},
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
>
|
||||
<DomainList />
|
||||
</div>
|
||||
</QuickFiltersLayout>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
z-index: 0;
|
||||
background: var(--l1-background);
|
||||
|
||||
// Column so the bottom strip sits under the scrolling content, not inside it.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.full-screen-content {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -70,7 +74,9 @@
|
||||
|
||||
.chat-support-gateway {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: calc(20px + var(--bottom-strip-height, 0px));
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import { USER_PREFERENCES } from 'constants/userPreferences';
|
||||
import AIAssistantModal from 'container/AIAssistant/AIAssistantModal';
|
||||
import AIAssistantPanel from 'container/AIAssistant/AIAssistantPanel';
|
||||
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import BottomStrip from 'container/BottomStrip';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -51,6 +52,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import history from 'lib/history';
|
||||
import { isNull } from 'lodash-es';
|
||||
@@ -402,6 +404,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}, [pathname]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
const isSavedViewEnabled = useSavedViewEnabled();
|
||||
|
||||
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||
const pageTitle = t(routeKey);
|
||||
@@ -868,6 +871,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
</OverlayScrollbar>
|
||||
</LayoutContent>
|
||||
</Sentry.ErrorBoundary>
|
||||
|
||||
{isSavedViewEnabled && isToDisplayLayout && !renderFullScreen && (
|
||||
<BottomStrip />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoggedIn && isAIAssistantEnabled && (
|
||||
|
||||
@@ -12,8 +12,12 @@ export const Layout = styled(LayoutComponent)`
|
||||
}
|
||||
`;
|
||||
|
||||
// Takes the height left in `.app-content` after the bottom strip.
|
||||
// `min-height: 0` is not needed right now, overlayscrollbars already sets
|
||||
// `overflow: auto` here. Kept so this does not break if that goes away.
|
||||
export const LayoutContent = styled(LayoutComponent.Content)`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
|
||||
40
frontend/src/container/BottomStrip/BottomStrip.module.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
.strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
flex-shrink: 0;
|
||||
height: var(--bottom-strip-height);
|
||||
padding: 0 12px;
|
||||
|
||||
background: var(--l2-background);
|
||||
border-top: 1px solid var(--l2-border);
|
||||
|
||||
// font styles
|
||||
font-family: var(--font-family-sf-mono, monospace);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: var(--line-height-none);
|
||||
|
||||
// Above page content, below the body-portalled overlays that are meant to
|
||||
// cover the strip.
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Temporary placeholder for the left slot. Replaced later.
|
||||
.version {
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import BottomStrip, {
|
||||
BOTTOM_STRIP_HEIGHT,
|
||||
BOTTOM_STRIP_HEIGHT_VAR,
|
||||
BOTTOM_STRIP_ON_CLASS,
|
||||
} from '..';
|
||||
|
||||
describe('BottomStrip', () => {
|
||||
it('publishes the body class and height property while mounted', () => {
|
||||
const { unmount } = render(<BottomStrip />);
|
||||
|
||||
expect(document.body.classList.contains(BOTTOM_STRIP_ON_CLASS)).toBe(true);
|
||||
expect(document.body.style.getPropertyValue(BOTTOM_STRIP_HEIGHT_VAR)).toBe(
|
||||
`${BOTTOM_STRIP_HEIGHT}px`,
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(document.body.classList.contains(BOTTOM_STRIP_ON_CLASS)).toBe(false);
|
||||
expect(document.body.style.getPropertyValue(BOTTOM_STRIP_HEIGHT_VAR)).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
// The string is whatever the Go build injected, so it is rendered untouched —
|
||||
// same as SideNav. Release tags carry the "v", local builds do not.
|
||||
it.each([['v0.134.67'], ['main-64f1c2a']])(
|
||||
'renders the build version %p exactly as given',
|
||||
(version) => {
|
||||
const { getByTestId } = render(<BottomStrip />, undefined, {
|
||||
appContextOverrides: {
|
||||
versionData: { version, ee: 'Y', setupCompleted: true },
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('bottom-strip-version')).toHaveTextContent(version);
|
||||
},
|
||||
);
|
||||
|
||||
it('renders the strip without a version when none is available', () => {
|
||||
const { getByTestId, queryByTestId } = render(<BottomStrip />, undefined, {
|
||||
appContextOverrides: { versionData: null },
|
||||
});
|
||||
|
||||
expect(getByTestId('bottom-strip')).toBeInTheDocument();
|
||||
expect(queryByTestId('bottom-strip-version')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
42
frontend/src/container/BottomStrip/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import styles from './BottomStrip.module.scss';
|
||||
|
||||
export const BOTTOM_STRIP_HEIGHT = 24;
|
||||
|
||||
export const BOTTOM_STRIP_ON_CLASS = 'bottom-strip-on';
|
||||
export const BOTTOM_STRIP_HEIGHT_VAR = '--bottom-strip-height';
|
||||
|
||||
function BottomStrip(): JSX.Element {
|
||||
const { versionData } = useAppContext();
|
||||
const version = versionData?.version?.trim();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.body.classList.add(BOTTOM_STRIP_ON_CLASS);
|
||||
document.body.style.setProperty(
|
||||
BOTTOM_STRIP_HEIGHT_VAR,
|
||||
`${BOTTOM_STRIP_HEIGHT}px`,
|
||||
);
|
||||
|
||||
return (): void => {
|
||||
document.body.classList.remove(BOTTOM_STRIP_ON_CLASS);
|
||||
document.body.style.removeProperty(BOTTOM_STRIP_HEIGHT_VAR);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.strip} data-testid="bottom-strip">
|
||||
<div className={styles.left}>
|
||||
{version && (
|
||||
<span className={styles.version} data-testid="bottom-strip-version">
|
||||
{version}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.right} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BottomStrip;
|
||||
@@ -1,6 +1,8 @@
|
||||
.create-alert-v2-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
left: 63px;
|
||||
right: 0;
|
||||
background-color: var(--l1-background);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.explorer-options-container {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
left: calc(50% + 240px);
|
||||
transform: translate(calc(-50% - 120px), 0);
|
||||
transition: left 0.2s linear;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.explorer-option-droppable-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
width: -webkit-fill-available;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
}
|
||||
|
||||
.trace-explorer-page {
|
||||
display: flex;
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
@@ -75,32 +73,8 @@
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
|
||||
.filter {
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
border-right: 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background-color: var(--l1-background);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
width: 258px;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-explorer {
|
||||
width: 100%;
|
||||
background: var(--l1-background);
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
.trace-explorer.filters-expanded {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
@@ -188,26 +186,21 @@ function Explorer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div
|
||||
<QuickFiltersLayout
|
||||
className="trace-explorer-page"
|
||||
data-testid="llm-observability-explorer"
|
||||
testId="llm-observability-explorer"
|
||||
showFilters={isOpen}
|
||||
quickFilterProps={{
|
||||
className: 'qf-traces-explorer',
|
||||
source: QuickFiltersSource.AI_OBSERVABILITY,
|
||||
signal: SignalType.AI_OBSERVABILITY,
|
||||
useFieldApis: quickFiltersFieldApis,
|
||||
handleFilterVisibilityChange: (): void => {
|
||||
setOpen(!isOpen);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Card className="filter" hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
signal={SignalType.AI_OBSERVABILITY}
|
||||
useFieldApis={quickFiltersFieldApis}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<div
|
||||
className={cx('trace-explorer', {
|
||||
'filters-expanded': isOpen,
|
||||
})}
|
||||
>
|
||||
<div className="trace-explorer">
|
||||
<div className="trace-explorer-header">
|
||||
<Toolbar
|
||||
showAutoRefresh
|
||||
@@ -291,7 +284,7 @@ function Explorer(): JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</QuickFiltersLayout>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
.meter-explorer-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.meter-explorer-quick-filters-section {
|
||||
width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.meter-explorer-content-section {
|
||||
width: 100%;
|
||||
// Clearance for the fixed ExplorerOptions bar.
|
||||
padding-bottom: 80px;
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
@@ -83,14 +72,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.quick-filters-open {
|
||||
.meter-explorer-content-section {
|
||||
width: calc(100% - 280px);
|
||||
}
|
||||
}
|
||||
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.dashboards-and-alerts-popover-container {
|
||||
|
||||
@@ -3,9 +3,8 @@ import { useQueryClient } from 'react-query';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -121,29 +120,21 @@ function Explorer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div
|
||||
className={cx('meter-explorer-container', {
|
||||
'quick-filters-open': showQuickFilters,
|
||||
})}
|
||||
<QuickFiltersLayout
|
||||
className="meter-explorer-container"
|
||||
showFilters={showQuickFilters}
|
||||
quickFilterProps={{
|
||||
className: 'qf-meter-explorer',
|
||||
source: QuickFiltersSource.METER_EXPLORER,
|
||||
signal: SignalType.METER_EXPLORER,
|
||||
showFilterCollapse: true,
|
||||
showQueryName: false,
|
||||
handleFilterVisibilityChange: (): void => {
|
||||
setShowQuickFilters(!showQuickFilters);
|
||||
},
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cx('meter-explorer-quick-filters-section', {
|
||||
hidden: !showQuickFilters,
|
||||
})}
|
||||
>
|
||||
<QuickFilters
|
||||
className="qf-meter-explorer"
|
||||
source={QuickFiltersSource.METER_EXPLORER}
|
||||
signal={SignalType.METER_EXPLORER}
|
||||
showFilterCollapse
|
||||
showQueryName={false}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setShowQuickFilters(!showQuickFilters);
|
||||
}}
|
||||
useFieldApis={quickFilterFieldApis}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="meter-explorer-content-section">
|
||||
<div className="meter-explorer-explore-content">
|
||||
<div className="explore-header">
|
||||
@@ -196,7 +187,7 @@ function Explorer(): JSX.Element {
|
||||
splitedQueries={splitedQueries}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</QuickFiltersLayout>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,9 +60,6 @@
|
||||
.metrics-table-container {
|
||||
padding-bottom: 48px;
|
||||
.ant-table {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
@@ -184,7 +181,9 @@
|
||||
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new
|
||||
// fixed-bottom UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
width: calc(100% - 54px);
|
||||
background: var(--l1-background);
|
||||
padding: 16px;
|
||||
|
||||
@@ -4,11 +4,13 @@ import amazonMskUrl from '@/assets/Logos/amazon-msk.svg';
|
||||
import androidJavaMonitoringUrl from '@/assets/Logos/android-java-monitoring.svg';
|
||||
import androidKotlinMonitoringUrl from '@/assets/Logos/android-kotlin-monitoring.svg';
|
||||
import anthropicApiMonitoringUrl from '@/assets/Logos/anthropic-api-monitoring.svg';
|
||||
import antigravityUrl from '@/assets/Logos/antigravity.svg';
|
||||
import apacheDruidUrl from '@/assets/Logos/apache-druid.svg';
|
||||
import apacheUrl from '@/assets/Logos/apache.svg';
|
||||
import apiGatewayUrl from '@/assets/Logos/api-gateway.svg';
|
||||
import argocdUrl from '@/assets/Logos/argocd.svg';
|
||||
import aspnetUrl from '@/assets/Logos/aspnet.svg';
|
||||
import auroraUrl from '@/assets/Logos/aurora.svg';
|
||||
import auth0Url from '@/assets/Logos/auth0.svg';
|
||||
import autogenUrl from '@/assets/Logos/autogen.svg';
|
||||
import awsAlbUrl from '@/assets/Logos/aws-alb.svg';
|
||||
@@ -28,6 +30,7 @@ import celeryUrl from '@/assets/Logos/celery.svg';
|
||||
import certManagerUrl from '@/assets/Logos/cert-manager.svg';
|
||||
import claudeCodeUrl from '@/assets/Logos/claude-code.svg';
|
||||
import clickhouseUrl from '@/assets/Logos/clickhouse.svg';
|
||||
import clineUrl from '@/assets/Logos/cline.svg';
|
||||
import cloudflareUrl from '@/assets/Logos/cloudflare.svg';
|
||||
import cloudnativePgUrl from '@/assets/Logos/cloudnative-pg.svg';
|
||||
import cloudwatchLogsUrl from '@/assets/Logos/cloudwatch-logs.svg';
|
||||
@@ -36,8 +39,10 @@ import confluentKafkaUrl from '@/assets/Logos/confluent-kafka.svg';
|
||||
import convexLogoUrl from '@/assets/Logos/convex-logo.svg';
|
||||
import cppUrl from '@/assets/Logos/cpp.svg';
|
||||
import crewAiUrl from '@/assets/Logos/crew-ai.svg';
|
||||
import cursorUrl from '@/assets/Logos/cursor.svg';
|
||||
import dashboardsUrl from '@/assets/Logos/dashboards.svg';
|
||||
import datadogUrl from '@/assets/Logos/datadog.svg';
|
||||
import daytonaUrl from '@/assets/Logos/daytona.svg';
|
||||
import dbosUrl from '@/assets/Logos/dbos.svg';
|
||||
import deepseekUrl from '@/assets/Logos/deepseek.svg';
|
||||
import denoUrl from '@/assets/Logos/deno.svg';
|
||||
@@ -47,6 +52,7 @@ import documentLoadUrl from '@/assets/Logos/document-load.svg';
|
||||
import dotnetUrl from '@/assets/Logos/dotnet.svg';
|
||||
import dspyUrl from '@/assets/Logos/dspy.svg';
|
||||
import dynamodbUrl from '@/assets/Logos/dynamodb.svg';
|
||||
import e2bUrl from '@/assets/Logos/e2b.svg';
|
||||
import ec2Url from '@/assets/Logos/ec2.svg';
|
||||
import ecsUrl from '@/assets/Logos/ecs.svg';
|
||||
import eksUrl from '@/assets/Logos/eks.svg';
|
||||
@@ -55,6 +61,7 @@ import elasticsearchUrl from '@/assets/Logos/elasticsearch.svg';
|
||||
import elbUrl from '@/assets/Logos/elb.svg';
|
||||
import elixirUrl from '@/assets/Logos/elixir.svg';
|
||||
import elkUrl from '@/assets/Logos/elk.svg';
|
||||
import emqxUrl from '@/assets/Logos/emqx.svg';
|
||||
import envoyUrl from '@/assets/Logos/envoy.svg';
|
||||
import externalApiMonitoringUrl from '@/assets/Logos/external-api-monitoring.svg';
|
||||
import fluentbitUrl from '@/assets/Logos/fluentbit.svg';
|
||||
@@ -101,6 +108,7 @@ import javascriptUrl from '@/assets/Logos/javascript.svg';
|
||||
import jbossUrl from '@/assets/Logos/jboss.svg';
|
||||
import jenkinsUrl from '@/assets/Logos/jenkins.svg';
|
||||
import kafkaUrl from '@/assets/Logos/kafka.svg';
|
||||
import karpenterUrl from '@/assets/Logos/karpenter.svg';
|
||||
import kedaUrl from '@/assets/Logos/keda.svg';
|
||||
import kongUrl from '@/assets/Logos/kong.svg';
|
||||
import kubernetesUrl from '@/assets/Logos/kubernetes.svg';
|
||||
@@ -116,9 +124,11 @@ import logrusUrl from '@/assets/Logos/logrus.svg';
|
||||
import logsUrl from '@/assets/Logos/logs.svg';
|
||||
import logstashUrl from '@/assets/Logos/logstash.svg';
|
||||
import mastraUrl from '@/assets/Logos/mastra.svg';
|
||||
import metaUrl from '@/assets/Logos/meta.svg';
|
||||
import metricsUrl from '@/assets/Logos/metrics.svg';
|
||||
import microsoftSqlServerUrl from '@/assets/Logos/microsoft-sql-server.svg';
|
||||
import mistralUrl from '@/assets/Logos/mistral.svg';
|
||||
import modalUrl from '@/assets/Logos/modal.svg';
|
||||
import mongoUrl from '@/assets/Logos/mongo.svg';
|
||||
import n8nUrl from '@/assets/Logos/n8n.svg';
|
||||
import neonUrl from '@/assets/Logos/neon.svg';
|
||||
@@ -141,6 +151,7 @@ import phpUrl from '@/assets/Logos/php.svg';
|
||||
import pinoUrl from '@/assets/Logos/pino.svg';
|
||||
import pipecatUrl from '@/assets/Logos/pipecat.svg';
|
||||
import planetscaleUrl from '@/assets/Logos/planetscale.svg';
|
||||
import podmanUrl from '@/assets/Logos/podman.svg';
|
||||
import postgresqlUrl from '@/assets/Logos/postgresql.svg';
|
||||
import prometheusUrl from '@/assets/Logos/prometheus.svg';
|
||||
import pydanticAiUrl from '@/assets/Logos/pydantic-ai.svg';
|
||||
@@ -158,6 +169,7 @@ import rubyOnRailsUrl from '@/assets/Logos/ruby-on-rails.svg';
|
||||
import rustUrl from '@/assets/Logos/rust.svg';
|
||||
import s3Url from '@/assets/Logos/s3.svg';
|
||||
import scalaUrl from '@/assets/Logos/scala.svg';
|
||||
import sglangUrl from '@/assets/Logos/sglang.svg';
|
||||
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
|
||||
import slogUrl from '@/assets/Logos/slog.svg';
|
||||
import slurmUrl from '@/assets/Logos/slurm.svg';
|
||||
@@ -179,6 +191,7 @@ import traceloopUrl from '@/assets/Logos/traceloop.svg';
|
||||
import tracesUrl from '@/assets/Logos/traces.svg';
|
||||
import vectorUrl from '@/assets/Logos/vector.svg';
|
||||
import vercelUrl from '@/assets/Logos/vercel.svg';
|
||||
import vllmUrl from '@/assets/Logos/vllm.svg';
|
||||
import vmUrl from '@/assets/Logos/vm.svg';
|
||||
import vpcUrl from '@/assets/Logos/vpc.svg';
|
||||
import windowsUrl from '@/assets/Logos/windows.svg';
|
||||
@@ -1014,35 +1027,64 @@ const onboardingConfigWithLinks = [
|
||||
'tracing',
|
||||
],
|
||||
id: 'golang',
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
link: '/docs/instrumentation/golang/overview/',
|
||||
question: {
|
||||
desc: 'What is your Environment?',
|
||||
desc: 'How do you want to instrument your application?',
|
||||
helpText:
|
||||
'The SDK needs code changes. Compile-time instrumentation rebuilds the binary with otelc. eBPF attaches to a running binary and needs neither.',
|
||||
helpLink: '/docs/instrumentation/golang/overview/',
|
||||
helpLinkText: 'Compare the options →',
|
||||
type: 'select',
|
||||
entityID: 'environment',
|
||||
entityID: 'framework',
|
||||
options: [
|
||||
{
|
||||
key: 'vm',
|
||||
label: 'VM',
|
||||
imgUrl: vmUrl,
|
||||
key: 'golang-sdk',
|
||||
label: 'OpenTelemetry SDK',
|
||||
imgUrl: goUrl,
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
question: {
|
||||
desc: 'What is your Environment?',
|
||||
type: 'select',
|
||||
entityID: 'environment',
|
||||
options: [
|
||||
{
|
||||
key: 'vm',
|
||||
label: 'VM',
|
||||
imgUrl: vmUrl,
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
},
|
||||
{
|
||||
key: 'k8s',
|
||||
label: 'Kubernetes',
|
||||
imgUrl: kubernetesUrl,
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
},
|
||||
{
|
||||
key: 'windows',
|
||||
label: 'Windows',
|
||||
imgUrl: windowsUrl,
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
},
|
||||
{
|
||||
key: 'docker',
|
||||
label: 'Docker',
|
||||
imgUrl: dockerUrl,
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'k8s',
|
||||
label: 'Kubernetes',
|
||||
imgUrl: kubernetesUrl,
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
key: 'golang-compile-time',
|
||||
label: 'Compile-Time (otelc)',
|
||||
imgUrl: goUrl,
|
||||
link: '/docs/instrumentation/golang/compile-time-instrumentation/',
|
||||
},
|
||||
{
|
||||
key: 'windows',
|
||||
label: 'Windows',
|
||||
imgUrl: windowsUrl,
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
},
|
||||
{
|
||||
key: 'docker',
|
||||
label: 'Docker',
|
||||
imgUrl: dockerUrl,
|
||||
link: '/docs/instrumentation/opentelemetry-golang/',
|
||||
key: 'golang-ebpf',
|
||||
label: 'eBPF',
|
||||
imgUrl: opentelemetryUrl,
|
||||
link: '/docs/instrumentation/golang/ebpf-instrumentation/',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -2868,22 +2910,28 @@ const onboardingConfigWithLinks = [
|
||||
imgUrl: lambdaUrl,
|
||||
link: '/docs/aws-monitoring/lambda/lambda-traces/',
|
||||
question: {
|
||||
desc: 'Which runtime does your Lambda function use?',
|
||||
desc: 'How is your Lambda function packaged?',
|
||||
helpText:
|
||||
'Python, Node.js, Java, and Ruby use the OpenTelemetry auto-instrumentation layer. Go has no layer, so you add the SDK to your code.',
|
||||
'Zip functions use the OpenTelemetry auto-instrumentation layer, except Go, which has no layer and needs the SDK in your code. Layers do not attach to container images, so those bake instrumentation into the image.',
|
||||
options: [
|
||||
{
|
||||
key: 'aws-lambda-traces-auto',
|
||||
label: 'Python, Node.js, Java, Ruby',
|
||||
label: 'Zip - Python, Node.js, Java, Ruby',
|
||||
imgUrl: lambdaUrl,
|
||||
link: '/docs/aws-monitoring/lambda/lambda-traces/',
|
||||
},
|
||||
{
|
||||
key: 'aws-lambda-traces-golang',
|
||||
label: 'Go',
|
||||
label: 'Zip - Go',
|
||||
imgUrl: goUrl,
|
||||
link: '/docs/aws-monitoring/lambda/lambda-traces-golang/',
|
||||
},
|
||||
{
|
||||
key: 'aws-lambda-traces-container-image',
|
||||
label: 'Container Image',
|
||||
imgUrl: dockerUrl,
|
||||
link: '/docs/aws-monitoring/lambda/lambda-traces-container-image/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -7489,5 +7537,455 @@ const onboardingConfigWithLinks = [
|
||||
id: 'mysql-logs',
|
||||
link: '/docs/integrations/opentelemetry-mysql/',
|
||||
},
|
||||
{
|
||||
dataSource: 'cursor',
|
||||
label: 'Cursor',
|
||||
imgUrl: cursorUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'ai coding assistant',
|
||||
'cursor',
|
||||
'cursor ai',
|
||||
'cursor observability',
|
||||
'cursor traces',
|
||||
'ide',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'token usage',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'cursor',
|
||||
link: '/docs/cursor-observability/',
|
||||
},
|
||||
{
|
||||
dataSource: 'antigravity-cli',
|
||||
label: 'Antigravity CLI',
|
||||
imgUrl: antigravityUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'ai coding assistant',
|
||||
'antigravity',
|
||||
'antigravity cli',
|
||||
'cli',
|
||||
'google antigravity',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'quota',
|
||||
'tool calls',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'antigravity-cli',
|
||||
link: '/docs/antigravity-cli-monitoring/',
|
||||
},
|
||||
{
|
||||
dataSource: 'qwen-code',
|
||||
label: 'Qwen Code',
|
||||
imgUrl: qwenUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'ai coding assistant',
|
||||
'cli',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'qwen',
|
||||
'qwen code',
|
||||
'qwen code cli',
|
||||
'token spend',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'qwen-code',
|
||||
link: '/docs/qwen-code-observability/',
|
||||
},
|
||||
{
|
||||
dataSource: 'deepseek-harness',
|
||||
label: 'DeepSeek Harness',
|
||||
imgUrl: deepseekUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'ai coding assistant',
|
||||
'deepseek',
|
||||
'deepseek harness',
|
||||
'dsh',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'token spend',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'deepseek-harness',
|
||||
link: '/docs/deepseek-harness-observability/',
|
||||
},
|
||||
{
|
||||
dataSource: 'muse-spark',
|
||||
label: 'Meta Muse Spark',
|
||||
imgUrl: metaUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'meta',
|
||||
'meta model api',
|
||||
'monitoring',
|
||||
'muse',
|
||||
'muse spark',
|
||||
'observability',
|
||||
'prompt cache',
|
||||
'reasoning tokens',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'muse-spark',
|
||||
link: '/docs/muse-spark-monitoring/',
|
||||
},
|
||||
{
|
||||
dataSource: 'openai-agents-sdk',
|
||||
label: 'OpenAI Agents SDK',
|
||||
imgUrl: openaiUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'agents sdk',
|
||||
'handoffs',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'openai',
|
||||
'openai agents sdk',
|
||||
'tool calls',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'openai-agents-sdk',
|
||||
link: '/docs/openai-agents-sdk-observability/',
|
||||
},
|
||||
{
|
||||
dataSource: 'e2b',
|
||||
label: 'E2B Sandbox',
|
||||
imgUrl: e2bUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'code execution',
|
||||
'e2b',
|
||||
'e2b sandbox',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'sandbox',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'e2b',
|
||||
link: '/docs/e2b-monitoring/',
|
||||
},
|
||||
{
|
||||
dataSource: 'daytona',
|
||||
label: 'Daytona Sandbox',
|
||||
imgUrl: daytonaUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'code execution',
|
||||
'daytona',
|
||||
'daytona sandbox',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'sandbox',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'daytona',
|
||||
link: '/docs/daytona-monitoring/',
|
||||
},
|
||||
{
|
||||
dataSource: 'vercel-sandbox',
|
||||
label: 'Vercel Sandbox',
|
||||
imgUrl: vercelUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'code execution',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'sandbox',
|
||||
'traces',
|
||||
'tracing',
|
||||
'vercel',
|
||||
'vercel sandbox',
|
||||
],
|
||||
id: 'vercel-sandbox',
|
||||
link: '/docs/vercel-sandbox-observability/',
|
||||
},
|
||||
{
|
||||
dataSource: 'modal',
|
||||
label: 'Modal',
|
||||
imgUrl: modalUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'container metrics',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'modal',
|
||||
'modal functions',
|
||||
'modal sandbox',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'sandbox',
|
||||
'serverless',
|
||||
'traces',
|
||||
'tracing',
|
||||
],
|
||||
id: 'modal',
|
||||
link: '/docs/integrations/modal/',
|
||||
},
|
||||
{
|
||||
dataSource: 'cline',
|
||||
label: 'Cline',
|
||||
imgUrl: clineUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'ai coding assistant',
|
||||
'cline',
|
||||
'cline monitoring',
|
||||
'cost per model',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'token usage',
|
||||
'vs code',
|
||||
],
|
||||
id: 'cline',
|
||||
link: '/docs/cline-monitoring/',
|
||||
},
|
||||
{
|
||||
dataSource: 'muse-code',
|
||||
label: 'Meta Muse Code',
|
||||
imgUrl: metaUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'agent',
|
||||
'ai coding assistant',
|
||||
'cli',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'meta',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'muse',
|
||||
'muse code',
|
||||
'observability',
|
||||
'prompt cache',
|
||||
'token usage',
|
||||
],
|
||||
id: 'muse-code',
|
||||
link: '/docs/muse-code-monitoring/',
|
||||
},
|
||||
{
|
||||
dataSource: 'vllm',
|
||||
label: 'vLLM',
|
||||
imgUrl: vllmUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'inference server',
|
||||
'kv cache',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'time to first token',
|
||||
'token throughput',
|
||||
'vllm',
|
||||
],
|
||||
id: 'vllm',
|
||||
link: '/docs/vllm-observability/',
|
||||
},
|
||||
{
|
||||
dataSource: 'sglang',
|
||||
label: 'SGLang',
|
||||
imgUrl: sglangUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'inference server',
|
||||
'kv cache',
|
||||
'llm',
|
||||
'llm monitoring',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'sglang',
|
||||
'time to first token',
|
||||
'token throughput',
|
||||
],
|
||||
id: 'sglang',
|
||||
link: '/docs/sglang-observability/',
|
||||
},
|
||||
{
|
||||
dataSource: 'karpenter',
|
||||
label: 'Karpenter',
|
||||
imgUrl: karpenterUrl,
|
||||
tags: ['infrastructure monitoring', 'metrics'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'autoscaling',
|
||||
'disruption',
|
||||
'karpenter',
|
||||
'karpenter metrics',
|
||||
'kubernetes',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'node provisioning',
|
||||
'observability',
|
||||
'opentelemetry karpenter',
|
||||
'prometheus',
|
||||
],
|
||||
id: 'karpenter',
|
||||
link: '/docs/metrics-management/opentelemetry-karpenter-metrics/',
|
||||
},
|
||||
{
|
||||
dataSource: 'podman',
|
||||
label: 'Podman',
|
||||
imgUrl: podmanUrl,
|
||||
tags: ['infrastructure monitoring', 'metrics'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'container metrics',
|
||||
'containers',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'opentelemetry podman',
|
||||
'podman',
|
||||
'podman metrics',
|
||||
],
|
||||
id: 'podman',
|
||||
link: '/docs/metrics-management/opentelemetry-podman-metrics/',
|
||||
},
|
||||
{
|
||||
dataSource: 'emqx',
|
||||
label: 'EMQX',
|
||||
imgUrl: emqxUrl,
|
||||
tags: ['Messaging Queues', 'metrics'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'broker',
|
||||
'emqx',
|
||||
'emqx metrics',
|
||||
'iot',
|
||||
'messaging queues',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'mqtt',
|
||||
'observability',
|
||||
'opentelemetry emqx',
|
||||
],
|
||||
id: 'emqx',
|
||||
link: '/docs/integrations/opentelemetry-emqx/',
|
||||
},
|
||||
{
|
||||
dataSource: 'aws-rds-aurora',
|
||||
label: 'AWS RDS Aurora',
|
||||
imgUrl: auroraUrl,
|
||||
tags: ['AWS', 'database'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'aurora',
|
||||
'aurora mysql',
|
||||
'aurora postgresql',
|
||||
'aws',
|
||||
'cloudwatch',
|
||||
'database',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'rds',
|
||||
'rds aurora',
|
||||
],
|
||||
id: 'aws-rds-aurora',
|
||||
link: '/docs/integrations/aws-rds-aurora/',
|
||||
},
|
||||
{
|
||||
dataSource: 'gcp-cloud-storage',
|
||||
label: 'GCP Cloud Storage',
|
||||
imgUrl: gcpCloudStorageUrl,
|
||||
tags: ['GCP'],
|
||||
module: 'dashboards',
|
||||
relatedSearchKeywords: [
|
||||
'bucket',
|
||||
'cloud storage',
|
||||
'gcp',
|
||||
'gcp cloud storage',
|
||||
'gcp integration',
|
||||
'gcs',
|
||||
'google cloud',
|
||||
'metrics',
|
||||
'monitoring',
|
||||
'object storage',
|
||||
'observability',
|
||||
],
|
||||
id: 'gcp-cloud-storage',
|
||||
link: '/integrations/gcp?service=cloudstorage',
|
||||
internalRedirect: true,
|
||||
},
|
||||
{
|
||||
dataSource: 'aws-lambda-microvms',
|
||||
label: 'AWS Lambda MicroVMs',
|
||||
imgUrl: lambdaUrl,
|
||||
tags: ['AWS'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'aws',
|
||||
'lambda',
|
||||
'lambda microvms',
|
||||
'logs',
|
||||
'metrics',
|
||||
'microvm',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'opentelemetry collector',
|
||||
'serverless',
|
||||
'traces',
|
||||
],
|
||||
id: 'aws-lambda-microvms',
|
||||
link: '/docs/aws-monitoring/lambda-microvms/',
|
||||
},
|
||||
];
|
||||
export default onboardingConfigWithLinks;
|
||||
|
||||
@@ -114,8 +114,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.beta-tag {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function NavItem({
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{showIcon && <div className="nav-item-active-marker" />}
|
||||
<div className={cx('nav-item-data', isBeta ? 'beta-tag' : '')}>
|
||||
<div className="nav-item-data">
|
||||
{showIcon && (
|
||||
<div className={cx('nav-item-icon', isEarlyAccess ? 'noz-wave' : '')}>
|
||||
{icon}
|
||||
|
||||
11
frontend/src/hooks/useSavedViewEnabled.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useSavedViewEnabled(): boolean {
|
||||
const [isEnabled] = useState(
|
||||
() => getLocalStorageKey(LOCALSTORAGE.SAVED_VIEW_ENABLED) === 'true',
|
||||
);
|
||||
|
||||
return isEnabled;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
type BuilderField,
|
||||
panelTypeDataSourceFormValuesMap,
|
||||
type PartialPanelTypes,
|
||||
} from 'lib/query/panelTypeDataSourceFormValuesMap';
|
||||
import { isStaticPanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
/**
|
||||
* The map is composed from a few shape rules rather than spelled out per panel type
|
||||
* and data source. These specs pin the rules themselves — each one fails only when a
|
||||
* rule changes, which is the moment to stop and decide, rather than whenever any
|
||||
* field moves.
|
||||
*
|
||||
* The composition it replaced was checked cell by cell against the previous literal
|
||||
* table, which is in git history at `main:frontend/src/lib/query/panelQuery.ts`.
|
||||
*/
|
||||
function fieldsFor(
|
||||
panelType: keyof PartialPanelTypes,
|
||||
dataSource: DataSource,
|
||||
): BuilderField[] {
|
||||
return panelTypeDataSourceFormValuesMap[panelType][dataSource].builder
|
||||
.queryData;
|
||||
}
|
||||
|
||||
/** Fields present in `to` but not in `from`. */
|
||||
function added(from: BuilderField[], to: BuilderField[]): BuilderField[] {
|
||||
return to.filter((field) => !from.includes(field)).sort();
|
||||
}
|
||||
|
||||
/** Panel types built on the aggregating field list. */
|
||||
const AGGREGATING_TYPES: (keyof PartialPanelTypes)[] = [
|
||||
PANEL_TYPES.AREA,
|
||||
PANEL_TYPES.BAR,
|
||||
PANEL_TYPES.HISTOGRAM,
|
||||
PANEL_TYPES.TABLE,
|
||||
PANEL_TYPES.PIE,
|
||||
];
|
||||
|
||||
/** Panel types that reduce each series to one cell or slice. */
|
||||
const SCALAR_TYPES: (keyof PartialPanelTypes)[] = [
|
||||
PANEL_TYPES.TABLE,
|
||||
PANEL_TYPES.PIE,
|
||||
];
|
||||
|
||||
describe('panelTypeDataSourceFormValuesMap', () => {
|
||||
const seriesLogs = fieldsFor(PANEL_TYPES.TIME_SERIES, DataSource.LOGS);
|
||||
const seriesMetrics = fieldsFor(PANEL_TYPES.TIME_SERIES, DataSource.METRICS);
|
||||
|
||||
// A kind with no entry throws on switch: handleQueryChange reads
|
||||
// map[panelType][dataSource] straight through, and the cast at its call site
|
||||
// hides the gap. Static kinds return before that call, so they are exempt.
|
||||
it('covers every panel kind that can be switched to', () => {
|
||||
const uncovered = Object.entries(PANEL_KIND_TO_PANEL_TYPE)
|
||||
.filter(([kind]) => !isStaticPanelKind(kind as never))
|
||||
.map(([, panelType]) => panelType)
|
||||
.filter((panelType) => !(panelType in panelTypeDataSourceFormValuesMap));
|
||||
|
||||
expect(uncovered).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('shares one builder surface between logs and traces', () => {
|
||||
Object.values(panelTypeDataSourceFormValuesMap).forEach((sources) => {
|
||||
expect(sources[DataSource.LOGS].builder.queryData).toStrictEqual(
|
||||
sources[DataSource.TRACES].builder.queryData,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// The provider pushes onto the list it reads from this map, so two cells backed by
|
||||
// one instance would leak fields into each other.
|
||||
it('gives every cell its own array instance', () => {
|
||||
const arrays = Object.values(panelTypeDataSourceFormValuesMap).flatMap(
|
||||
(sources) =>
|
||||
Object.values(sources).map((source) => source.builder.queryData),
|
||||
);
|
||||
|
||||
expect(new Set(arrays).size).toBe(arrays.length);
|
||||
});
|
||||
|
||||
// One consequence of composing: the aggregating types share a single field list, so
|
||||
// an edit meant for charts reaches table and pie too.
|
||||
it.each(AGGREGATING_TYPES)(
|
||||
'gives %s the same non-metrics fields as a time series',
|
||||
(panelType) => {
|
||||
expect(fieldsFor(panelType, DataSource.LOGS)).toStrictEqual(seriesLogs);
|
||||
},
|
||||
);
|
||||
|
||||
it('adds both metrics aggregation steps for metrics', () => {
|
||||
expect(added(seriesLogs, seriesMetrics)).toStrictEqual([
|
||||
'spaceAggregation',
|
||||
'timeAggregation',
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(SCALAR_TYPES)('offers reduceTo to %s on metrics only', (panelType) => {
|
||||
expect(
|
||||
added(seriesMetrics, fieldsFor(panelType, DataSource.METRICS)),
|
||||
).toStrictEqual(['reduceTo']);
|
||||
expect(fieldsFor(panelType, DataSource.LOGS)).not.toContain('reduceTo');
|
||||
});
|
||||
|
||||
it('drops grouping, paging and ordering for a single value', () => {
|
||||
const value = fieldsFor(PANEL_TYPES.VALUE, DataSource.LOGS);
|
||||
|
||||
expect(added(value, seriesLogs)).toStrictEqual([
|
||||
'groupBy',
|
||||
'limit',
|
||||
'orderBy',
|
||||
]);
|
||||
expect(value).toContain('reduceTo');
|
||||
});
|
||||
|
||||
it('offers no aggregation fields to raw rows', () => {
|
||||
const rows = fieldsFor(PANEL_TYPES.LIST, DataSource.LOGS);
|
||||
|
||||
expect(rows).not.toContain('aggregateAttribute');
|
||||
expect(rows).not.toContain('aggregateOperator');
|
||||
expect(rows).not.toContain('groupBy');
|
||||
expect(rows).not.toContain('having');
|
||||
expect(rows).not.toContain('stepInterval');
|
||||
});
|
||||
|
||||
it('drops paging and ordering for metrics rows', () => {
|
||||
expect(
|
||||
added(
|
||||
fieldsFor(PANEL_TYPES.LIST, DataSource.METRICS),
|
||||
fieldsFor(PANEL_TYPES.LIST, DataSource.LOGS),
|
||||
),
|
||||
).toStrictEqual(['functions', 'limit', 'orderBy']);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Panel query shaping shared by the dashboard panel editor and the query
|
||||
* builder: the per-panel-type field allowlist, the panel-type switch, and the
|
||||
* dirty-check used to decide whether a panel has unsaved query edits.
|
||||
* builder: the panel-type switch and the dirty-check used to decide whether a
|
||||
* panel has unsaved query edits.
|
||||
*/
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
} from 'constants/queryBuilder';
|
||||
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
panelTypeDataSourceFormValuesMap,
|
||||
PartialPanelTypes,
|
||||
} from 'lib/query/panelTypeDataSourceFormValuesMap';
|
||||
|
||||
// Asks "would saving the current panel change the persisted widget spec?".
|
||||
//
|
||||
@@ -91,453 +95,6 @@ export const getIsQueryModified = (
|
||||
);
|
||||
};
|
||||
|
||||
export type PartialPanelTypes = {
|
||||
[PANEL_TYPES.BAR]: 'bar';
|
||||
[PANEL_TYPES.LIST]: 'list';
|
||||
[PANEL_TYPES.TABLE]: 'table';
|
||||
[PANEL_TYPES.TIME_SERIES]: 'graph';
|
||||
[PANEL_TYPES.VALUE]: 'value';
|
||||
[PANEL_TYPES.PIE]: 'pie';
|
||||
[PANEL_TYPES.HISTOGRAM]: 'histogram';
|
||||
};
|
||||
|
||||
export const panelTypeDataSourceFormValuesMap: Record<
|
||||
keyof PartialPanelTypes,
|
||||
Record<DataSource, any>
|
||||
> = {
|
||||
[PANEL_TYPES.BAR]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
'queryName',
|
||||
'disabled',
|
||||
'functions',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[PANEL_TYPES.TIME_SERIES]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
'queryName',
|
||||
'disabled',
|
||||
'functions',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[PANEL_TYPES.HISTOGRAM]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
'queryName',
|
||||
'disabled',
|
||||
'functions',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[PANEL_TYPES.TABLE]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'expression',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'reduceTo',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'expression',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[PANEL_TYPES.PIE]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'expression',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'reduceTo',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'queryName',
|
||||
'expression',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[PANEL_TYPES.LIST]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'queryName',
|
||||
'filters',
|
||||
'filter',
|
||||
'limit',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: ['queryName', 'filters', 'filter', 'aggregations'],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'queryName',
|
||||
'filters',
|
||||
'filter',
|
||||
'limit',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[PANEL_TYPES.VALUE]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'reduceTo',
|
||||
'having',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'having',
|
||||
'reduceTo',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'reduceTo',
|
||||
'having',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function handleQueryChange(
|
||||
newPanelType: keyof PartialPanelTypes,
|
||||
supersetQuery: Query,
|
||||
@@ -555,7 +112,7 @@ export function handleQueryChange(
|
||||
panelTypeDataSourceFormValuesMap[newPanelType][dataSource].builder
|
||||
.queryData;
|
||||
|
||||
fieldsToSelect.forEach((field: keyof IBuilderQuery) => {
|
||||
fieldsToSelect.forEach((field) => {
|
||||
set(tempQuery, field, supersetQuery.builder.queryData[index][field]);
|
||||
});
|
||||
|
||||
|
||||
140
frontend/src/lib/query/panelTypeDataSourceFormValuesMap.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Builder fields carried across a panel-type switch, per panel type and data source.
|
||||
* Each shape is cut from the widest one by omission.
|
||||
*/
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type PartialPanelTypes = {
|
||||
[PANEL_TYPES.BAR]: 'bar';
|
||||
[PANEL_TYPES.LIST]: 'list';
|
||||
[PANEL_TYPES.TABLE]: 'table';
|
||||
[PANEL_TYPES.TIME_SERIES]: 'graph';
|
||||
[PANEL_TYPES.AREA]: 'area';
|
||||
[PANEL_TYPES.VALUE]: 'value';
|
||||
[PANEL_TYPES.PIE]: 'pie';
|
||||
[PANEL_TYPES.HISTOGRAM]: 'histogram';
|
||||
};
|
||||
|
||||
export type BuilderField = keyof IBuilderQuery;
|
||||
|
||||
export type PanelTypeFormValues = {
|
||||
builder: { queryData: BuilderField[] };
|
||||
};
|
||||
|
||||
/** A field added to `IBuilderQuery` fails to compile here until answered either way. */
|
||||
const IS_CARRIED = {
|
||||
queryName: true,
|
||||
aggregateOperator: true,
|
||||
aggregateAttribute: true,
|
||||
aggregations: true,
|
||||
timeAggregation: true,
|
||||
spaceAggregation: true,
|
||||
functions: true,
|
||||
filter: true,
|
||||
filters: true,
|
||||
groupBy: true,
|
||||
expression: true,
|
||||
disabled: true,
|
||||
having: true,
|
||||
limit: true,
|
||||
stepInterval: true,
|
||||
orderBy: true,
|
||||
reduceTo: true,
|
||||
legend: true,
|
||||
// `dataSource` is appended by the provider; the rest drive surfaces this switch
|
||||
// does not reach.
|
||||
dataSource: false,
|
||||
temporality: false,
|
||||
pageSize: false,
|
||||
offset: false,
|
||||
selectColumns: false,
|
||||
source: false,
|
||||
builderQueryType: false,
|
||||
} satisfies Record<BuilderField, boolean>;
|
||||
|
||||
function omit(
|
||||
fields: readonly BuilderField[],
|
||||
...omitted: BuilderField[]
|
||||
): BuilderField[] {
|
||||
return fields.filter((field) => !omitted.includes(field));
|
||||
}
|
||||
|
||||
const METRICS_AGGREGATION: readonly BuilderField[] = [
|
||||
'timeAggregation',
|
||||
'spaceAggregation',
|
||||
];
|
||||
|
||||
const SCALAR_METRICS: readonly BuilderField[] = (
|
||||
Object.entries(IS_CARRIED) as [BuilderField, boolean][]
|
||||
)
|
||||
.filter(([, carried]) => carried)
|
||||
.map(([field]) => field);
|
||||
|
||||
// `reduceTo` is offered for metrics only, an asymmetry carried over from the old table.
|
||||
const SERIES_METRICS: readonly BuilderField[] = omit(
|
||||
SCALAR_METRICS,
|
||||
'reduceTo',
|
||||
);
|
||||
|
||||
const SERIES: readonly BuilderField[] = omit(
|
||||
SERIES_METRICS,
|
||||
...METRICS_AGGREGATION,
|
||||
);
|
||||
|
||||
const SINGLE_VALUE_METRICS: readonly BuilderField[] = omit(
|
||||
SCALAR_METRICS,
|
||||
'groupBy',
|
||||
'limit',
|
||||
'orderBy',
|
||||
);
|
||||
const SINGLE_VALUE: readonly BuilderField[] = omit(
|
||||
SINGLE_VALUE_METRICS,
|
||||
...METRICS_AGGREGATION,
|
||||
);
|
||||
|
||||
const RAW_ROWS: readonly BuilderField[] = omit(
|
||||
SERIES,
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'groupBy',
|
||||
'having',
|
||||
'stepInterval',
|
||||
'disabled',
|
||||
'legend',
|
||||
'expression',
|
||||
);
|
||||
const RAW_ROWS_METRICS: readonly BuilderField[] = omit(
|
||||
RAW_ROWS,
|
||||
'limit',
|
||||
'orderBy',
|
||||
'functions',
|
||||
);
|
||||
|
||||
/** Each cell gets its own copy; a shared instance would let cells contaminate
|
||||
* each other. */
|
||||
function bySource(
|
||||
logsAndTraces: readonly BuilderField[],
|
||||
metrics: readonly BuilderField[],
|
||||
): Record<DataSource, PanelTypeFormValues> {
|
||||
return {
|
||||
[DataSource.LOGS]: { builder: { queryData: [...logsAndTraces] } },
|
||||
[DataSource.TRACES]: { builder: { queryData: [...logsAndTraces] } },
|
||||
[DataSource.METRICS]: { builder: { queryData: [...metrics] } },
|
||||
};
|
||||
}
|
||||
|
||||
export const panelTypeDataSourceFormValuesMap: Record<
|
||||
keyof PartialPanelTypes,
|
||||
Record<DataSource, PanelTypeFormValues>
|
||||
> = {
|
||||
[PANEL_TYPES.TIME_SERIES]: bySource(SERIES, SERIES_METRICS),
|
||||
[PANEL_TYPES.AREA]: bySource(SERIES, SERIES_METRICS),
|
||||
[PANEL_TYPES.BAR]: bySource(SERIES, SERIES_METRICS),
|
||||
[PANEL_TYPES.HISTOGRAM]: bySource(SERIES, SERIES_METRICS),
|
||||
[PANEL_TYPES.TABLE]: bySource(SERIES, SCALAR_METRICS),
|
||||
[PANEL_TYPES.PIE]: bySource(SERIES, SCALAR_METRICS),
|
||||
[PANEL_TYPES.VALUE]: bySource(SINGLE_VALUE, SINGLE_VALUE_METRICS),
|
||||
[PANEL_TYPES.LIST]: bySource(RAW_ROWS, RAW_ROWS_METRICS),
|
||||
};
|
||||
@@ -13,7 +13,7 @@ describe('Get Series Data', () => {
|
||||
expect(seriesData).toHaveLength(5);
|
||||
expect(seriesData[1].label).toBe('firstLegend');
|
||||
expect(seriesData[1].show).toBe(true);
|
||||
expect(seriesData[1].fill).toBe('#FF6F91');
|
||||
expect(seriesData[1].fill).toBe('#83C2EB');
|
||||
expect(seriesData[1].width).toBe(2);
|
||||
});
|
||||
|
||||
|
||||
@@ -57,3 +57,12 @@
|
||||
color: var(--l3-foreground);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* The bottom legend's box is only the rows reserved for it. */
|
||||
.container:not(.isRight) .emptyState {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { LegendAction, LegendPosition, LegendProps } from '../types';
|
||||
import { LEGEND_ITEM_EXTRA_WIDTH, MAX_LEGEND_WIDTH } from './constants';
|
||||
import LegendRow from './LegendRow';
|
||||
import LegendToolbar from './LegendToolbar';
|
||||
import { filterLegendItems, getShownSeriesState } from './utils';
|
||||
import { getVisibleSeriesState } from './utils';
|
||||
|
||||
import styles from './Legend.module.scss';
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function Legend({
|
||||
items,
|
||||
position,
|
||||
averageLegendWidth = MAX_LEGEND_WIDTH,
|
||||
showSearch = false,
|
||||
focusedSeriesIndex,
|
||||
onAction,
|
||||
showCopy = true,
|
||||
@@ -30,27 +31,22 @@ export default function Legend({
|
||||
const itemWidth = averageLegendWidth + LEGEND_ITEM_EXTRA_WIDTH;
|
||||
const isRightPosition = position === LegendPosition.RIGHT;
|
||||
|
||||
const { visibleCount, soleShownSeriesIndex } = useMemo(
|
||||
() => getShownSeriesState(items),
|
||||
[items],
|
||||
);
|
||||
// The layout decides: it reserves the height.
|
||||
const showToolbar = showSearch && items.length > 0;
|
||||
|
||||
// A bottom legend gets two rows; spending one on chrome costs more chart than
|
||||
// the readout is worth.
|
||||
const showToolbar = isRightPosition && items.length > 0;
|
||||
const showFilter = showToolbar;
|
||||
const effectiveQuery = showToolbar ? filterQuery : '';
|
||||
|
||||
const effectiveQuery = showFilter ? filterQuery : '';
|
||||
|
||||
const visibleLegendItems = useMemo(
|
||||
() => filterLegendItems(items, effectiveQuery),
|
||||
const {
|
||||
listedItems,
|
||||
visibleCount,
|
||||
onlyVisibleSeriesIndex,
|
||||
areAllSeriesVisible,
|
||||
} = useMemo(
|
||||
() => getVisibleSeriesState(items, effectiveQuery),
|
||||
[items, effectiveQuery],
|
||||
);
|
||||
|
||||
const isEmptyState =
|
||||
!!effectiveQuery.trim() && visibleLegendItems.length === 0;
|
||||
|
||||
const isAllShown = visibleCount === items.length;
|
||||
const isEmptyState = !!effectiveQuery.trim() && listedItems.length === 0;
|
||||
|
||||
// A row that unmounts under the pointer never fires its own mouseleave.
|
||||
const handleMouseLeave = useCallback(
|
||||
@@ -63,14 +59,20 @@ export default function Legend({
|
||||
<LegendRow
|
||||
key={item.seriesIndex}
|
||||
item={item}
|
||||
isSoleShown={soleShownSeriesIndex === item.seriesIndex}
|
||||
isAllShown={isAllShown}
|
||||
isOneSeriesVisible={onlyVisibleSeriesIndex === item.seriesIndex}
|
||||
areAllSeriesVisible={areAllSeriesVisible}
|
||||
isFocused={focusedSeriesIndex === item.seriesIndex}
|
||||
showCopy={showCopy}
|
||||
onAction={onAction}
|
||||
/>
|
||||
),
|
||||
[soleShownSeriesIndex, isAllShown, focusedSeriesIndex, showCopy, onAction],
|
||||
[
|
||||
onlyVisibleSeriesIndex,
|
||||
areAllSeriesVisible,
|
||||
focusedSeriesIndex,
|
||||
showCopy,
|
||||
onAction,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -87,7 +89,7 @@ export default function Legend({
|
||||
<LegendToolbar
|
||||
visibleCount={visibleCount}
|
||||
totalCount={items.length}
|
||||
showFilter={showFilter}
|
||||
position={position}
|
||||
filterQuery={filterQuery}
|
||||
onFilterQueryChange={setFilterQuery}
|
||||
/>
|
||||
@@ -101,7 +103,7 @@ export default function Legend({
|
||||
className={styles.scroller}
|
||||
listClassName={styles.gridList}
|
||||
itemClassName={styles.gridItem}
|
||||
data={visibleLegendItems}
|
||||
data={listedItems}
|
||||
itemContent={(_, item): JSX.Element => renderLegendItem(item)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -13,9 +13,9 @@ import styles from './LegendRow.module.scss';
|
||||
export interface LegendRowProps {
|
||||
item: LegendItem;
|
||||
/** The only series currently shown, so hiding it is refused. */
|
||||
isSoleShown: boolean;
|
||||
isOneSeriesVisible: boolean;
|
||||
/** Nothing is hidden, so the row's action can only narrow the selection. */
|
||||
isAllShown: boolean;
|
||||
areAllSeriesVisible: boolean;
|
||||
isFocused: boolean;
|
||||
showCopy: boolean;
|
||||
onAction: OnLegendAction;
|
||||
@@ -29,15 +29,15 @@ export interface LegendRowProps {
|
||||
*/
|
||||
function LegendRow({
|
||||
item,
|
||||
isSoleShown,
|
||||
isAllShown,
|
||||
isOneSeriesVisible,
|
||||
areAllSeriesVisible,
|
||||
isFocused,
|
||||
showCopy,
|
||||
onAction,
|
||||
}: LegendRowProps): JSX.Element {
|
||||
const { seriesIndex, show } = item;
|
||||
const label = item.label ?? '';
|
||||
const isShowAllAction = show && !isAllShown;
|
||||
const isShowAllAction = show && !areAllSeriesVisible;
|
||||
const scopeActionLabel = isShowAllAction
|
||||
? 'Show all series'
|
||||
: 'Show only current series';
|
||||
@@ -47,15 +47,15 @@ function LegendRow({
|
||||
|
||||
/** Everything showing -> isolate; showing alone -> restore all. */
|
||||
const handleRowClick = useCallback((): void => {
|
||||
if (isSoleShown) {
|
||||
if (isOneSeriesVisible) {
|
||||
onAction({ type: LegendAction.SHOW_ALL });
|
||||
return;
|
||||
}
|
||||
onAction({
|
||||
type: isAllShown ? LegendAction.SHOW_ONLY : LegendAction.TOGGLE,
|
||||
type: areAllSeriesVisible ? LegendAction.SHOW_ONLY : LegendAction.TOGGLE,
|
||||
seriesIndex,
|
||||
});
|
||||
}, [isSoleShown, isAllShown, onAction, seriesIndex]);
|
||||
}, [isOneSeriesVisible, areAllSeriesVisible, onAction, seriesIndex]);
|
||||
|
||||
const handleMarkerClick = useCallback(
|
||||
(event: MouseEvent<HTMLButtonElement>): void => {
|
||||
@@ -126,7 +126,7 @@ function LegendRow({
|
||||
backgroundColor: show ? seriesColor : 'transparent',
|
||||
}}
|
||||
onClick={handleMarkerClick}
|
||||
disabled={isSoleShown}
|
||||
disabled={isOneSeriesVisible}
|
||||
aria-label={`${show ? 'Hide' : 'Show'} ${label}`}
|
||||
data-is-legend-marker={true}
|
||||
data-testid={`legend-marker-${seriesIndex}`}
|
||||
|
||||
@@ -33,3 +33,32 @@
|
||||
.searchIcon {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
/* Height + margin must match LEGEND_TOOLBAR_HEIGHT and LEGEND_TOOLBAR_GAP. */
|
||||
.inlineToolbar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
height: 24px;
|
||||
margin-bottom: var(--spacing-2);
|
||||
padding-right: var(--spacing-4);
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 0 0 auto;
|
||||
width: 240px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.statusInline {
|
||||
// Truncates rather than wrapping onto a row the legend has not reserved.
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.searchInputInline {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { ChangeEvent, useCallback } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Input } from 'antd';
|
||||
import { Search } from '@signozhq/icons';
|
||||
|
||||
import { LegendPosition } from '../types';
|
||||
|
||||
import styles from './LegendToolbar.module.scss';
|
||||
|
||||
export interface LegendToolbarProps {
|
||||
visibleCount: number;
|
||||
totalCount: number;
|
||||
/** Search is intrinsic to the right-positioned legend. */
|
||||
showFilter: boolean;
|
||||
/** Layout only: the column stacks, the bottom row does not. */
|
||||
position: LegendPosition;
|
||||
filterQuery: string;
|
||||
onFilterQueryChange: (query: string) => void;
|
||||
}
|
||||
@@ -17,7 +20,7 @@ export interface LegendToolbarProps {
|
||||
export default function LegendToolbar({
|
||||
visibleCount,
|
||||
totalCount,
|
||||
showFilter,
|
||||
position,
|
||||
filterQuery,
|
||||
onFilterQueryChange,
|
||||
}: LegendToolbarProps): JSX.Element {
|
||||
@@ -27,30 +30,48 @@ export default function LegendToolbar({
|
||||
[onFilterQueryChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showFilter && (
|
||||
const searchProps = {
|
||||
allowClear: true,
|
||||
prefix: <Search size={12} className={styles.searchIcon} />,
|
||||
placeholder: 'Search...',
|
||||
value: filterQuery,
|
||||
onChange: handleFilterChange,
|
||||
className: styles.searchInput,
|
||||
'data-testid': 'legend-search-input',
|
||||
};
|
||||
|
||||
const status = (
|
||||
<span
|
||||
className={cx(styles.status, {
|
||||
[styles.statusInline]: position !== LegendPosition.RIGHT,
|
||||
})}
|
||||
aria-live="polite"
|
||||
data-testid="legend-status"
|
||||
>
|
||||
{`Showing ${visibleCount} of ${totalCount} series`}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (position === LegendPosition.RIGHT) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.searchContainer}>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<Search size={12} className={styles.searchIcon} />}
|
||||
placeholder="Search..."
|
||||
value={filterQuery}
|
||||
onChange={handleFilterChange}
|
||||
className={styles.searchInput}
|
||||
data-testid="legend-search-input"
|
||||
/>
|
||||
<Input {...searchProps} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.toolbar}>
|
||||
<span
|
||||
className={styles.status}
|
||||
aria-live="polite"
|
||||
data-testid="legend-status"
|
||||
>
|
||||
{`Showing ${visibleCount} of ${totalCount} series`}
|
||||
</span>
|
||||
<div className={styles.toolbar}>{status}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.inlineToolbar}>
|
||||
<div className={styles.search}>
|
||||
<Input
|
||||
{...searchProps}
|
||||
className={cx(styles.searchInput, styles.searchInputInline)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
{status}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function UPlotLegend({
|
||||
position = LegendPosition.BOTTOM,
|
||||
config,
|
||||
averageLegendWidth,
|
||||
showSearch,
|
||||
}: UPlotLegendProps): JSX.Element {
|
||||
const { legendItemsMap, focusedSeriesIndex } = useLegendsSync({ config });
|
||||
const onAction = useLegendActions();
|
||||
@@ -27,6 +28,7 @@ export default function UPlotLegend({
|
||||
items={items}
|
||||
position={position}
|
||||
averageLegendWidth={averageLegendWidth}
|
||||
showSearch={showSearch}
|
||||
focusedSeriesIndex={focusedSeriesIndex}
|
||||
onAction={onAction}
|
||||
/>
|
||||
|
||||
@@ -88,11 +88,15 @@ describe('UPlotLegend', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderLegend = (position?: LegendPosition): RenderResult =>
|
||||
const renderLegend = (
|
||||
position?: LegendPosition,
|
||||
showSearch = true,
|
||||
): RenderResult =>
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<UPlotLegend
|
||||
position={position}
|
||||
showSearch={showSearch}
|
||||
// config is consumed by the mocked useLegendsSync hook, not directly
|
||||
config={{} as any}
|
||||
/>
|
||||
@@ -100,14 +104,38 @@ describe('UPlotLegend', () => {
|
||||
);
|
||||
|
||||
describe('layout and position', () => {
|
||||
it('renders the search input on a RIGHT legend', () => {
|
||||
renderLegend(LegendPosition.RIGHT);
|
||||
it.each([LegendPosition.RIGHT, LegendPosition.BOTTOM])(
|
||||
'gives the legend a search box and a readout (%s)',
|
||||
(position) => {
|
||||
renderLegend(position);
|
||||
|
||||
expect(screen.getByTestId('legend-search-input')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('legend-search-input')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('legend-status')).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps a BOTTOM legend bare — its two rows all go to series', () => {
|
||||
renderLegend();
|
||||
it.each([LegendPosition.RIGHT, LegendPosition.BOTTOM])(
|
||||
'counts down the readout as the search narrows the list (%s)',
|
||||
async (position) => {
|
||||
const user = userEvent.setup();
|
||||
renderLegend(position);
|
||||
|
||||
// B is hidden.
|
||||
expect(screen.getByTestId('legend-status')).toHaveTextContent(
|
||||
'Showing 2 of 3 series',
|
||||
);
|
||||
|
||||
await user.type(screen.getByTestId('legend-search-input'), 'a');
|
||||
|
||||
// Only A matches, counted against all three.
|
||||
expect(screen.getByTestId('legend-status')).toHaveTextContent(
|
||||
'Showing 1 of 3 series',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps a BOTTOM legend bare while every series is on screen', () => {
|
||||
renderLegend(LegendPosition.BOTTOM, false);
|
||||
|
||||
expect(screen.queryByTestId('legend-search-input')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('legend-status')).not.toBeInTheDocument();
|
||||
@@ -116,6 +144,16 @@ describe('UPlotLegend', () => {
|
||||
expect(screen.getByTestId('legend-scope-0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters a BOTTOM legend from its search box', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderLegend(LegendPosition.BOTTOM);
|
||||
|
||||
await user.type(screen.getByTestId('legend-search-input'), 'b');
|
||||
|
||||
expect(screen.getByText('B')).toBeInTheDocument();
|
||||
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the marker with the series colour, filled only when shown', () => {
|
||||
renderLegend(LegendPosition.RIGHT);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LegendItem } from 'lib/uPlotV2/config/types';
|
||||
|
||||
import { filterLegendItems, getShownSeriesState } from '../utils';
|
||||
import { filterLegendItems, getVisibleSeriesState } from '../utils';
|
||||
|
||||
const items = (shown: boolean[]): LegendItem[] =>
|
||||
shown.map((show, index) => ({
|
||||
@@ -10,26 +10,49 @@ const items = (shown: boolean[]): LegendItem[] =>
|
||||
show,
|
||||
}));
|
||||
|
||||
describe('getShownSeriesState', () => {
|
||||
describe('getVisibleSeriesState', () => {
|
||||
it('counts the shown series', () => {
|
||||
expect(getShownSeriesState(items([true, false, true]))).toStrictEqual({
|
||||
visibleCount: 2,
|
||||
soleShownSeriesIndex: null,
|
||||
});
|
||||
const state = getVisibleSeriesState(items([true, false, true]), '');
|
||||
|
||||
expect(state.visibleCount).toBe(2);
|
||||
expect(state.onlyVisibleSeriesIndex).toBeNull();
|
||||
expect(state.areAllSeriesVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('names the series when exactly one is shown', () => {
|
||||
expect(getShownSeriesState(items([false, true, false]))).toStrictEqual({
|
||||
visibleCount: 1,
|
||||
soleShownSeriesIndex: 2,
|
||||
});
|
||||
const state = getVisibleSeriesState(items([false, true, false]), '');
|
||||
|
||||
expect(state.visibleCount).toBe(1);
|
||||
expect(state.onlyVisibleSeriesIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('reports nothing shown', () => {
|
||||
expect(getShownSeriesState(items([false, false]))).toStrictEqual({
|
||||
visibleCount: 0,
|
||||
soleShownSeriesIndex: null,
|
||||
});
|
||||
const state = getVisibleSeriesState(items([false, false]), '');
|
||||
|
||||
expect(state.visibleCount).toBe(0);
|
||||
expect(state.onlyVisibleSeriesIndex).toBeNull();
|
||||
});
|
||||
|
||||
it('reports every series shown', () => {
|
||||
expect(
|
||||
getVisibleSeriesState(items([true, true]), '').areAllSeriesVisible,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('counts only the series the search listed', () => {
|
||||
const state = getVisibleSeriesState(items([true, true, false]), 'series-1');
|
||||
|
||||
expect(state.listedItems.map((item) => item.label)).toStrictEqual([
|
||||
'series-1',
|
||||
]);
|
||||
expect(state.visibleCount).toBe(1);
|
||||
});
|
||||
|
||||
it('reads isolation off every series, not the listed ones', () => {
|
||||
const state = getVisibleSeriesState(items([false, true, false]), 'series-2');
|
||||
|
||||
expect(state.listedItems).toHaveLength(1);
|
||||
expect(state.onlyVisibleSeriesIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -21,5 +21,9 @@ export const LEGEND_ROW_HEIGHT = 28;
|
||||
export const LEGEND_ROW_GAP = 2;
|
||||
export const LEGEND_MAX_BOTTOM_ROWS = 2;
|
||||
|
||||
/** Must match `.inlineToolbar`'s height and margin-bottom, or it eats a row. */
|
||||
export const LEGEND_TOOLBAR_HEIGHT = 24;
|
||||
export const LEGEND_TOOLBAR_GAP = 4;
|
||||
|
||||
/** Hover delay before a row's full-name tooltip opens. */
|
||||
export const LEGEND_TOOLTIP_DELAY_MS = 500;
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import { LegendItem } from 'lib/uPlotV2/config/types';
|
||||
|
||||
export interface ShownSeriesState {
|
||||
export interface LegendViewState {
|
||||
listedItems: LegendItem[];
|
||||
/** Listed items that are toggled on, against every series in the readout. */
|
||||
visibleCount: number;
|
||||
/** The series index when exactly one series is shown, else null. */
|
||||
soleShownSeriesIndex: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Driven by what is actually shown, never a remembered isolation: hiding series
|
||||
* one at a time down to a single one is the same state as "Only".
|
||||
*/
|
||||
export function getShownSeriesState(items: LegendItem[]): ShownSeriesState {
|
||||
const shown = items.filter((item) => item.show);
|
||||
|
||||
return {
|
||||
visibleCount: shown.length,
|
||||
soleShownSeriesIndex: shown.length === 1 ? shown[0].seriesIndex : null,
|
||||
};
|
||||
/** The series index when exactly one series is toggled on, else null. */
|
||||
onlyVisibleSeriesIndex: number | null;
|
||||
areAllSeriesVisible: boolean;
|
||||
}
|
||||
|
||||
export function filterLegendItems(
|
||||
@@ -32,3 +22,23 @@ export function filterLegendItems(
|
||||
item.label?.toLowerCase().includes(normalisedQuery),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Isolation is driven by what is actually shown, never a remembered one: hiding
|
||||
* series one at a time down to a single one is the same state as "Only". It is
|
||||
* read off the whole series set, not off what the search left listed.
|
||||
*/
|
||||
export function getVisibleSeriesState(
|
||||
items: LegendItem[],
|
||||
query: string,
|
||||
): LegendViewState {
|
||||
const visible = items.filter((item) => item.show);
|
||||
const listedItems = filterLegendItems(items, query);
|
||||
|
||||
return {
|
||||
listedItems,
|
||||
visibleCount: listedItems.filter((item) => item.show).length,
|
||||
onlyVisibleSeriesIndex: visible.length === 1 ? visible[0].seriesIndex : null,
|
||||
areAllSeriesVisible: visible.length === items.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@ function createTooltipContent(
|
||||
};
|
||||
}
|
||||
|
||||
function createUPlotInstance(cursorIdx: number | null): uPlot {
|
||||
function createUPlotInstance(cursorIdx: number | null, timestamp = 1): uPlot {
|
||||
return {
|
||||
data: [[1], []],
|
||||
data: [[timestamp], []],
|
||||
cursor: { idx: cursorIdx },
|
||||
// The rest of the uPlot fields are not used by Tooltip
|
||||
} as unknown as uPlot;
|
||||
@@ -122,6 +122,19 @@ describe('Tooltip', () => {
|
||||
expect(screen.getByText(expectedTitle)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('drops the date from the header title for a point on the current day', () => {
|
||||
const todayTimestamp = dayjs().tz('UTC').startOf('hour').unix();
|
||||
const uPlotInstance = createUPlotInstance(0, todayTimestamp);
|
||||
|
||||
renderTooltip({ uPlotInstance });
|
||||
|
||||
const expectedTitle = dayjs(todayTimestamp * 1000)
|
||||
.tz('UTC')
|
||||
.format(DATE_TIME_FORMATS.TIME_SECONDS);
|
||||
|
||||
expect(screen.getByText(expectedTitle)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render header when showTooltipHeader is false', () => {
|
||||
const uPlotInstance = createUPlotInstance(0);
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useMemo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import type { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { Pin } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import type uPlot from 'uplot';
|
||||
import { formatTimestampOmittingTodaysDate } from 'utils/timeUtils';
|
||||
|
||||
import { TooltipContentItem } from '../../../types';
|
||||
import TooltipItem from '../TooltipItem/TooltipItem';
|
||||
@@ -19,6 +18,7 @@ interface TooltipHeaderProps {
|
||||
isPinned: boolean;
|
||||
activeItem: TooltipContentItem | null;
|
||||
headerRowClassName?: string;
|
||||
/** Overrides the default, which drops the date part for points on the current day. */
|
||||
dateFormat?: string;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function TooltipHeader({
|
||||
isPinned,
|
||||
activeItem,
|
||||
headerRowClassName,
|
||||
dateFormat = DATE_TIME_FORMATS.MONTH_DATETIME_SECONDS,
|
||||
dateFormat,
|
||||
}: TooltipHeaderProps): JSX.Element {
|
||||
const { timezone: userTimezone } = useTimezone();
|
||||
const resolvedTimezone = timezone?.value ?? userTimezone.value;
|
||||
@@ -46,9 +46,11 @@ export default function TooltipHeader({
|
||||
if (timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
return dayjs(timestamp * 1000)
|
||||
.tz(resolvedTimezone)
|
||||
.format(dateFormat);
|
||||
return formatTimestampOmittingTodaysDate(
|
||||
timestamp * 1000,
|
||||
resolvedTimezone,
|
||||
dateFormat,
|
||||
);
|
||||
}, [
|
||||
resolvedTimezone,
|
||||
uPlotInstance.data,
|
||||
|
||||
@@ -145,6 +145,8 @@ export interface LegendProps {
|
||||
/** Legend placement; always supplied by the container. */
|
||||
position: LegendPosition;
|
||||
averageLegendWidth?: number;
|
||||
/** Set by the chart layout, which reserves the height for it. */
|
||||
showSearch?: boolean;
|
||||
/** Series index highlighted by the chart cursor. */
|
||||
focusedSeriesIndex: number | null;
|
||||
onAction: OnLegendAction;
|
||||
@@ -158,6 +160,7 @@ export interface UPlotLegendProps {
|
||||
position?: LegendPosition;
|
||||
config: UPlotConfigBuilder;
|
||||
averageLegendWidth?: number;
|
||||
showSearch?: boolean;
|
||||
}
|
||||
|
||||
export interface TooltipContentItem {
|
||||
|
||||
@@ -265,7 +265,7 @@ function getPathBuilder({
|
||||
drawStyle,
|
||||
lineInterpolation,
|
||||
barAlignment = BarAlignment.Center,
|
||||
barWidthFactor = 0.6,
|
||||
barWidthFactor = 0.85,
|
||||
barMaxWidth = 200,
|
||||
stepInterval,
|
||||
}: {
|
||||
|
||||
@@ -297,7 +297,7 @@ describe('UPlotSeriesBuilder', () => {
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
expect(config.stroke).toBe('#E64A3C');
|
||||
expect(config.stroke).toBe('#AD42E0');
|
||||
});
|
||||
|
||||
it('passes through pointsFilter when provided', () => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import ChartLayout from 'lib/visualization/layout/ChartLayout/ChartLayout';
|
||||
import ChartLayout, {
|
||||
LegendLayout,
|
||||
} from 'lib/visualization/layout/ChartLayout/ChartLayout';
|
||||
import UPlotLegend from 'lib/uPlotV2/components/Legend/UPlotLegend';
|
||||
import {
|
||||
LegendPosition,
|
||||
@@ -58,7 +60,7 @@ export default function ChartWrapper({
|
||||
);
|
||||
|
||||
const legendComponent = useCallback(
|
||||
(averageLegendWidth: number): React.ReactNode => {
|
||||
({ averageLegendWidth, showSearch }: LegendLayout): React.ReactNode => {
|
||||
if (!showLegend) {
|
||||
return null;
|
||||
}
|
||||
@@ -67,6 +69,7 @@ export default function ChartWrapper({
|
||||
config={config}
|
||||
position={legendConfig.position}
|
||||
averageLegendWidth={averageLegendWidth}
|
||||
showSearch={showSearch}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -68,17 +68,23 @@ export default function Pie({
|
||||
|
||||
// Reuse the uPlot chart/legend split so the donut + legend get the same area
|
||||
// allocation (right column, or up-to-two bottom rows) as every other panel.
|
||||
const { width, height, legendWidth, legendHeight, averageLegendWidth } =
|
||||
useMemo(
|
||||
() =>
|
||||
calculateChartDimensions({
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
legendConfig: { position },
|
||||
seriesLabels: data.map((slice) => slice.label),
|
||||
}),
|
||||
[containerWidth, containerHeight, position, data],
|
||||
);
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
legendWidth,
|
||||
legendHeight,
|
||||
averageLegendWidth,
|
||||
showLegendSearch,
|
||||
} = useMemo(
|
||||
() =>
|
||||
calculateChartDimensions({
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
legendConfig: { position },
|
||||
seriesLabels: data.map((slice) => slice.label),
|
||||
}),
|
||||
[containerWidth, containerHeight, position, data],
|
||||
);
|
||||
|
||||
// Donut geometry derived from the allocated chart box, sized to leave room
|
||||
// for the external leader labels (see getDonutGeometry).
|
||||
@@ -224,6 +230,7 @@ export default function Pie({
|
||||
items={legendItems}
|
||||
position={position}
|
||||
averageLegendWidth={averageLegendWidth}
|
||||
showSearch={showLegendSearch}
|
||||
focusedSeriesIndex={focusedSeriesIndex}
|
||||
onAction={onLegendAction}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('calculateChartDimensions', () => {
|
||||
legendWidth: 0,
|
||||
legendHeight: 0,
|
||||
averageLegendWidth: 0,
|
||||
showLegendSearch: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,10 +107,10 @@ describe('calculateChartDimensions', () => {
|
||||
legendConfig: { position: LegendPosition.BOTTOM },
|
||||
seriesLabels: labels(40),
|
||||
});
|
||||
// Two 28px rows + the 2px row gap + 12px bottom padding — no room for a
|
||||
// clipped third row, and none left over.
|
||||
expect(dims.legendHeight).toBe(70);
|
||||
expect(dims.height).toBe(430);
|
||||
// Two 28px rows + 2px gap + 12px padding, plus the 24px search row + 4px.
|
||||
expect(dims.showLegendSearch).toBe(true);
|
||||
expect(dims.legendHeight).toBe(98);
|
||||
expect(dims.height).toBe(402);
|
||||
});
|
||||
|
||||
it('BOTTOM: items one past a row still reserve two rows', () => {
|
||||
@@ -123,6 +124,50 @@ describe('calculateChartDimensions', () => {
|
||||
expect(dims.legendHeight).toBe(70);
|
||||
});
|
||||
|
||||
it('BOTTOM: no search row while every item is already on screen', () => {
|
||||
// 1000px fits 4 per row, so 8 items fill both reserved rows exactly.
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 1000,
|
||||
containerHeight: 500,
|
||||
legendConfig: { position: LegendPosition.BOTTOM },
|
||||
seriesLabels: labels(8),
|
||||
});
|
||||
expect(dims.showLegendSearch).toBe(false);
|
||||
expect(dims.legendHeight).toBe(70);
|
||||
});
|
||||
|
||||
it('BOTTOM: a search row once the grid overflows the reserved rows', () => {
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 1000,
|
||||
containerHeight: 500,
|
||||
legendConfig: { position: LegendPosition.BOTTOM },
|
||||
seriesLabels: labels(9),
|
||||
});
|
||||
expect(dims.showLegendSearch).toBe(true);
|
||||
expect(dims.legendHeight).toBe(98);
|
||||
});
|
||||
|
||||
it('BOTTOM: no search row when it would push the legend past a short panel', () => {
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 1000,
|
||||
containerHeight: 120,
|
||||
legendConfig: { position: LegendPosition.BOTTOM },
|
||||
seriesLabels: labels(40),
|
||||
});
|
||||
expect(dims.showLegendSearch).toBe(false);
|
||||
expect(dims.legendHeight).toBe(40);
|
||||
});
|
||||
|
||||
it('RIGHT: always carries its chrome; the column has the height for it', () => {
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 1000,
|
||||
containerHeight: 500,
|
||||
legendConfig: { position: LegendPosition.RIGHT },
|
||||
seriesLabels: labels(40),
|
||||
});
|
||||
expect(dims.showLegendSearch).toBe(true);
|
||||
});
|
||||
|
||||
it('BOTTOM: reserves the rows the grid actually lays out, not the rows a bare width estimate allows', () => {
|
||||
// The item width alone suggests three fit on one row; the grid's per-item
|
||||
// padding and column gap leave room for two.
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
LEGEND_MAX_BOTTOM_ROWS,
|
||||
MIN_LEGEND_ITEM_WIDTH,
|
||||
LEGEND_COLUMN_GAP,
|
||||
LEGEND_TOOLBAR_GAP,
|
||||
LEGEND_TOOLBAR_HEIGHT,
|
||||
LEGEND_ITEM_EXTRA_WIDTH,
|
||||
LEGEND_ROW_GAP,
|
||||
LEGEND_ROW_HEIGHT,
|
||||
@@ -15,6 +17,8 @@ export interface ChartDimensions {
|
||||
legendWidth: number;
|
||||
legendHeight: number;
|
||||
averageLegendWidth: number;
|
||||
/** For a BOTTOM legend that row's height is inside `legendHeight`. */
|
||||
showLegendSearch: boolean;
|
||||
}
|
||||
|
||||
const AVG_CHAR_WIDTH = 8;
|
||||
@@ -76,6 +80,8 @@ export function calculateAverageLegendWidth(legends: string[]): number {
|
||||
* - `legendHeight` is exactly those rows plus the wrapper's bottom padding, so
|
||||
* the rectangle never clips a row or reserves space for half of one. Two
|
||||
* rows that would take half a short panel fall back to one row.
|
||||
* - A grid overflowing those rows also gets a search row, whose height is
|
||||
* part of `legendHeight`.
|
||||
* - Chart height is `containerHeight - legendHeight`, never below 0.
|
||||
* - `legendsPerSet` is the number of legend items that fit horizontally, based on the same text-width approximation.
|
||||
*
|
||||
@@ -101,6 +107,7 @@ export function calculateChartDimensions({
|
||||
legendWidth: 0,
|
||||
legendHeight: 0,
|
||||
averageLegendWidth: 0,
|
||||
showLegendSearch: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,6 +147,7 @@ export function calculateChartDimensions({
|
||||
legendHeight: containerHeight,
|
||||
// Single vertical list on the right.
|
||||
averageLegendWidth: rightLegendWidth,
|
||||
showLegendSearch: legendItemCount > 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,18 +166,25 @@ export function calculateChartDimensions({
|
||||
),
|
||||
);
|
||||
|
||||
// The wrapper's bottom padding is inside this height (border-box).
|
||||
const heightForRows = (rowCount: number): number =>
|
||||
// The wrapper's bottom padding and the search row are inside this height.
|
||||
const heightForRows = (rowCount: number, withToolbar: boolean): number =>
|
||||
rowCount * LEGEND_ROW_HEIGHT +
|
||||
(rowCount - 1) * LEGEND_ROW_GAP +
|
||||
LEGEND_PADDING;
|
||||
LEGEND_PADDING +
|
||||
(withToolbar ? LEGEND_TOOLBAR_HEIGHT + LEGEND_TOOLBAR_GAP : 0);
|
||||
|
||||
const shortPanelBudget = containerHeight * MAX_SHORT_PANEL_LEGEND_RATIO;
|
||||
const gridRowCount = Math.ceil(legendItemCount / legendItemsPerRow);
|
||||
|
||||
// Only once rows overflow — below that every series is already on screen —
|
||||
// and only while the row it costs leaves the legend inside the panel's share.
|
||||
const showLegendSearch =
|
||||
gridRowCount > LEGEND_MAX_BOTTOM_ROWS &&
|
||||
heightForRows(1, true) <= shortPanelBudget;
|
||||
|
||||
const neededRowCount = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
LEGEND_MAX_BOTTOM_ROWS,
|
||||
Math.ceil(legendItemCount / legendItemsPerRow),
|
||||
),
|
||||
Math.min(LEGEND_MAX_BOTTOM_ROWS, gridRowCount),
|
||||
);
|
||||
|
||||
// Without this, short grid panels hand most of their area to the legend and
|
||||
@@ -177,11 +192,11 @@ export function calculateChartDimensions({
|
||||
// row's items are clipped rather than removed, so they are scroll-only here.
|
||||
const legendRowCount =
|
||||
neededRowCount > 1 &&
|
||||
heightForRows(neededRowCount) > containerHeight * MAX_SHORT_PANEL_LEGEND_RATIO
|
||||
heightForRows(neededRowCount, showLegendSearch) > shortPanelBudget
|
||||
? 1
|
||||
: neededRowCount;
|
||||
|
||||
const bottomLegendHeight = heightForRows(legendRowCount);
|
||||
const bottomLegendHeight = heightForRows(legendRowCount, showLegendSearch);
|
||||
|
||||
return {
|
||||
width: containerWidth,
|
||||
@@ -189,5 +204,6 @@ export function calculateChartDimensions({
|
||||
legendWidth: containerWidth,
|
||||
legendHeight: bottomLegendHeight,
|
||||
averageLegendWidth: legendItemWidth,
|
||||
showLegendSearch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,9 +7,14 @@ import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
|
||||
import 'lib/visualization/layout/ChartLayout/ChartLayout.styles.scss';
|
||||
|
||||
export interface LegendLayout {
|
||||
averageLegendWidth: number;
|
||||
showSearch: boolean;
|
||||
}
|
||||
|
||||
export interface ChartLayoutProps {
|
||||
showLegend?: boolean;
|
||||
legendComponent: (legendPerSet: number) => React.ReactNode;
|
||||
legendComponent: (layout: LegendLayout) => React.ReactNode;
|
||||
children: (props: {
|
||||
chartWidth: number;
|
||||
chartHeight: number;
|
||||
@@ -40,6 +45,7 @@ export default function ChartLayout({
|
||||
legendWidth: 0,
|
||||
legendHeight: 0,
|
||||
averageLegendWidth: MAX_LEGEND_WIDTH,
|
||||
showLegendSearch: false,
|
||||
};
|
||||
}
|
||||
const legendItemsMap = config.getLegendItems();
|
||||
@@ -81,7 +87,10 @@ export default function ChartLayout({
|
||||
width: chartDimensions.legendWidth,
|
||||
}}
|
||||
>
|
||||
{legendComponent(chartDimensions.averageLegendWidth)}
|
||||
{legendComponent({
|
||||
averageLegendWidth: chartDimensions.averageLegendWidth,
|
||||
showSearch: chartDimensions.showLegendSearch,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
.all-errors-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
.all-errors-quick-filter-section {
|
||||
width: 0%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
.right-toolbar-actions-container {
|
||||
display: flex;
|
||||
@@ -18,14 +11,4 @@
|
||||
.ant-tabs {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
.all-errors-quick-filter-section {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ import { Filter } from '@signozhq/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import cx from 'classnames';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import ResourceAttributesFilterV2 from 'container/ResourceAttributeFilterV2/ResourceAttributesFilterV2';
|
||||
@@ -59,63 +57,52 @@ function AllErrors(): JSX.Element {
|
||||
const quickFilterFieldApis = useSignalFieldApis();
|
||||
|
||||
return (
|
||||
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
|
||||
{showFilters && (
|
||||
<section className={cx('all-errors-quick-filter-section')}>
|
||||
<QuickFilters
|
||||
className="qf-exceptions"
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
signal={SignalType.EXCEPTIONS}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={quickFilterFieldApis}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<section
|
||||
className={cx(
|
||||
'all-errors-right-section',
|
||||
showFilters ? 'filter-visible' : '',
|
||||
)}
|
||||
>
|
||||
<TypicalOverlayScrollbar>
|
||||
<>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
leftActions={
|
||||
!showFilters ? (
|
||||
<Tooltip title="Show Filters">
|
||||
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||
<Filter size="md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
rightActions={
|
||||
<div className="right-toolbar-actions-container">
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={handleRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<QuickFiltersLayout
|
||||
className="all-errors-page"
|
||||
contentClassName="all-errors-right-section"
|
||||
showFilters={showFilters}
|
||||
quickFilterProps={{
|
||||
className: 'qf-exceptions',
|
||||
source: QuickFiltersSource.EXCEPTIONS,
|
||||
signal: SignalType.EXCEPTIONS,
|
||||
handleFilterVisibilityChange,
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
leftActions={
|
||||
!showFilters ? (
|
||||
<Tooltip title="Show Filters">
|
||||
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||
<Filter size="md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
rightActions={
|
||||
<div className="right-toolbar-actions-container">
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={handleRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
<ResourceAttributesFilterV2 />
|
||||
<RouteTab
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
showRightSection={false}
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</>
|
||||
</TypicalOverlayScrollbar>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ResourceAttributesFilterV2 />
|
||||
<RouteTab
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
showRightSection={false}
|
||||
/>
|
||||
</QuickFiltersLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
.api-monitoring-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
.ant-tabs {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 0px;
|
||||
@@ -15,22 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -13,9 +13,12 @@ function ApiMonitoringPage(): JSX.Element {
|
||||
const routes: TabRoutes[] = [Explorer];
|
||||
|
||||
return (
|
||||
<div className="api-monitoring-page">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="api-monitoring-page"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,8 @@ import type {
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
handleQueryChange,
|
||||
type PartialPanelTypes,
|
||||
} from 'lib/query/panelQuery';
|
||||
import { handleQueryChange } from 'lib/query/panelQuery';
|
||||
import type { PartialPanelTypes } from 'lib/query/panelTypeDataSourceFormValuesMap';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
|
||||
import type {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
|
||||
// UI belongs in the bounded layout, not in another offset here.
|
||||
bottom: var(--bottom-strip-height, 0px);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
|
||||
@@ -67,6 +67,8 @@ export const NoPreviewData: Story = {
|
||||
/** The editor remains usable while the independently fetched preview has failed. */
|
||||
export const PreviewQueryError: Story = {
|
||||
args: { dataState: 'error' },
|
||||
// The mocked queries deliberately fail; the resulting console errors are the state under test.
|
||||
parameters: { allowConsoleErrors: true },
|
||||
};
|
||||
|
||||
/** A locked dashboard: the editor still opens, but it cannot save. */
|
||||
|
||||
@@ -91,6 +91,8 @@ export const NoData: Story = {
|
||||
/** A partial data failure: the dashboard remains visible while panel queries fail. */
|
||||
export const PanelQueryError: Story = {
|
||||
args: { dataState: 'error' },
|
||||
// The mocked queries deliberately fail; the resulting console errors are the state under test.
|
||||
parameters: { allowConsoleErrors: true },
|
||||
};
|
||||
|
||||
/** The panels mid-fetch, with the header, variable bar and grid already laid out. */
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
.infra-monitoring-module-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 8px;
|
||||
margin-bottom: 0px;
|
||||
@@ -17,22 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -13,8 +13,11 @@ export default function InfrastructureMonitoringPage(): JSX.Element {
|
||||
const routes: TabRoutes[] = [Hosts, Kubernetes];
|
||||
|
||||
return (
|
||||
<div className="infra-monitoring-module-container">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="infra-monitoring-module-container"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
.logs-module-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 0px;
|
||||
@@ -20,25 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -13,8 +13,11 @@ export default function LogsModulePage(): JSX.Element {
|
||||
const routes: TabRoutes[] = [logsExplorer, logsPipelines, logSaveView];
|
||||
|
||||
return (
|
||||
<div className="logs-module-container">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="logs-module-container"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
.messaging-queues-module-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 8px;
|
||||
margin-bottom: 0px;
|
||||
@@ -17,22 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -68,8 +68,11 @@ export default function MessagingQueuesMainPage(): JSX.Element {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="messaging-queues-module-container">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
<RouteTab
|
||||
className="messaging-queues-module-container"
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { screen, userEvent, within } from 'storybook/test';
|
||||
import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
|
||||
@@ -56,13 +56,18 @@ export const ConfigurationChecklist: Story = {
|
||||
|
||||
// The button reads plain "Configuration" while the checks are in flight and
|
||||
// ignores clicks until they land, so the count is what to wait on.
|
||||
await userEvent.click(
|
||||
await canvas.findByRole(
|
||||
'button',
|
||||
{ name: /missing configuration/i },
|
||||
untilLoaded,
|
||||
),
|
||||
const button = await canvas.findByRole(
|
||||
'button',
|
||||
{ name: /missing configuration/i },
|
||||
untilLoaded,
|
||||
);
|
||||
// The count shows once any one check lands; the button stays loading, and
|
||||
// ignores clicks, until all three have.
|
||||
await waitFor(
|
||||
() => expect(button).not.toHaveClass('ant-btn-loading'),
|
||||
untilLoaded,
|
||||
);
|
||||
await userEvent.click(button);
|
||||
// The modal header and its title both carry the text.
|
||||
await screen.findAllByText(
|
||||
'Kafka Service Attributes',
|
||||
|
||||