Merge branch 'main' into juanicabanas/controlled-collapsable-section-option
CodeQL checks / Analyze (go) (push) Waiting to run Details
CodeQL checks / Analyze (javascript) (push) Waiting to run Details
CodeQL checks / Analyze (python) (push) Waiting to run Details

This commit is contained in:
Juan Cabanas 2025-04-28 20:04:06 -03:00
commit b24e0a167e
No known key found for this signature in database
GPG Key ID: 6ADA94D1EA2A18C4
20 changed files with 192 additions and 64 deletions

View File

@ -36,9 +36,10 @@ runs:
shell: bash
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
BRANCH: ${{ inputs.branch }}
run: |
git clone https://x-access-token:${GH_TOKEN}@github.com/grafana/grafana-bench.git ../grafana-bench
cd ../grafana-bench
git switch ${{ inputs.branch }}
git switch "$BRANCH"
go install .

View File

@ -28,11 +28,13 @@ runs:
steps:
- name: Process Go coverage output
shell: bash
env:
COVERAGE_FILE: ${{ inputs.coverage-file }}
run: |
# Ensure valid coverage file even if empty
if [ ! -s ${{ inputs.coverage-file }} ]; then
if [ ! -s "$COVERAGE_FILE" ]; then
echo "Coverage file is empty, creating a minimal valid file"
echo "mode: set" > ${{ inputs.coverage-file }}
echo "mode: set" > "$COVERAGE_FILE"
fi
- name: Report coverage to CodeCov

View File

@ -52,7 +52,7 @@ jobs:
private_key: ${{ env.GH_APP_PEM }}
- name: Checkout Actions
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: "grafana/grafana-github-actions"
path: ./actions

View File

@ -38,11 +38,13 @@ jobs:
- name: Check if issue is in target project
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
TARGET_PROJECT: ${{ env.TARGET_PROJECT }}
run: |
gh api graphql -f query='
query($org: String!, $repo: String!) {
repository(name: $repo, owner: $org) {
issue (number: ${{ github.event.issue.number }}) {
issue (number: $ISSUE_NUMBER) {
id
projectItems(first:20) {
nodes {
@ -55,12 +57,14 @@ jobs:
}
}' -f org=$ORGANIZATION -f repo=$REPO > projects_data.json
echo 'IN_TARGET_PROJ='$(jq '.data.repository.issue.projectItems.nodes[] | select(.project.number==${{ env.TARGET_PROJECT }}) | .project != null' projects_data.json) >> $GITHUB_ENV
echo 'IN_TARGET_PROJ='$(jq '.data.repository.issue.projectItems.nodes[] | select(.project.number=='"$TARGET_PROJECT"') | .project != null' projects_data.json) >> $GITHUB_ENV
echo 'ITEM_ID='$(jq '.data.repository.issue.id' projects_data.json) >> $GITHUB_ENV
- name: Set up label array
if: env.IN_TARGET_PROJ
env:
LABEL_IDS: ${{ env.LABEL_IDS }}
run: |
IFS=',' read -ra LABEL_IDs <<< "${{ env.LABEL_IDs }}"
IFS=',' read -ra LABEL_IDs <<< "$LABEL_IDS"
for item in "${LABEL_IDs[@]}"; do
echo "Item: $item"
done
@ -68,6 +72,7 @@ jobs:
if: env.IN_TARGET_PROJ
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
LABEL_IDS: ${{ env.LABEL_IDS }}
run: |
gh api graphql -f query='
mutation ($labelableId: ID!, $labelIds: [ID!]!) {
@ -76,4 +81,4 @@ jobs:
) {
clientMutationId
}
}' -f labelableId=$ITEM_ID -f labelIds=${{ env.LABEL_IDs }}
}' -f labelableId=$ITEM_ID -f labelIds=$LABEL_IDS

View File

@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout Actions
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: "grafana/grafana-github-actions"
path: ./actions
@ -83,7 +83,7 @@ jobs:
private_key: ${{ env.GH_APP_PEM }}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Send issue to the auto triager action
id: auto_triage
@ -99,7 +99,7 @@ jobs:
- name: "Send Slack notification"
if: ${{ steps.auto_triage.outputs.triage_labels != '' }}
uses: slackapi/slack-github-action@v1.27.0
uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0
with:
payload: >
{

View File

@ -15,6 +15,9 @@ on:
issues:
types: [opened, closed]
permissions:
contents: read
jobs:
config:
runs-on: "ubuntu-latest"
@ -35,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: "grafana/grafana-github-actions"
path: ./actions

View File

@ -31,7 +31,7 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- name: Checkout Actions
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: "grafana/grafana-github-actions"
path: ./actions

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: "grafana/grafana-github-actions"
path: ./actions

View File

@ -17,6 +17,9 @@ on:
# target branch onto the source branch, to verify compatibility before merging.
jobs:
dispatch-job:
permissions:
contents: read
actions: write
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}

View File

@ -107,10 +107,14 @@ jobs:
key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }}
- name: Set the step name
id: set_file_name
env:
INI_NAME: ${{ matrix.ini_file }}
run: |
FILE_NAME=$(basename "${{ matrix.ini_file }}" .ini)
echo "FILE_NAME=$FILE_NAME" >> $GITHUB_ENV
- name: Run tests for ${{ env.FILE_NAME }}
FILE_NAME=$(basename "$env.INI_NAME" .ini)
echo "FILE_NAME=$FILE_NAME" >> $GITHUB_OUTPUT
- name: Run tests for ${{ steps.set_file_name.outputs.FILE_NAME }}
env:
INI_NAME: ${{ matrix.ini_file }}
run: |
cp -rf ${{ matrix.ini_file }} ${{ github.workspace }}/scripts/grafana-server/custom.ini
cp -rf $INI_NAME ${{ github.workspace }}/scripts/grafana-server/custom.ini
yarn e2e:dashboards-search || echo "Test failed but marking as success since unified search is behind a feature flag and should not block PRs"

View File

@ -50,10 +50,12 @@ jobs:
# Check if the user is in the list from the secret
- name: Check if user is allowed
id: check_user
env:
ALLOWED_USERS: ${{ env.ALLOWED_USERS }}
USERNAME: ${{ github.event.sender.login }}
run: |
# Convert the comma-separated list to an array
IFS=',' read -ra ALLOWED_USERS <<< "${{ env.ALLOWED_USERS }}"
USERNAME="${{ github.event.sender.login }}"
IFS=',' read -ra ALLOWED_USERS <<< "$ALLOWED_USERS"
# Check if user is in the allowed list
for allowed_user in "${ALLOWED_USERS[@]}"; do

View File

@ -344,11 +344,19 @@ func processHits(dec *json.Decoder, sr *SearchResponse) error {
return err
}
if tok == "hits" {
switch tok {
case "hits":
if err := streamHitsArray(dec, sr); err != nil {
return err
}
} else {
case "total":
var total *SearchResponseHitsTotal
err := dec.Decode(&total)
if err != nil {
return err
}
sr.Hits.Total = total
default:
// ignore these fields as they are not used in the current implementation
err := skipUnknownField(dec)
if err != nil {

View File

@ -44,9 +44,15 @@ func (r *SearchRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(root)
}
type SearchResponseHitsTotal struct {
Value int `json:"value"`
Relation string `json:"relation"`
}
// SearchResponseHits represents search response hits
type SearchResponseHits struct {
Hits []map[string]interface{}
Hits []map[string]interface{}
Total *SearchResponseHitsTotal `json:"total"`
}
// SearchResponse represents a search response

View File

@ -208,7 +208,12 @@ func processLogsResponse(res *es.SearchResponse, target *Query, configuredFields
frames := data.Frames{}
frame := data.NewFrame("", fields...)
setPreferredVisType(frame, data.VisTypeLogs)
setLogsCustomMeta(frame, searchWords, stringToIntWithDefaultValue(target.Metrics[0].Settings.Get("limit").MustString(), defaultSize))
var total int
if res.Hits.Total != nil {
total = res.Hits.Total.Value
}
setLogsCustomMeta(frame, searchWords, stringToIntWithDefaultValue(target.Metrics[0].Settings.Get("limit").MustString(), defaultSize), total)
frames = append(frames, frame)
queryRes.Frames = frames
@ -1192,7 +1197,7 @@ func setPreferredVisType(frame *data.Frame, visType data.VisType) {
frame.Meta.PreferredVisualization = visType
}
func setLogsCustomMeta(frame *data.Frame, searchWords map[string]bool, limit int) {
func setLogsCustomMeta(frame *data.Frame, searchWords map[string]bool, limit int, total int) {
i := 0
searchWordsList := make([]string, len(searchWords))
for searchWord := range searchWords {
@ -1212,6 +1217,7 @@ func setLogsCustomMeta(frame *data.Frame, searchWords map[string]bool, limit int
frame.Meta.Custom = map[string]interface{}{
"searchWords": searchWordsList,
"limit": limit,
"total": total,
}
}

View File

@ -46,6 +46,7 @@ func TestProcessLogsResponse(t *testing.T) {
{
"aggregations": {},
"hits": {
"total": { "value": 2 },
"hits": [
{
"_id": "fdsfs",
@ -107,7 +108,7 @@ func TestProcessLogsResponse(t *testing.T) {
logsFrame := frames[0]
meta := logsFrame.Meta
require.Equal(t, map[string]any{"searchWords": []string{"hello", "message"}, "limit": 500}, meta.Custom)
require.Equal(t, map[string]any{"searchWords": []string{"hello", "message"}, "limit": 500, "total": 2}, meta.Custom)
require.Equal(t, data.VisTypeLogs, string(meta.PreferredVisualization))
logsFieldMap := make(map[string]*data.Field)
@ -431,6 +432,7 @@ func TestProcessLogsResponse(t *testing.T) {
require.Equal(t, map[string]any{
"searchWords": []string{"hello", "message"},
"limit": 500,
"total": 109,
}, customMeta)
})
}
@ -703,7 +705,7 @@ func TestProcessRawDocumentResponse(t *testing.T) {
"responses": [
{
"hits": {
"total": 100,
"total": { "value": 100 },
"hits": [
{
"_id": "1",
@ -3239,7 +3241,7 @@ func TestParseResponse(t *testing.T) {
},
{
"hits": {
"total": 2,
"total": { "value": 2 },
"hits": [
{
"_id": "5",

View File

@ -10,7 +10,8 @@
// "searchWords": [
// "hello",
// "message"
// ]
// ],
// "total": 81
// },
// "preferredVisualisationType": "logs"
// }
@ -45,7 +46,8 @@
"searchWords": [
"hello",
"message"
]
],
"total": 81
},
"preferredVisualisationType": "logs"
},

View File

@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObject } from '@grafana/scenes';
import { Box, Icon, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui';
import { Box, Icon, Text, useElementSelection, useStyles2, useTheme2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
@ -24,7 +24,7 @@ export function DashboardOutline({ editPane }: Props) {
const dashboard = getDashboardSceneFor(editPane);
return (
<Box padding={1} gap={0.25} display="flex" direction="column">
<Box padding={1} gap={0} display="flex" direction="column">
<DashboardOutlineNode sceneObject={dashboard} editPane={editPane} depth={0} />
</Box>
);
@ -40,6 +40,7 @@ function DashboardOutlineNode({
depth: number;
}) {
const [isCollapsed, setIsCollapsed] = useState(depth > 0);
const theme = useTheme2();
const { key } = sceneObject.useState();
const styles = useStyles2(getStyles);
const { isSelected, onSelect } = useElementSelection(key);
@ -53,7 +54,7 @@ function DashboardOutlineNode({
const elementCollapsed = editableElement.getCollapsedState?.();
const outlineRename = useOutlineRename(editableElement);
const onNameClicked = (evt: React.PointerEvent) => {
const onNodeClicked = (evt: React.PointerEvent) => {
// Only select via clicking outline never deselect
if (!isSelected) {
onSelect?.(evt);
@ -62,7 +63,8 @@ function DashboardOutlineNode({
editableElement.scrollIntoView?.();
};
const onToggleCollapse = () => {
const onToggleCollapse = (evt: React.MouseEvent) => {
evt.stopPropagation();
setIsCollapsed(!isCollapsed);
// Sync expanded state with canvas element
@ -80,16 +82,19 @@ function DashboardOutlineNode({
return (
<>
<Stack gap={0.5}>
<div
className={cx(styles.container, isSelected && styles.containerSelected)}
style={{ paddingLeft: theme.spacing(depth * 3) }}
onPointerDown={onNodeClicked}
>
{elementInfo.isContainer && (
<button role="treeitem" className={styles.angleButton} onClick={onToggleCollapse}>
<button role="treeitem" className={styles.angleButton} onPointerDown={onToggleCollapse}>
<Icon name={!isCollapsed ? 'angle-down' : 'angle-right'} />
</button>
)}
<button
role="button"
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)}
onPointerDown={onNameClicked}
className={cx(styles.nodeName, isCloned && styles.nodeNameClone)}
onDoubleClick={outlineRename.onNameDoubleClicked}
>
<Icon size="sm" name={elementInfo.icon} />
@ -111,10 +116,11 @@ function DashboardOutlineNode({
</>
)}
</button>
</Stack>
</div>
{elementInfo.isContainer && !isCollapsed && (
<div className={styles.container} role="group">
<div className={styles.nodeChildren}>
<div className={styles.nodeChildrenLine} style={{ marginLeft: theme.spacing(depth * 3) }} />
{children.length > 0 ? (
children.map((child) => (
<DashboardOutlineNode
@ -139,11 +145,29 @@ function getStyles(theme: GrafanaTheme2) {
return {
container: css({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
marginLeft: theme.spacing(1),
paddingLeft: theme.spacing(1.5),
borderLeft: `1px solid ${theme.colors.border.medium}`,
alignItems: 'center',
flexGrow: 1,
borderRadius: theme.shape.radius.default,
position: 'relative',
marginBottom: theme.spacing(0.25),
color: theme.colors.text.secondary,
'&:hover': {
color: theme.colors.text.primary,
outline: `1px dashed ${theme.colors.border.strong}`,
outlineOffset: '0px',
backgroundColor: theme.colors.emphasize(theme.colors.background.primary, 0.05),
},
}),
containerSelected: css({
outline: `1px dashed ${theme.colors.primary.border} !important`,
outlineOffset: '0px',
color: theme.colors.text.primary,
'&:hover': {
outline: `1px dashed ${theme.colors.primary.border}`,
color: theme.colors.text.primary,
},
}),
angleButton: css({
boxShadow: 'none',
@ -151,57 +175,58 @@ function getStyles(theme: GrafanaTheme2) {
background: 'transparent',
borderRadius: theme.shape.radius.default,
padding: 0,
color: theme.colors.text.secondary,
color: 'inherit',
lineHeight: 0,
}),
nodeButton: css({
nodeName: css({
boxShadow: 'none',
border: 'none',
background: 'transparent',
padding: theme.spacing(0.25, 1, 0.25, 0),
borderRadius: theme.shape.radius.default,
color: theme.colors.text.secondary,
color: 'inherit',
display: 'flex',
flexGrow: 1,
alignItems: 'center',
gap: theme.spacing(0.5),
overflow: 'hidden',
'&:hover': {
color: theme.colors.text.primary,
outline: `1px dashed ${theme.colors.border.strong}`,
outlineOffset: '0px',
backgroundColor: theme.colors.emphasize(theme.colors.background.canvas, 0.08),
},
'> span': {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}),
nodeButtonSelected: css({
color: theme.colors.text.primary,
outline: `1px dashed ${theme.colors.primary.border} !important`,
outlineOffset: '0px',
'&:hover': {
outline: `1px dashed ${theme.colors.primary.border}`,
},
}),
hiddenIcon: css({
color: theme.colors.text.secondary,
marginLeft: theme.spacing(1),
}),
nodeButtonClone: css({
nodeNameClone: css({
color: theme.colors.text.secondary,
cursor: 'not-allowed',
}),
outlineInput: css({
border: `1px solid ${theme.colors.primary.border}`,
border: `1px solid ${theme.components.input.borderColor}`,
height: theme.spacing(3),
borderRadius: theme.shape.radius.default,
'&:focus': {
outline: 'none',
boxShadow: 'none',
},
}),
nodeChildren: css({
display: 'flex',
flexDirection: 'column',
position: 'relative',
}),
nodeChildrenLine: css({
position: 'absolute',
width: '1px',
height: '100%',
left: '7px',
zIndex: 1,
backgroundColor: theme.colors.border.weak,
}),
};
}

View File

@ -24,7 +24,8 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa
new OptionsPaneCategoryDescriptor({ title: '', id: 'row-options' })
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.row.title', 'Title'),
title: '',
skipField: true,
render: () => <RowTitleInput row={model} isNewElement={isNewElement} />,
})
)
@ -85,6 +86,7 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool
return (
<Field
label={t('dashboard.rows-layout.row-options.row.title', 'Title')}
invalid={!hasUniqueTitle}
error={
!hasUniqueTitle ? t('dashboard.rows-layout.row-options.title-not-unique', 'Title should be unique') : undefined

View File

@ -34,6 +34,7 @@ import {
filterLogLevels,
getSeriesProperties,
LIMIT_LABEL,
TOTAL_LABEL,
logRowToSingleRowDataFrame,
logSeriesToLogsModel,
queryLogsSample,
@ -492,6 +493,52 @@ describe('dataFrameToLogsModel', () => {
});
});
it('given one series with total as custom meta property should return correct total', () => {
const series: DataFrame[] = [
createDataFrame({
fields: [],
meta: {
custom: {
total: 9999,
},
},
}),
];
const logsModel = dataFrameToLogsModel(series, 1);
expect(logsModel.meta![0]).toMatchObject({
label: TOTAL_LABEL,
value: 9999,
kind: LogsMetaKind.Number,
});
});
it('given multiple series with total as custom meta property should return correct total', () => {
const series: DataFrame[] = [
createDataFrame({
fields: [],
meta: {
custom: {
total: 4,
},
},
}),
createDataFrame({
fields: [],
meta: {
custom: {
total: 5,
},
},
}),
];
const logsModel = dataFrameToLogsModel(series, 1);
expect(logsModel.meta![0]).toMatchObject({
label: TOTAL_LABEL,
value: 9,
kind: LogsMetaKind.Number,
});
});
it('should return the expected meta when the line limit is reached', () => {
const series: DataFrame[] = getTestDataFrame();
series[0].meta = {

View File

@ -51,6 +51,7 @@ import { createLogRowsMap, getLogLevel, getLogLevelFromKey, sortInAscendingOrder
export const LIMIT_LABEL = 'Line limit';
export const COMMON_LABELS = 'Common labels';
export const TOTAL_LABEL = 'Total lines';
export const LogLevelColor = {
[LogLevel.critical]: colors[7],
@ -492,6 +493,15 @@ export function logSeriesToLogsModel(
});
}
const totalValue = logSeries.reduce((acc, series) => (acc += series.meta?.custom?.total), 0);
if (totalValue > 0) {
meta.push({
label: TOTAL_LABEL,
value: totalValue,
kind: LogsMetaKind.Number,
});
}
let totalBytes = 0;
const queriesVisited: { [refId: string]: boolean } = {};
// To add just 1 error message