Explore: Add hide_logs_download and hide button to download logs (#99512)

* Explore: Add `disableLogsDownload` and hide button to download logs

* change copy

* Explore: Change `disableLogsDownload` to `hide_logs_download`

* change casing in frontend

* also hide from inspector

* add test

* lint
This commit is contained in:
Sven Grossmann 2025-01-29 11:53:52 +01:00 committed by GitHub
parent 6ea87802ed
commit 336449c169
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 55 additions and 8 deletions

View File

@ -1574,6 +1574,9 @@ enabled = true
# set the default offset for the time picker # set the default offset for the time picker
defaultTimeOffset = 1h defaultTimeOffset = 1h
# hides the download logs button in Explore
hide_logs_download = false
#################################### Help ############################# #################################### Help #############################
[help] [help]
# Enable the Help section # Enable the Help section

View File

@ -1934,6 +1934,10 @@ Enable or disable the Explore section. Default is `enabled`.
Set a default time offset from now on the time picker. Default is 1 hour. Set a default time offset from now on the time picker. Default is 1 hour.
This setting should be expressed as a duration. Examples: 1h (hour), 1d (day), 1w (week), 1M (month). This setting should be expressed as a duration. Examples: 1h (hour), 1d (day), 1w (week), 1M (month).
#### `hide_logs_download`
Show or hide the button to download logs in Explore. Default is `false`, so that the button will be visible.
### `[help]` ### `[help]`
Configures the help section. Configures the help section.

View File

@ -238,6 +238,7 @@ export interface GrafanaConfig {
listScopesEndpoint?: string; listScopesEndpoint?: string;
reportingStaticContext?: Record<string, string>; reportingStaticContext?: Record<string, string>;
exploreDefaultTimeOffset?: string; exploreDefaultTimeOffset?: string;
exploreHideLogsDownload?: boolean;
// The namespace to use for kubernetes apiserver requests // The namespace to use for kubernetes apiserver requests
namespace: string; namespace: string;

View File

@ -203,6 +203,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
cloudMigrationPollIntervalMs = 2000; cloudMigrationPollIntervalMs = 2000;
reportingStaticContext?: Record<string, string>; reportingStaticContext?: Record<string, string>;
exploreDefaultTimeOffset = '1h'; exploreDefaultTimeOffset = '1h';
exploreHideLogsDownload: boolean | undefined;
/** /**
* Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of * Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of

View File

@ -212,6 +212,7 @@ type FrontendSettingsDTO struct {
CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"` CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"`
EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"` EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"`
ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"` ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"`
ExploreHideLogsDownload bool `json:"ExploreHideLogsDownload"`
Auth FrontendSettingsAuthDTO `json:"auth"` Auth FrontendSettingsAuthDTO `json:"auth"`

View File

@ -247,6 +247,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable, LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable,
ReportingStaticContext: hs.Cfg.ReportingStaticContext, ReportingStaticContext: hs.Cfg.ReportingStaticContext,
ExploreDefaultTimeOffset: hs.Cfg.ExploreDefaultTimeOffset, ExploreDefaultTimeOffset: hs.Cfg.ExploreDefaultTimeOffset,
ExploreHideLogsDownload: hs.Cfg.ExploreHideLogsDownload,
DefaultDatasourceManageAlertsUIToggle: hs.Cfg.DefaultDatasourceManageAlertsUIToggle, DefaultDatasourceManageAlertsUIToggle: hs.Cfg.DefaultDatasourceManageAlertsUIToggle,

View File

@ -511,6 +511,7 @@ type Cfg struct {
// Explore UI // Explore UI
ExploreEnabled bool ExploreEnabled bool
ExploreDefaultTimeOffset string ExploreDefaultTimeOffset string
ExploreHideLogsDownload bool
// Help UI // Help UI
HelpEnabled bool HelpEnabled bool
@ -1214,6 +1215,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error {
} else { } else {
cfg.ExploreDefaultTimeOffset = exploreDefaultTimeOffset cfg.ExploreDefaultTimeOffset = exploreDefaultTimeOffset
} }
cfg.ExploreHideLogsDownload = explore.Key("hide_logs_download").MustBool(false)
help := iniFile.Section("help") help := iniFile.Section("help")
cfg.HelpEnabled = help.Key("enabled").MustBool(true) cfg.HelpEnabled = help.Key("enabled").MustBool(true)

View File

@ -5,6 +5,7 @@ import { ComponentProps } from 'react';
import { FieldType, LogLevel, LogsDedupStrategy, standardTransformersRegistry, toDataFrame } from '@grafana/data'; import { FieldType, LogLevel, LogsDedupStrategy, standardTransformersRegistry, toDataFrame } from '@grafana/data';
import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize'; import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize';
import { config } from '@grafana/runtime';
import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage'; import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage';
import { logRowsToReadableJson } from '../../logs/utils'; import { logRowsToReadableJson } from '../../logs/utils';
@ -32,11 +33,12 @@ const defaultProps: LogsMetaRowProps = {
clearDetectedFields: jest.fn(), clearDetectedFields: jest.fn(),
}; };
const setup = (propOverrides?: object) => { const setup = (propOverrides?: object, disableDownload = false) => {
const props = { const props = {
...defaultProps, ...defaultProps,
...propOverrides, ...propOverrides,
}; };
config.exploreHideLogsDownload = disableDownload;
return render(<LogsMetaRow {...props} />); return render(<LogsMetaRow {...props} />);
}; };
@ -121,6 +123,11 @@ describe('LogsMetaRow', () => {
expect(screen.getByText('Download').closest('button')).toBeInTheDocument(); expect(screen.getByText('Download').closest('button')).toBeInTheDocument();
}); });
it('does not render a button to show the download menu if disabled', async () => {
setup({}, true);
expect(screen.queryByText('Download')).toBeNull();
});
it('renders a button to show the download menu', async () => { it('renders a button to show the download menu', async () => {
setup(); setup();

View File

@ -16,7 +16,7 @@ import {
Labels, Labels,
} from '@grafana/data'; } from '@grafana/data';
import { DataFrame } from '@grafana/data/'; import { DataFrame } from '@grafana/data/';
import { reportInteraction } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime';
import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui';
import { downloadDataFrameAsCsv, downloadLogsModelAsTxt } from '../../inspector/utils/download'; import { downloadDataFrameAsCsv, downloadLogsModelAsTxt } from '../../inspector/utils/download';
@ -182,11 +182,13 @@ export const LogsMetaRow = memo(
}; };
})} })}
/> />
{!config.exploreHideLogsDownload && (
<Dropdown overlay={downloadMenu}> <Dropdown overlay={downloadMenu}>
<ToolbarButton isOpen={false} variant="canvas" icon="download-alt"> <ToolbarButton isOpen={false} variant="canvas" icon="download-alt">
Download Download
</ToolbarButton> </ToolbarButton>
</Dropdown> </Dropdown>
)}
</div> </div>
)} )}
</> </>

View File

@ -4,6 +4,7 @@ import { ComponentProps } from 'react';
import { Props } from 'react-virtualized-auto-sizer'; import { Props } from 'react-virtualized-auto-sizer';
import { DataFrame, FieldType } from '@grafana/data'; import { DataFrame, FieldType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { InspectDataTab } from './InspectDataTab'; import { InspectDataTab } from './InspectDataTab';
@ -75,6 +76,8 @@ describe('InspectDataTab', () => {
expect(screen.getByText(/Second data frame/i)).toBeInTheDocument(); expect(screen.getByText(/Second data frame/i)).toBeInTheDocument();
}); });
it('should show download logs button if logs data', () => { it('should show download logs button if logs data', () => {
const oldConfig = config.exploreHideLogsDownload;
config.exploreHideLogsDownload = false;
const dataWithLogs = [ const dataWithLogs = [
{ {
name: 'Data frame with logs', name: 'Data frame with logs',
@ -91,6 +94,28 @@ describe('InspectDataTab', () => {
] as unknown as DataFrame[]; ] as unknown as DataFrame[];
render(<InspectDataTab {...createProps({ data: dataWithLogs })} />); render(<InspectDataTab {...createProps({ data: dataWithLogs })} />);
expect(screen.getByText(/Download logs/i)).toBeInTheDocument(); expect(screen.getByText(/Download logs/i)).toBeInTheDocument();
config.exploreHideLogsDownload = oldConfig;
});
it('should not show download logs button if logs data but config disabled', () => {
const oldConfig = config.exploreHideLogsDownload;
config.exploreHideLogsDownload = true;
const dataWithLogs = [
{
name: 'Data frame with logs',
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300], config: {} },
{ name: 'name', type: FieldType.string, values: ['uniqueA', 'b', 'c'], config: {} },
{ name: 'value', type: FieldType.number, values: [1, 2, 3], config: {} },
],
length: 3,
meta: {
preferredVisualisationType: 'logs',
},
},
] as unknown as DataFrame[];
render(<InspectDataTab {...createProps({ data: dataWithLogs })} />);
expect(screen.queryByText(/Download logs/i)).not.toBeInTheDocument();
config.exploreHideLogsDownload = oldConfig;
}); });
it('should not show download logs button if no logs data', () => { it('should not show download logs button if no logs data', () => {
render(<InspectDataTab {...createProps()} />); render(<InspectDataTab {...createProps()} />);

View File

@ -223,7 +223,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
<Button variant="primary" onClick={() => this.exportCsv(dataFrames, hasLogs)} size="sm"> <Button variant="primary" onClick={() => this.exportCsv(dataFrames, hasLogs)} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-csv">Download CSV</Trans> <Trans i18nKey="dashboard.inspect-data.download-csv">Download CSV</Trans>
</Button> </Button>
{hasLogs && ( {hasLogs && !config.exploreHideLogsDownload && (
<Button variant="primary" onClick={this.onExportLogsAsTxt} size="sm"> <Button variant="primary" onClick={this.onExportLogsAsTxt} size="sm">
<Trans i18nKey="dashboard.inspect-data.download-logs">Download logs</Trans> <Trans i18nKey="dashboard.inspect-data.download-logs">Download logs</Trans>
</Button> </Button>