adonisjs-server-stats 1.11.6 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/core/explain-utils.d.ts +77 -0
- package/dist/core/index.d.ts +8 -2
- package/dist/core/index.js +1089 -877
- package/dist/core/queries-columns.d.ts +42 -0
- package/dist/core/queries-controller.d.ts +94 -0
- package/dist/core/query-utils.d.ts +49 -0
- package/dist/core/types-diagnostics.d.ts +2 -0
- package/dist/react/{CacheSection-BYN53kYO.js → CacheSection-DZFOiOYH.js} +1 -1
- package/dist/react/{CacheTab-DOhuK05d.js → CacheTab-D8js5o69.js} +1 -1
- package/dist/react/{ConfigSection-B9EHh4Rp.js → ConfigSection-DoJmvNkF.js} +1 -1
- package/dist/react/{ConfigTab-C8kriE2b.js → ConfigTab-CDvljuBF.js} +1 -1
- package/dist/react/{CustomPaneTab-CvzQS_Wh.js → CustomPaneTab-Db9eIQ-9.js} +1 -1
- package/dist/react/{EmailsSection-BJyFJf7A.js → EmailsSection-D7oxvAjA.js} +1 -1
- package/dist/react/{EmailsTab-Ch8jp10B.js → EmailsTab-BITuGTQm.js} +1 -1
- package/dist/react/{EventsSection-DJPwHeT8.js → EventsSection-7mDZv70I.js} +1 -1
- package/dist/react/{EventsTab-B-FoehXC.js → EventsTab-DF13BChE.js} +1 -1
- package/dist/react/InternalsContent-BNOnSoi9.js +393 -0
- package/dist/react/{InternalsSection-B6VlVx5f.js → InternalsSection-BwrTfpjA.js} +1 -1
- package/dist/react/{InternalsTab-CkEKpRMU.js → InternalsTab--RD-L1dX.js} +1 -1
- package/dist/react/{JobsSection-DWF4i1t_.js → JobsSection-B_VJFWy6.js} +1 -1
- package/dist/react/{JobsTab-DqnifQXV.js → JobsTab-DK8Fl9nz.js} +1 -1
- package/dist/react/{LogEntryRow-CMMkqA9M.js → LogEntryRow-Bdie9SFe.js} +1 -1
- package/dist/react/{LogsSection-C1xC5aP4.js → LogsSection-CrWqxWRr.js} +2 -2
- package/dist/react/{LogsTab-CS4sLfLw.js → LogsTab-DYSpL637.js} +2 -2
- package/dist/react/{OverviewSection-CxvfOR0v.js → OverviewSection-CuaccMZt.js} +1 -1
- package/dist/react/QueriesContent-BDIx8zFn.js +288 -0
- package/dist/react/QueriesSection-CvGGh5Ij.js +117 -0
- package/dist/react/QueriesTab-C7CCwhVc.js +87 -0
- package/dist/react/{RequestsSection-DETN9oZb.js → RequestsSection-mENyeNsk.js} +2 -2
- package/dist/react/{RoutesSection-CmorkJeC.js → RoutesSection-DjovlaNS.js} +1 -1
- package/dist/react/{RoutesTab-CbzBOzpc.js → RoutesTab-CbQECiUc.js} +1 -1
- package/dist/react/{SplitPaneWrapper-BiIgT4ND.js → SplitPaneWrapper-DyRyH5Re.js} +1 -1
- package/dist/react/{TimelineTab-Ue9tUD_n.js → TimelineTab-BKTFbsPe.js} +2 -2
- package/dist/react/components/Dashboard/shared/DataTable.d.ts +1 -1
- package/dist/react/components/shared/QueriesContent.d.ts +33 -0
- package/dist/react/{index-DwDK-4oX.js → index-CAUHa8QX.js} +2 -2
- package/dist/react/index.js +1 -1
- package/dist/react/style.css +1 -1
- package/dist/src/collectors/db_pool_collector.js +17 -23
- package/dist/src/dashboard/query_explain_handler.d.ts +11 -0
- package/dist/src/dashboard/query_explain_handler.js +64 -11
- package/dist/src/debug/query_collector.d.ts +1 -0
- package/dist/src/debug/query_collector.js +3 -0
- package/dist/src/edge/client/dashboard.js +2 -2
- package/dist/src/edge/client/debug-panel-deferred.js +1 -1
- package/dist/src/edge/client-vue/dashboard.js +4 -4
- package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
- package/dist/src/provider/diagnostics.d.ts +3 -0
- package/dist/src/provider/diagnostics.js +1 -0
- package/dist/src/provider/server_stats_provider.d.ts +7 -0
- package/dist/src/provider/server_stats_provider.js +38 -0
- package/dist/src/routes/debug_routes.d.ts +4 -0
- package/dist/src/routes/debug_routes.js +45 -0
- package/dist/src/routes/register_routes.d.ts +4 -0
- package/dist/src/routes/register_routes.js +2 -0
- package/dist/src/styles/components.css +254 -0
- package/dist/src/styles/dashboard.css +26 -185
- package/dist/src/styles/debug-panel.css +2 -24
- package/dist/vue/{CacheSection-DT2Mwf_s.js → CacheSection-Bx41lpfK.js} +1 -1
- package/dist/vue/{ConfigSection-BwKwS9lh.js → ConfigSection-fkfUdCmx.js} +1 -1
- package/dist/vue/{EmailsSection-B65g0FVS.js → EmailsSection-CbcDKF9Q.js} +1 -1
- package/dist/vue/{EventsSection-CxqtVF-o.js → EventsSection-CWbTYOBi.js} +1 -1
- package/dist/vue/{InternalsSection-BJUXE-5F.js → InternalsSection-jhTCuK3j.js} +149 -130
- package/dist/vue/{InternalsTab-DEMjqtlw.js → InternalsTab-521fxYYj.js} +152 -133
- package/dist/vue/{JobsSection-rMIyMb-g.js → JobsSection-CGGO6rtS.js} +1 -1
- package/dist/vue/{LogsSection-DmmZVJ7D.js → LogsSection-BPB-C-XA.js} +1 -1
- package/dist/vue/{OverviewSection-BMabyqw-.js → OverviewSection-BTcnPeoM.js} +1 -1
- package/dist/vue/QueriesSection-2SE3igQJ.js +411 -0
- package/dist/vue/QueriesTab-DO5XwJ--.js +112 -0
- package/dist/vue/{RequestsSection-CTu4jPZ_.js → RequestsSection-DXcG5Zok.js} +1 -1
- package/dist/vue/{RoutesSection-zQZDedL7.js → RoutesSection-BkNGWdoP.js} +1 -1
- package/dist/vue/components/Dashboard/sections/QueriesSection.vue.d.ts +1 -41
- package/dist/vue/{index-CM3yNVUR.js → index-CptF6Qbp.js} +2 -2
- package/dist/vue/index.js +1 -1
- package/dist/vue/style.css +1 -1
- package/package.json +1 -1
- package/dist/react/InternalsContent-O8ino9oM.js +0 -370
- package/dist/react/QueriesSection-CrMdU5Ax.js +0 -458
- package/dist/react/QueriesTab-x85PjkyS.js +0 -88
- package/dist/vue/QueriesSection-BfDFwGqH.js +0 -428
- package/dist/vue/QueriesTab-DuTG7cpC.js +0 -106
|
@@ -23,6 +23,8 @@ export interface DiagnosticsInput {
|
|
|
23
23
|
hasQueueCollector?: boolean;
|
|
24
24
|
resolvedCollectors?: CollectorRef[];
|
|
25
25
|
config: DiagnosticsConfig | null;
|
|
26
|
+
/** Lucid connections that have debug: true (empty = no query events emitted). */
|
|
27
|
+
lucidDebugConnections?: string[];
|
|
26
28
|
}
|
|
27
29
|
interface DiagnosticsConfig {
|
|
28
30
|
intervalMs: number;
|
|
@@ -130,5 +132,6 @@ export declare function buildDiagnostics(input: DiagnosticsInput): {
|
|
|
130
132
|
slowQueryThresholdMs: number;
|
|
131
133
|
enabled: boolean;
|
|
132
134
|
};
|
|
135
|
+
lucidDebugConnections: string[];
|
|
133
136
|
};
|
|
134
137
|
export {};
|
|
@@ -123,5 +123,6 @@ export function buildDiagnostics(input) {
|
|
|
123
123
|
integrations: buildIntegrationsDiagnostics(input),
|
|
124
124
|
config: buildConfigDiagnostics(input.config),
|
|
125
125
|
devToolbar: buildDevToolbarDiagnostics(input.config),
|
|
126
|
+
lucidDebugConnections: input.lucidDebugConnections ?? [],
|
|
126
127
|
};
|
|
127
128
|
}
|
|
@@ -31,6 +31,7 @@ export default class ServerStatsProvider {
|
|
|
31
31
|
transmitChannels: string[];
|
|
32
32
|
private resolvedConfig;
|
|
33
33
|
private resolvedCollectors;
|
|
34
|
+
private lucidDebugConnections;
|
|
34
35
|
/** Resolves when initStats completes (or rejects). Null if not yet scheduled. */
|
|
35
36
|
private initPromise;
|
|
36
37
|
constructor(app: ApplicationService);
|
|
@@ -49,6 +50,11 @@ export default class ServerStatsProvider {
|
|
|
49
50
|
private setupDevToolbar;
|
|
50
51
|
private setupLogBroadcast;
|
|
51
52
|
private resolve;
|
|
53
|
+
/**
|
|
54
|
+
* Check Lucid database connections for `debug: true` and warn if missing.
|
|
55
|
+
* Without it, Lucid won't emit `db:query` events and no queries will be captured.
|
|
56
|
+
*/
|
|
57
|
+
private checkLucidDebugFlag;
|
|
52
58
|
getDiagnostics(): {
|
|
53
59
|
timers: {
|
|
54
60
|
collectionInterval: {
|
|
@@ -124,6 +130,7 @@ export default class ServerStatsProvider {
|
|
|
124
130
|
slowQueryThresholdMs: number;
|
|
125
131
|
enabled: boolean;
|
|
126
132
|
};
|
|
133
|
+
lucidDebugConnections: string[];
|
|
127
134
|
};
|
|
128
135
|
shutdown(): Promise<void>;
|
|
129
136
|
}
|
|
@@ -33,6 +33,7 @@ export default class ServerStatsProvider {
|
|
|
33
33
|
transmitChannels = [];
|
|
34
34
|
resolvedConfig = null;
|
|
35
35
|
resolvedCollectors = [];
|
|
36
|
+
lucidDebugConnections = [];
|
|
36
37
|
/** Resolves when initStats completes (or rejects). Null if not yet scheduled. */
|
|
37
38
|
initPromise = null;
|
|
38
39
|
constructor(app) {
|
|
@@ -87,6 +88,8 @@ export default class ServerStatsProvider {
|
|
|
87
88
|
getStatsController: () => this.statsController,
|
|
88
89
|
getDebugController: () => this.debugController,
|
|
89
90
|
getDashboardController: () => this.dashboardController,
|
|
91
|
+
getDebugStore: () => this.debugStore,
|
|
92
|
+
getApp: () => this.app,
|
|
90
93
|
statsEndpoint,
|
|
91
94
|
debugEndpoint,
|
|
92
95
|
dashboardPath,
|
|
@@ -126,6 +129,7 @@ export default class ServerStatsProvider {
|
|
|
126
129
|
const SC = (await import('../controller/server_stats_controller.js')).default;
|
|
127
130
|
this.statsController = new SC(this.engine);
|
|
128
131
|
if (config.devToolbar?.enabled && !this.app.inProduction) {
|
|
132
|
+
this.checkLucidDebugFlag();
|
|
129
133
|
await this.setupDevToolbar(config);
|
|
130
134
|
}
|
|
131
135
|
const iv = await setupStatsIntervalHelper(this.engine, config, container);
|
|
@@ -193,6 +197,39 @@ export default class ServerStatsProvider {
|
|
|
193
197
|
return null;
|
|
194
198
|
}
|
|
195
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Check Lucid database connections for `debug: true` and warn if missing.
|
|
202
|
+
* Without it, Lucid won't emit `db:query` events and no queries will be captured.
|
|
203
|
+
*/
|
|
204
|
+
checkLucidDebugFlag() {
|
|
205
|
+
const dbConfig = this.app.config.get('database');
|
|
206
|
+
if (!dbConfig?.connections)
|
|
207
|
+
return;
|
|
208
|
+
const enabled = [];
|
|
209
|
+
const disabled = [];
|
|
210
|
+
for (const [name, conn] of Object.entries(dbConfig.connections)) {
|
|
211
|
+
if (conn?.debug)
|
|
212
|
+
enabled.push(name);
|
|
213
|
+
else
|
|
214
|
+
disabled.push(name);
|
|
215
|
+
}
|
|
216
|
+
this.lucidDebugConnections = enabled;
|
|
217
|
+
if (enabled.length === 0 && disabled.length > 0) {
|
|
218
|
+
log.block('query capture is disabled — no Lucid connections have debug: true', [
|
|
219
|
+
'',
|
|
220
|
+
dim('Lucid only emits db:query events when debug is enabled on a connection.'),
|
|
221
|
+
dim('Add this to your database connection in config/database.ts:'),
|
|
222
|
+
'',
|
|
223
|
+
` ${disabled[0]}: {`,
|
|
224
|
+
` client: '...',`,
|
|
225
|
+
` debug: true, ${dim('// ← enables query capture')}`,
|
|
226
|
+
` connection: { ... },`,
|
|
227
|
+
` }`,
|
|
228
|
+
'',
|
|
229
|
+
dim(`Connections without debug: ${disabled.join(', ')}`),
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
196
233
|
getDiagnostics() {
|
|
197
234
|
return buildDiagnostics({
|
|
198
235
|
intervalId: this.intervalId,
|
|
@@ -209,6 +246,7 @@ export default class ServerStatsProvider {
|
|
|
209
246
|
hasCacheCollector: this.resolvedCollectors.some((c) => c.name === 'redis'),
|
|
210
247
|
hasQueueCollector: this.resolvedCollectors.some((c) => c.name === 'queue'),
|
|
211
248
|
config: this.resolvedConfig,
|
|
249
|
+
lucidDebugConnections: this.lucidDebugConnections,
|
|
212
250
|
});
|
|
213
251
|
}
|
|
214
252
|
async shutdown() {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { ApiController } from '../controller/api_controller.js';
|
|
2
2
|
import type DebugController from '../controller/debug_controller.js';
|
|
3
|
+
import type { DebugStore } from '../debug/debug_store.js';
|
|
3
4
|
import type { AdonisRouter } from './router_types.js';
|
|
4
5
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
6
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
5
7
|
interface DebugRoutesOpts {
|
|
6
8
|
router: AdonisRouter;
|
|
7
9
|
debugEndpoint: string;
|
|
8
10
|
getDebugController: () => DebugController | null;
|
|
9
11
|
getApiController: () => ApiController | null;
|
|
12
|
+
getDebugStore?: () => DebugStore | null;
|
|
13
|
+
getApp?: () => ApplicationService | null;
|
|
10
14
|
middleware: Array<(ctx: HttpContext, next: () => Promise<void>) => Promise<void>>;
|
|
11
15
|
whenReady?: () => Promise<void>;
|
|
12
16
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAppDbClient, buildExplainSql, extractPlan } from '../dashboard/query_explain_handler.js';
|
|
1
2
|
let _whenReady;
|
|
2
3
|
function bindDebug(getController, method) {
|
|
3
4
|
return async (ctx) => {
|
|
@@ -89,6 +90,49 @@ function registerDebugTraceRoutes(router, getApiController) {
|
|
|
89
90
|
}))
|
|
90
91
|
.as('server-stats.debug.traceDetail');
|
|
91
92
|
}
|
|
93
|
+
function registerDebugExplainRoute(router, getDebugStore, getApp) {
|
|
94
|
+
if (!getDebugStore || !getApp)
|
|
95
|
+
return;
|
|
96
|
+
router
|
|
97
|
+
.get('/queries/:id/explain', async (ctx) => {
|
|
98
|
+
if (_whenReady)
|
|
99
|
+
await _whenReady();
|
|
100
|
+
const store = getDebugStore();
|
|
101
|
+
const app = getApp();
|
|
102
|
+
if (!store || !app) {
|
|
103
|
+
return ctx.response.serviceUnavailable({ error: 'Debug store not ready' });
|
|
104
|
+
}
|
|
105
|
+
const query = store.queries.getQueryById(Number(ctx.params.id));
|
|
106
|
+
if (!query)
|
|
107
|
+
return ctx.response.notFound({ error: 'Query not found' });
|
|
108
|
+
if (!query.sql.trim().toUpperCase().startsWith('SELECT')) {
|
|
109
|
+
return ctx.response.badRequest({ error: 'EXPLAIN is only supported for SELECT queries' });
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const appDb = await getAppDbClient(app, query.connection || undefined);
|
|
113
|
+
if (!appDb) {
|
|
114
|
+
return ctx.response.serviceUnavailable({ error: 'App database connection not available' });
|
|
115
|
+
}
|
|
116
|
+
const bindings = Array.isArray(query.bindings) ? query.bindings : [];
|
|
117
|
+
const explainSql = buildExplainSql(query.sql, appDb.dialect);
|
|
118
|
+
if (!explainSql) {
|
|
119
|
+
return ctx.response.badRequest({
|
|
120
|
+
error: `EXPLAIN is not supported for ${appDb.dialect} databases`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const explainResult = await appDb.raw(explainSql, bindings);
|
|
124
|
+
const plan = extractPlan(explainResult, appDb.dialect);
|
|
125
|
+
return ctx.response.json({ queryId: query.id, sql: query.sql, plan });
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return ctx.response.internalServerError({
|
|
129
|
+
error: 'EXPLAIN failed',
|
|
130
|
+
message: error?.message ?? 'Unknown error',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.as('server-stats.debug.queryExplain');
|
|
135
|
+
}
|
|
92
136
|
/** Register debug panel API routes. */
|
|
93
137
|
export function registerDebugRoutes(opts) {
|
|
94
138
|
const { router, debugEndpoint, getDebugController, getApiController, middleware } = opts;
|
|
@@ -99,6 +143,7 @@ export function registerDebugRoutes(opts) {
|
|
|
99
143
|
.group(() => {
|
|
100
144
|
registerDebugConfigRoutes(router, getDebugController);
|
|
101
145
|
registerDebugQueryAndEventRoutes(router, getApiController);
|
|
146
|
+
registerDebugExplainRoute(router, opts.getDebugStore, opts.getApp);
|
|
102
147
|
registerDebugLogRoutes(router, getApiController);
|
|
103
148
|
registerDebugEmailRoutes(router, getApiController);
|
|
104
149
|
registerDebugTraceRoutes(router, getApiController);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ApiController } from '../controller/api_controller.js';
|
|
2
2
|
import type DebugController from '../controller/debug_controller.js';
|
|
3
|
+
import type { DebugStore } from '../debug/debug_store.js';
|
|
3
4
|
import type ServerStatsController from '../controller/server_stats_controller.js';
|
|
4
5
|
import type DashboardController from '../dashboard/dashboard_controller.js';
|
|
5
6
|
import type { AdonisRouter } from './router_types.js';
|
|
6
7
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
8
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
7
9
|
/**
|
|
8
10
|
* Options for the unified route registration function.
|
|
9
11
|
*/
|
|
@@ -13,6 +15,8 @@ export interface RegisterRoutesOptions {
|
|
|
13
15
|
getStatsController: () => ServerStatsController | null;
|
|
14
16
|
getDebugController: () => DebugController | null;
|
|
15
17
|
getDashboardController: () => DashboardController | null;
|
|
18
|
+
getDebugStore?: () => DebugStore | null;
|
|
19
|
+
getApp?: () => ApplicationService | null;
|
|
16
20
|
statsEndpoint?: string | false;
|
|
17
21
|
debugEndpoint?: string;
|
|
18
22
|
dashboardPath?: string;
|
|
@@ -16,6 +16,8 @@ export function registerAllRoutes(options) {
|
|
|
16
16
|
debugEndpoint: options.debugEndpoint,
|
|
17
17
|
getDebugController: options.getDebugController,
|
|
18
18
|
getApiController: options.getApiController,
|
|
19
|
+
getDebugStore: options.getDebugStore,
|
|
20
|
+
getApp: options.getApp,
|
|
19
21
|
middleware,
|
|
20
22
|
whenReady: options.whenReady,
|
|
21
23
|
});
|
|
@@ -94,13 +94,29 @@
|
|
|
94
94
|
.ss-dash-empty,
|
|
95
95
|
.ss-dbg-empty {
|
|
96
96
|
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
97
98
|
align-items: center;
|
|
98
99
|
justify-content: center;
|
|
100
|
+
gap: 6px;
|
|
99
101
|
height: 100%;
|
|
100
102
|
min-height: 120px;
|
|
101
103
|
color: var(--ss-dim);
|
|
102
104
|
font-size: 12px;
|
|
103
105
|
}
|
|
106
|
+
.ss-empty-hint {
|
|
107
|
+
font-size: 11px;
|
|
108
|
+
color: var(--ss-dim);
|
|
109
|
+
opacity: 0.7;
|
|
110
|
+
max-width: 420px;
|
|
111
|
+
text-align: center;
|
|
112
|
+
line-height: 1.5;
|
|
113
|
+
}
|
|
114
|
+
.ss-empty-hint code {
|
|
115
|
+
background: var(--ss-surface, rgba(255,255,255,0.06));
|
|
116
|
+
padding: 1px 5px;
|
|
117
|
+
border-radius: 3px;
|
|
118
|
+
font-size: 10px;
|
|
119
|
+
}
|
|
104
120
|
|
|
105
121
|
/* ── Info cards ───────────────────────────────────────────────── */
|
|
106
122
|
.ss-info-cards,
|
|
@@ -435,6 +451,7 @@
|
|
|
435
451
|
background: var(--ss-surface);
|
|
436
452
|
border-bottom: 1px solid var(--ss-border);
|
|
437
453
|
white-space: nowrap;
|
|
454
|
+
z-index: 2;
|
|
438
455
|
}
|
|
439
456
|
.ss-table td,
|
|
440
457
|
.ss-dash-table td,
|
|
@@ -446,6 +463,7 @@
|
|
|
446
463
|
text-overflow: ellipsis;
|
|
447
464
|
white-space: nowrap;
|
|
448
465
|
color: var(--ss-text);
|
|
466
|
+
max-width: 0;
|
|
449
467
|
}
|
|
450
468
|
.ss-table tr:hover td,
|
|
451
469
|
.ss-dash-table tr:hover td,
|
|
@@ -517,6 +535,242 @@
|
|
|
517
535
|
background: var(--ss-red-bg);
|
|
518
536
|
color: var(--ss-red-fg);
|
|
519
537
|
}
|
|
538
|
+
/* SQL method variants (query tables) */
|
|
539
|
+
.ss-method-select,
|
|
540
|
+
.ss-dash-method-select,
|
|
541
|
+
.ss-dbg-method-select {
|
|
542
|
+
background: var(--ss-green-bg);
|
|
543
|
+
color: var(--ss-green-fg);
|
|
544
|
+
}
|
|
545
|
+
.ss-method-insert,
|
|
546
|
+
.ss-dash-method-insert,
|
|
547
|
+
.ss-dbg-method-insert {
|
|
548
|
+
background: var(--ss-blue-bg);
|
|
549
|
+
color: var(--ss-blue-fg);
|
|
550
|
+
}
|
|
551
|
+
.ss-method-update,
|
|
552
|
+
.ss-dash-method-update,
|
|
553
|
+
.ss-dbg-method-update {
|
|
554
|
+
background: var(--ss-amber-bg);
|
|
555
|
+
color: var(--ss-amber-fg);
|
|
556
|
+
}
|
|
557
|
+
.ss-method-head,
|
|
558
|
+
.ss-method-options,
|
|
559
|
+
.ss-method-raw,
|
|
560
|
+
.ss-dash-method-head,
|
|
561
|
+
.ss-dash-method-options,
|
|
562
|
+
.ss-dash-method-raw,
|
|
563
|
+
.ss-dbg-method-head,
|
|
564
|
+
.ss-dbg-method-options,
|
|
565
|
+
.ss-dbg-method-raw {
|
|
566
|
+
background: var(--ss-surface);
|
|
567
|
+
color: var(--ss-dim);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ── SQL cell text ───────────────────────────────────────────── */
|
|
571
|
+
.ss-sql,
|
|
572
|
+
.ss-dash-sql,
|
|
573
|
+
.ss-dbg-sql {
|
|
574
|
+
font-size: 11px;
|
|
575
|
+
color: var(--ss-sql-color);
|
|
576
|
+
word-break: break-all;
|
|
577
|
+
cursor: pointer;
|
|
578
|
+
max-width: 600px;
|
|
579
|
+
display: inline-block;
|
|
580
|
+
}
|
|
581
|
+
.ss-sql.ss-expanded,
|
|
582
|
+
.ss-dash-sql.ss-dash-expanded,
|
|
583
|
+
.ss-dbg-sql.ss-dbg-expanded {
|
|
584
|
+
white-space: pre-wrap;
|
|
585
|
+
max-width: none;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/* ── Duplicate query badge ───────────────────────────────────── */
|
|
589
|
+
.ss-dup,
|
|
590
|
+
.ss-dash-dup,
|
|
591
|
+
.ss-dbg-dup {
|
|
592
|
+
color: var(--ss-purple-fg);
|
|
593
|
+
font-size: 10px;
|
|
594
|
+
font-weight: 600;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* ── Scrollable table container ──────────────────────────────── */
|
|
598
|
+
.ss-table-wrap,
|
|
599
|
+
.ss-dash-table-wrap,
|
|
600
|
+
.ss-dbg-table-wrap {
|
|
601
|
+
flex: 1;
|
|
602
|
+
overflow-x: auto;
|
|
603
|
+
overflow-y: visible;
|
|
604
|
+
margin: 0 -2px;
|
|
605
|
+
padding: 0 2px;
|
|
606
|
+
scrollbar-width: thin;
|
|
607
|
+
scrollbar-color: var(--ss-border) transparent;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* ── Sortable column headers ─────────────────────────────────── */
|
|
611
|
+
.ss-sortable,
|
|
612
|
+
.ss-dash-sortable,
|
|
613
|
+
.ss-dbg-sortable {
|
|
614
|
+
cursor: pointer;
|
|
615
|
+
user-select: none;
|
|
616
|
+
}
|
|
617
|
+
.ss-sortable:hover,
|
|
618
|
+
.ss-dash-sortable:hover,
|
|
619
|
+
.ss-dbg-sortable:hover {
|
|
620
|
+
color: var(--ss-accent);
|
|
621
|
+
}
|
|
622
|
+
.ss-sort-arrow,
|
|
623
|
+
.ss-dash-sort-arrow,
|
|
624
|
+
.ss-dbg-sort-arrow {
|
|
625
|
+
font-size: 10px;
|
|
626
|
+
color: var(--ss-accent);
|
|
627
|
+
margin-left: 2px;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* ── Clickable table rows ────────────────────────────────────── */
|
|
631
|
+
.ss-table tr.ss-clickable,
|
|
632
|
+
.ss-dash-table tr.ss-dash-clickable,
|
|
633
|
+
.ss-dbg-table tr.ss-dbg-clickable {
|
|
634
|
+
cursor: pointer;
|
|
635
|
+
}
|
|
636
|
+
.ss-table tr.ss-clickable:hover td,
|
|
637
|
+
.ss-dash-table tr.ss-dash-clickable:hover td,
|
|
638
|
+
.ss-dbg-table tr.ss-dbg-clickable:hover td {
|
|
639
|
+
background: var(--ss-hover-accent);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* ── Event time ──────────────────────────────────────────────── */
|
|
643
|
+
.ss-event-time,
|
|
644
|
+
.ss-dash-event-time,
|
|
645
|
+
.ss-dbg-event-time {
|
|
646
|
+
color: var(--ss-dim);
|
|
647
|
+
font-size: 10px;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* ── EXPLAIN panel ───────────────────────────────────────────── */
|
|
651
|
+
.ss-explain-row td,
|
|
652
|
+
.ss-dash-explain-row td,
|
|
653
|
+
.ss-dbg-explain-row td {
|
|
654
|
+
white-space: normal;
|
|
655
|
+
overflow: visible;
|
|
656
|
+
max-width: none;
|
|
657
|
+
}
|
|
658
|
+
.ss-table tr.ss-explain-row:hover td,
|
|
659
|
+
.ss-dash-table tr.ss-dash-explain-row:hover td,
|
|
660
|
+
.ss-dbg-table tr.ss-dbg-explain-row:hover td {
|
|
661
|
+
background: var(--ss-surface-alt);
|
|
662
|
+
}
|
|
663
|
+
.ss-explain,
|
|
664
|
+
.ss-dash-explain,
|
|
665
|
+
.ss-dbg-explain {
|
|
666
|
+
padding: 12px 16px;
|
|
667
|
+
background: var(--ss-surface-alt);
|
|
668
|
+
border-top: 1px solid var(--ss-border-dim);
|
|
669
|
+
font-size: 11px;
|
|
670
|
+
overflow: hidden;
|
|
671
|
+
word-break: break-word;
|
|
672
|
+
}
|
|
673
|
+
.ss-explain-btn,
|
|
674
|
+
.ss-dash-explain-btn,
|
|
675
|
+
.ss-dbg-explain-btn {
|
|
676
|
+
padding: 2px 8px;
|
|
677
|
+
font-size: 9px;
|
|
678
|
+
color: var(--ss-purple-fg);
|
|
679
|
+
background: var(--ss-purple-bg);
|
|
680
|
+
border: none;
|
|
681
|
+
border-radius: 3px;
|
|
682
|
+
cursor: pointer;
|
|
683
|
+
text-transform: uppercase;
|
|
684
|
+
letter-spacing: 0.04em;
|
|
685
|
+
font-weight: 600;
|
|
686
|
+
}
|
|
687
|
+
.ss-explain-btn:hover,
|
|
688
|
+
.ss-dash-explain-btn:hover,
|
|
689
|
+
.ss-dbg-explain-btn:hover {
|
|
690
|
+
opacity: 0.8;
|
|
691
|
+
}
|
|
692
|
+
.ss-explain-btn-active,
|
|
693
|
+
.ss-dash-explain-btn-active,
|
|
694
|
+
.ss-dbg-explain-btn-active {
|
|
695
|
+
background: var(--ss-purple-fg);
|
|
696
|
+
color: #fff;
|
|
697
|
+
}
|
|
698
|
+
.ss-explain-result,
|
|
699
|
+
.ss-dash-explain-result,
|
|
700
|
+
.ss-dbg-explain-result {
|
|
701
|
+
margin-top: 6px;
|
|
702
|
+
font-size: 11px;
|
|
703
|
+
color: var(--ss-text-secondary);
|
|
704
|
+
}
|
|
705
|
+
.ss-explain-error,
|
|
706
|
+
.ss-dash-explain-error,
|
|
707
|
+
.ss-dbg-explain-error {
|
|
708
|
+
color: var(--ss-red-fg);
|
|
709
|
+
padding: 8px;
|
|
710
|
+
background: var(--ss-red-bg);
|
|
711
|
+
border-radius: 4px;
|
|
712
|
+
}
|
|
713
|
+
.ss-explain-node,
|
|
714
|
+
.ss-dash-explain-node,
|
|
715
|
+
.ss-dbg-explain-node {
|
|
716
|
+
padding: 4px 0;
|
|
717
|
+
border-left: 2px solid var(--ss-border-dim);
|
|
718
|
+
padding-left: 10px;
|
|
719
|
+
margin-top: 2px;
|
|
720
|
+
}
|
|
721
|
+
.ss-explain-node:first-child,
|
|
722
|
+
.ss-dash-explain-node:first-child,
|
|
723
|
+
.ss-dbg-explain-node:first-child {
|
|
724
|
+
border-left: 2px solid var(--ss-purple-fg);
|
|
725
|
+
}
|
|
726
|
+
.ss-explain-node-header,
|
|
727
|
+
.ss-dash-explain-node-header,
|
|
728
|
+
.ss-dbg-explain-node-header {
|
|
729
|
+
font-weight: 600;
|
|
730
|
+
color: var(--ss-text-primary);
|
|
731
|
+
font-size: 11px;
|
|
732
|
+
}
|
|
733
|
+
.ss-explain-node-type,
|
|
734
|
+
.ss-dash-explain-node-type,
|
|
735
|
+
.ss-dbg-explain-node-type {
|
|
736
|
+
color: var(--ss-purple-fg);
|
|
737
|
+
font-weight: 700;
|
|
738
|
+
}
|
|
739
|
+
.ss-explain-metrics,
|
|
740
|
+
.ss-dash-explain-metrics,
|
|
741
|
+
.ss-dbg-explain-metrics {
|
|
742
|
+
font-size: 10px;
|
|
743
|
+
color: var(--ss-dim);
|
|
744
|
+
margin-top: 1px;
|
|
745
|
+
font-family: var(--ss-font-mono);
|
|
746
|
+
word-break: break-all;
|
|
747
|
+
overflow-wrap: break-word;
|
|
748
|
+
}
|
|
749
|
+
.ss-explain-result table,
|
|
750
|
+
.ss-dash-explain-result table,
|
|
751
|
+
.ss-dbg-explain-result table {
|
|
752
|
+
width: 100%;
|
|
753
|
+
border-collapse: collapse;
|
|
754
|
+
margin-top: 4px;
|
|
755
|
+
}
|
|
756
|
+
.ss-explain-result th,
|
|
757
|
+
.ss-dash-explain-result th,
|
|
758
|
+
.ss-dbg-explain-result th {
|
|
759
|
+
padding: 3px 8px;
|
|
760
|
+
text-align: left;
|
|
761
|
+
font-weight: 600;
|
|
762
|
+
color: var(--ss-muted);
|
|
763
|
+
border-bottom: 1px solid var(--ss-border);
|
|
764
|
+
font-size: 9px;
|
|
765
|
+
text-transform: uppercase;
|
|
766
|
+
}
|
|
767
|
+
.ss-explain-result td,
|
|
768
|
+
.ss-dash-explain-result td,
|
|
769
|
+
.ss-dbg-explain-result td {
|
|
770
|
+
padding: 3px 8px;
|
|
771
|
+
border-bottom: 1px solid var(--ss-border-faint);
|
|
772
|
+
color: var(--ss-text-secondary);
|
|
773
|
+
}
|
|
520
774
|
|
|
521
775
|
/* ── 3. Log system ───────────────────────────────────────────── */
|
|
522
776
|
/* Log entry row: dashboard uses 16px horizontal padding, debug uses 12px.
|