Skip to content

Commit 3321b6a

Browse files
committed
PromQL: Single value
1 parent fb1e64c commit 3321b6a

4 files changed

Lines changed: 300 additions & 5 deletions

File tree

src/components/GeneralPage.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import ImportAction from './ImportAction';
6464
import NewChatModal from './NewChatModal';
6565
import Prompt from './Prompt';
6666
import ReadinessAlert from './ReadinessAlert';
67+
import PromQLValue from './PromQLValue';
6768
import QueryBrowserGraph from './QueryBrowserGraph';
6869
import ResponseTools from './ResponseTools';
6970
import WelcomeNotice from './WelcomeNotice';
@@ -101,7 +102,12 @@ type CodeProps = {
101102
};
102103

103104
const Code: React.FC<CodeProps> = ({ children, className }) => {
104-
const isPromQL = className?.includes('language-promql');
105+
const isPromQLInstant = className?.includes('language-promql-instant');
106+
const isPromQL = className?.includes('language-promql') && !isPromQLInstant;
107+
108+
if (isPromQLInstant && children) {
109+
return <PromQLValue query={String(children).trim()} />;
110+
}
105111

106112
if (isPromQL && children) {
107113
return <QueryBrowserGraph query={String(children).trim()} />;

src/components/PromQLValue.tsx

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import * as React from 'react';
2+
import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk';
3+
import { Spinner, TextArea } from '@patternfly/react-core';
4+
import { debounce } from 'lodash';
5+
6+
const PROMETHEUS_BASE_PATH = '/api/prometheus';
7+
const PROMETHEUS_TENANCY_BASE_PATH = '/api/prometheus-tenancy';
8+
const POLL_INTERVAL = 15 * 1000;
9+
10+
type PrometheusResponse = {
11+
status: string;
12+
data: {
13+
resultType: 'vector' | 'scalar' | 'matrix' | 'string';
14+
result: Array<{
15+
metric: Record<string, string>;
16+
value: [number, string];
17+
}>;
18+
};
19+
};
20+
21+
const formatValue = (value: string): string => {
22+
const num = parseFloat(value);
23+
if (isNaN(num)) {
24+
return value;
25+
}
26+
// Format large numbers with appropriate units
27+
if (Math.abs(num) >= 1e9) {
28+
return `${(num / 1e9).toFixed(2)}B`;
29+
}
30+
if (Math.abs(num) >= 1e6) {
31+
return `${(num / 1e6).toFixed(2)}M`;
32+
}
33+
if (Math.abs(num) >= 1e3) {
34+
return `${(num / 1e3).toFixed(2)}K`;
35+
}
36+
// Format decimals nicely
37+
if (num % 1 !== 0) {
38+
return num.toFixed(4).replace(/\.?0+$/, '');
39+
}
40+
return num.toString();
41+
};
42+
43+
const PromQLValue: React.FC<{ query: string }> = ({ query: initialQuery }) => {
44+
const [inputValue, setInputValue] = React.useState('');
45+
const [query, setQuery] = React.useState('');
46+
const [loading, setLoading] = React.useState(true);
47+
const [error, setError] = React.useState<string | null>(null);
48+
const [results, setResults] = React.useState<
49+
Array<{ metric: Record<string, string>; value: string }>
50+
>([]);
51+
const [updateKey, setUpdateKey] = React.useState(0);
52+
53+
const debouncedSetQuery = React.useMemo(
54+
() =>
55+
debounce((value: string) => {
56+
setQuery(value);
57+
}, 500),
58+
[],
59+
);
60+
61+
const effectiveQuery = inputValue || initialQuery;
62+
63+
React.useEffect(() => {
64+
debouncedSetQuery(effectiveQuery);
65+
}, [debouncedSetQuery, effectiveQuery]);
66+
67+
const fetchData = React.useCallback(
68+
async (isInitialLoad: boolean) => {
69+
if (isInitialLoad) {
70+
setLoading(true);
71+
}
72+
setError(null);
73+
74+
const encodedQuery = encodeURIComponent(query);
75+
const url = `${PROMETHEUS_BASE_PATH}/api/v1/query?query=${encodedQuery}`;
76+
77+
try {
78+
const response: PrometheusResponse = await consoleFetchJSON(url);
79+
80+
if (response.status !== 'success') {
81+
setError('Query failed');
82+
return;
83+
}
84+
85+
const data = response.data;
86+
if (data.resultType === 'scalar') {
87+
// Scalar result is [timestamp, value]
88+
const scalarResult = data.result as unknown as [number, string];
89+
setResults([{ metric: {}, value: scalarResult[1] }]);
90+
setUpdateKey((k) => k + 1);
91+
} else if (data.resultType === 'vector') {
92+
setResults(
93+
data.result.map((r) => ({
94+
metric: r.metric,
95+
value: r.value[1],
96+
})),
97+
);
98+
setUpdateKey((k) => k + 1);
99+
} else {
100+
setError(`Unexpected result type: ${data.resultType}`);
101+
}
102+
} catch (err) {
103+
// Try tenancy endpoint as fallback
104+
try {
105+
const tenancyUrl = `${PROMETHEUS_TENANCY_BASE_PATH}/api/v1/query?query=${encodedQuery}`;
106+
const response: PrometheusResponse = await consoleFetchJSON(tenancyUrl);
107+
108+
if (response.status !== 'success') {
109+
setError('Query failed');
110+
return;
111+
}
112+
113+
const data = response.data;
114+
if (data.resultType === 'scalar') {
115+
const scalarResult = data.result as unknown as [number, string];
116+
setResults([{ metric: {}, value: scalarResult[1] }]);
117+
setUpdateKey((k) => k + 1);
118+
} else if (data.resultType === 'vector') {
119+
setResults(
120+
data.result.map((r) => ({
121+
metric: r.metric,
122+
value: r.value[1],
123+
})),
124+
);
125+
setUpdateKey((k) => k + 1);
126+
} else {
127+
setError(`Unexpected result type: ${data.resultType}`);
128+
}
129+
} catch {
130+
setError(err instanceof Error ? err.message : 'Failed to fetch data');
131+
}
132+
} finally {
133+
if (isInitialLoad) {
134+
setLoading(false);
135+
}
136+
}
137+
},
138+
[query],
139+
);
140+
141+
React.useEffect(() => {
142+
if (!query) {
143+
return;
144+
}
145+
146+
fetchData(true);
147+
148+
const intervalId = setInterval(() => {
149+
fetchData(false);
150+
}, POLL_INTERVAL);
151+
152+
return () => clearInterval(intervalId);
153+
}, [fetchData, query]);
154+
155+
const onChange = React.useCallback(
156+
(_event: React.ChangeEvent<HTMLTextAreaElement>, value: string) => {
157+
setInputValue(value);
158+
},
159+
[],
160+
);
161+
162+
const renderResults = () => {
163+
if (loading) {
164+
return <Spinner size="md" />;
165+
}
166+
167+
if (error) {
168+
return <span className="ols-plugin__promql-value-error">{error}</span>;
169+
}
170+
171+
if (results.length === 0) {
172+
return <span className="ols-plugin__promql-value-empty">No data</span>;
173+
}
174+
175+
if (results.length === 1 && Object.keys(results[0].metric).length === 0) {
176+
// Single scalar value
177+
return (
178+
<span className="ols-plugin__promql-value-badge" key={updateKey}>
179+
{formatValue(results[0].value)}
180+
</span>
181+
);
182+
}
183+
184+
// Multiple results with labels
185+
return (
186+
<div className="ols-plugin__promql-value-list" key={updateKey}>
187+
{results.map((result, index) => {
188+
const labelStr = Object.entries(result.metric)
189+
.map(([k, v]) => `${k}="${v}"`)
190+
.join(', ');
191+
return (
192+
<div className="ols-plugin__promql-value-item" key={index}>
193+
<span className="ols-plugin__promql-value-badge">{formatValue(result.value)}</span>
194+
{labelStr && <span className="ols-plugin__promql-value-labels">{labelStr}</span>}
195+
</div>
196+
);
197+
})}
198+
</div>
199+
);
200+
};
201+
202+
return (
203+
<div className="ols-plugin__promql-value">
204+
{renderResults()}
205+
<TextArea
206+
aria-label="PromQL query"
207+
autoResize
208+
className="ols-plugin__promql-value-input"
209+
onChange={onChange}
210+
resizeOrientation="vertical"
211+
rows={2}
212+
value={inputValue || initialQuery}
213+
/>
214+
</div>
215+
);
216+
};
217+
218+
export default PromQLValue;

src/components/general-page.css

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,60 @@
130130
margin-bottom: var(--pf-t--global--spacer--lg);
131131
margin-top: -25px;
132132
}
133+
134+
.ols-plugin__promql-value {
135+
margin: var(--pf-t--global--spacer--md) 0;
136+
}
137+
138+
.ols-plugin__promql-value-badge {
139+
animation: ols-plugin-flash 0.6s ease-out;
140+
background-color: var(--pf-t--global--background--color--100);
141+
border: 1px solid var(--pf-t--global--border--color--default);
142+
border-radius: var(--pf-t--global--border--radius--small);
143+
font-family: var(--pf-t--global--font--family--mono);
144+
font-size: var(--pf-t--global--font--size--heading--md);
145+
font-weight: var(--pf-t--global--font--weight--body--bold);
146+
padding: var(--pf-t--global--spacer--xs) var(--pf-t--global--spacer--sm);
147+
}
148+
149+
@keyframes ols-plugin-flash {
150+
0% {
151+
background-color: var(--pf-t--global--background--color--tertiary--default);
152+
}
153+
154+
100% {
155+
background-color: var(--pf-t--global--background--color--secondary--default);
156+
}
157+
}
158+
159+
.ols-plugin__promql-value-list {
160+
display: flex;
161+
flex-direction: column;
162+
gap: var(--pf-t--global--spacer--sm);
163+
}
164+
165+
.ols-plugin__promql-value-item {
166+
align-items: baseline;
167+
display: flex;
168+
gap: var(--pf-t--global--spacer--md);
169+
}
170+
171+
.ols-plugin__promql-value-labels {
172+
color: var(--pf-t--global--text--color--subtle);
173+
font-family: var(--pf-t--global--font--family--mono);
174+
font-size: var(--pf-t--global--font--size--body--sm);
175+
}
176+
177+
.ols-plugin__promql-value-error {
178+
color: var(--pf-t--global--color--status--danger--default);
179+
}
180+
181+
.ols-plugin__promql-value-empty {
182+
color: var(--pf-t--global--text--color--subtle);
183+
font-style: italic;
184+
}
185+
186+
.ols-plugin__promql-value-input {
187+
font-size: var(--pf-t--global--font--size--body--sm);
188+
margin-top: var(--pf-t--global--spacer--md);
189+
}

src/llm-instructions.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,25 @@ export const LLM_INSTRUCTIONS = `
33
44
The OpenShift Lightspeed console UI has special rendering capabilities for certain code block types. When appropriate, use these features to provide more helpful and visual responses, but only when they are more helpful than plain text or code examples.
55
6-
### PromQL line graphs
6+
### PromQL visualization
77
8-
The frontend can render Prometheus metrics as line graphs. Each \`\`\`promql code block in your response will automatically generate a corresponding line graph.
8+
The frontend can render Prometheus metrics in two ways:
99
10-
Only include PromQL when the user is explicitly asking for Prometheus metrics, monitoring data, alerts, or quantitative troubleshooting (for example: CPU/memory usage, rates, latency, error counts, “show me”, “how much”, “query”, or “graph”). For conceptual or definition questions that do not request metrics (for example: “what is a node?”), do not include PromQL.
10+
1. **Line graphs** (\`\`\`promql): Use for queries that show how values change over time, such as rates, trends, or time-series data. Examples: CPU usage over time, request rates, memory consumption trends.
1111
12-
When you include PromQL, use a \`\`\`promql code block. Each \`\`\`promql code block must contain exactly one PromQL query and must be valid PromQL that can be run as-is.
12+
2. **Single values** (\`\`\`promql-instant): Use for queries that return a point-in-time snapshot or aggregate, such as current counts, totals, or status checks. Examples: total number of pods, current memory usage, count of errors.
13+
14+
Use \`\`\`promql (line graph) for:
15+
- Queries where seeing the trend over time is meaningful
16+
- Queries with rate(), irate(), increase(), delta(), or deriv() functions
17+
- Raw metrics the user wants to monitor over time
18+
- Questions like "show me the trend", "how has X changed", "graph the usage"
19+
20+
IMPORTANT: Include a \`\`\`promql-instant code block for prompts that ask about any scalar metric value, such as:
21+
- A specific resource metric ("what's the CPU usage of pod X", "how much memory is node Y using", etc.)
22+
- The current value of a metric (CPU usage, memory usage, request rate, etc.)
23+
24+
Only include PromQL when the user is explicitly asking for Prometheus metrics, monitoring data, alerts, or quantitative troubleshooting (for example: CPU/memory usage, rates, latency, error counts, "show me", "how much", "query", or "graph"). For conceptual or definition questions that do not request metrics (for example: "what is a node?"), do not include PromQL.
25+
26+
Each code block must contain exactly one PromQL query and must be valid PromQL that can be run as-is.
1327
`.trim();

0 commit comments

Comments
 (0)