mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
merge master
This commit is contained in:
@@ -231,6 +231,7 @@ verify_email_enabled = false
|
||||
|
||||
# Background text for the user field on the login page
|
||||
login_hint = email or username
|
||||
password_hint = password
|
||||
|
||||
# Default UI theme ("dark" or "light")
|
||||
default_theme = dark
|
||||
|
||||
@@ -211,6 +211,7 @@ log_queries =
|
||||
|
||||
# Background text for the user field on the login page
|
||||
;login_hint = email or username
|
||||
;password_hint = password
|
||||
|
||||
# Default UI theme ("dark" or "light")
|
||||
;default_theme = dark
|
||||
|
||||
@@ -162,9 +162,9 @@ executed with working directory set to the installation path.
|
||||
|
||||
### enable_gzip
|
||||
|
||||
Set this option to `true` to enable HTTP compression, this can improve
|
||||
transfer speed and bandwidth utilization. It is recommended that most
|
||||
users set it to `true`. By default it is set to `false` for compatibility
|
||||
Set this option to `true` to enable HTTP compression, this can improve
|
||||
transfer speed and bandwidth utilization. It is recommended that most
|
||||
users set it to `true`. By default it is set to `false` for compatibility
|
||||
reasons.
|
||||
|
||||
### cert_file
|
||||
@@ -342,6 +342,14 @@ options are `Admin` and `Editor`. e.g. :
|
||||
Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
|
||||
Defaults to `false`.
|
||||
|
||||
### login_hint
|
||||
|
||||
Text used as placeholder text on login page for login/username input.
|
||||
|
||||
### password_hint
|
||||
|
||||
Text used as placeholder text on login page for password input.
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth]
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"jquery": "^3.2.1",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.2",
|
||||
"papaparse": "^4.6.3",
|
||||
"react": "^16.6.3",
|
||||
"react-color": "^2.17.0",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
@@ -46,6 +47,7 @@
|
||||
"@types/jquery": "^1.10.35",
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/node": "^10.12.18",
|
||||
"@types/papaparse": "^4.5.9",
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-test-renderer": "^16.0.3",
|
||||
|
||||
@@ -39,7 +39,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
|
||||
this.props.onChange(color);
|
||||
};
|
||||
|
||||
handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
onChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const newColor = tinycolor(event.currentTarget.value);
|
||||
|
||||
this.setState({
|
||||
@@ -51,7 +51,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
|
||||
}
|
||||
};
|
||||
|
||||
handleBlur = () => {
|
||||
onBlur = () => {
|
||||
const newColor = tinycolor(this.state.value);
|
||||
|
||||
if (!newColor.isValid()) {
|
||||
@@ -84,7 +84,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<input className="gf-form-input" value={value} onChange={this.handleChange} onBlur={this.handleBlur} />
|
||||
<input className="gf-form-input" value={value} onChange={this.onChange} onBlur={this.onBlur} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
static displayName = displayName;
|
||||
pickerTriggerRef = createRef<HTMLDivElement>();
|
||||
|
||||
handleColorChange = (color: string) => {
|
||||
onColorChange = (color: string) => {
|
||||
const { onColorChange, onChange } = this.props;
|
||||
const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler;
|
||||
|
||||
@@ -25,7 +25,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
render() {
|
||||
const popoverElement = React.createElement(popover, {
|
||||
...this.props,
|
||||
onChange: this.handleColorChange,
|
||||
onChange: this.onColorChange,
|
||||
});
|
||||
const { theme, children } = this.props;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
changeHandler(getColorFromHexRgbOrName(color, theme.type));
|
||||
};
|
||||
|
||||
handleTabChange = (tab: PickerType | keyof T) => {
|
||||
onTabChange = (tab: PickerType | keyof T) => {
|
||||
return () => this.setState({ activePicker: tab });
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
<>
|
||||
{Object.keys(customPickers).map(key => {
|
||||
return (
|
||||
<div className={this.getTabClassName(key)} onClick={this.handleTabChange(key)} key={key}>
|
||||
<div className={this.getTabClassName(key)} onClick={this.onTabChange(key)} key={key}>
|
||||
{customPickers[key].name}
|
||||
</div>
|
||||
);
|
||||
@@ -119,10 +119,10 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
return (
|
||||
<div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
|
||||
<div className="ColorPickerPopover__tabs">
|
||||
<div className={this.getTabClassName('palette')} onClick={this.handleTabChange('palette')}>
|
||||
<div className={this.getTabClassName('palette')} onClick={this.onTabChange('palette')}>
|
||||
Colors
|
||||
</div>
|
||||
<div className={this.getTabClassName('spectrum')} onClick={this.handleTabChange('spectrum')}>
|
||||
<div className={this.getTabClassName('spectrum')} onClick={this.onTabChange('spectrum')}>
|
||||
Custom
|
||||
</div>
|
||||
{this.renderCustomPickerTabs()}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
|
||||
.panel-options-group__title {
|
||||
font-size: 1.1rem;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import TableInputCSV from './TableInputCSV';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { TableData } from '../../types/data';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
|
||||
const TableInputStories = storiesOf('UI/Table/Input', module);
|
||||
|
||||
TableInputStories.addDecorator(withCenteredStory);
|
||||
|
||||
TableInputStories.add('default', () => {
|
||||
return (
|
||||
<div style={{ width: '90%', height: '90vh' }}>
|
||||
<TableInputCSV
|
||||
text={'a,b,c\n1,2,3'}
|
||||
onTableParsed={(table: TableData, text: string) => {
|
||||
console.log('Table', table, text);
|
||||
action('Table')(table, text);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
import renderer from 'react-test-renderer';
|
||||
import TableInputCSV from './TableInputCSV';
|
||||
import { TableData } from '../../types/data';
|
||||
|
||||
describe('TableInputCSV', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<TableInputCSV
|
||||
text={'a,b,c\n1,2,3'}
|
||||
onTableParsed={(table: TableData, text: string) => {
|
||||
// console.log('Table:', table, 'from:', text);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
.toJSON();
|
||||
//expect(tree).toMatchSnapshot();
|
||||
expect(tree).toBeDefined();
|
||||
});
|
||||
});
|
||||
95
packages/grafana-ui/src/components/Table/TableInputCSV.tsx
Normal file
95
packages/grafana-ui/src/components/Table/TableInputCSV.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { parseCSV, TableParseOptions, TableParseDetails } from '../../utils/processTableData';
|
||||
import { TableData } from '../../types/data';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
|
||||
interface Props {
|
||||
options?: TableParseOptions;
|
||||
text: string;
|
||||
onTableParsed: (table: TableData, text: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
text: string;
|
||||
table: TableData;
|
||||
details: TableParseDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects the container div to have size set and will fill it 100%
|
||||
*/
|
||||
class TableInputCSV extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Shoud this happen in onComponentMounted?
|
||||
const { text, options, onTableParsed } = props;
|
||||
const details = {};
|
||||
const table = parseCSV(text, options, details);
|
||||
this.state = {
|
||||
text,
|
||||
table,
|
||||
details,
|
||||
};
|
||||
onTableParsed(table, text);
|
||||
}
|
||||
|
||||
readCSV = debounce(() => {
|
||||
const details = {};
|
||||
const table = parseCSV(this.state.text, this.props.options, details);
|
||||
this.setState({ table, details });
|
||||
}, 150);
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const { text } = this.state;
|
||||
if (text !== prevState.text || this.props.options !== prevProps.options) {
|
||||
this.readCSV();
|
||||
}
|
||||
// If the props text has changed, replace our local version
|
||||
if (this.props.text !== prevProps.text && this.props.text !== text) {
|
||||
this.setState({ text: this.props.text });
|
||||
}
|
||||
|
||||
if (this.state.table !== prevState.table) {
|
||||
this.props.onTableParsed(this.state.table, this.state.text);
|
||||
}
|
||||
}
|
||||
|
||||
onFooterClicked = (event: any) => {
|
||||
console.log('Errors', this.state);
|
||||
const message = this.state.details
|
||||
.errors!.map(err => {
|
||||
return err.message;
|
||||
})
|
||||
.join('\n');
|
||||
alert('CSV Parsing Errors:\n' + message);
|
||||
};
|
||||
|
||||
onTextChange = (event: any) => {
|
||||
this.setState({ text: event.target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { table, details } = this.state;
|
||||
|
||||
const hasErrors = details.errors && details.errors.length > 0;
|
||||
const footerClassNames = hasErrors ? 'gf-table-input-csv-err' : '';
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<div className="gf-table-input-csv" style={{ width, height }}>
|
||||
<textarea placeholder="Enter CSV here..." value={this.state.text} onChange={this.onTextChange} />
|
||||
<footer onClick={this.onFooterClicked} className={footerClassNames}>
|
||||
Rows:{table.rows.length}, Columns:{table.columns.length}
|
||||
{hasErrors ? <i className="fa fa-exclamation-triangle" /> : <i className="fa fa-check-circle" />}
|
||||
</footer>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TableInputCSV;
|
||||
24
packages/grafana-ui/src/components/Table/_TableInputCSV.scss
Normal file
24
packages/grafana-ui/src/components/Table/_TableInputCSV.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
.gf-table-input-csv {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gf-table-input-csv textarea {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.gf-table-input-csv footer {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
border: 1px solid #222;
|
||||
background: #ccc;
|
||||
padding: 1px 4px;
|
||||
font-size: 80%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gf-table-input-csv footer.gf-table-input-csv-err {
|
||||
background: yellow;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
@import 'DeleteButton/DeleteButton';
|
||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||
@import 'Table/Table';
|
||||
@import 'Table/TableInputCSV';
|
||||
@import 'Tooltip/Tooltip';
|
||||
@import 'Select/Select';
|
||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||
|
||||
@@ -17,7 +17,7 @@ $enable-hover-media-query: false !default;
|
||||
// Control the default styling of most Bootstrap elements by modifying these
|
||||
// variables. Mostly focused on spacing.
|
||||
|
||||
$spacer: 1rem !default;
|
||||
$spacer: ${theme.spacing.m} !default;
|
||||
$spacer-x: $spacer !default;
|
||||
$spacer-y: $spacer !default;
|
||||
$spacers: (
|
||||
@@ -46,7 +46,7 @@ $spacers: (
|
||||
),
|
||||
),
|
||||
) !default;
|
||||
$border-width: 1px !default;
|
||||
$border-width: ${theme.border.width.s} !default;
|
||||
|
||||
// Grid breakpoints
|
||||
//
|
||||
@@ -54,11 +54,11 @@ $border-width: 1px !default;
|
||||
// adapting to different screen sizes, for use in media queries.
|
||||
|
||||
$grid-breakpoints: (
|
||||
xs: 0,
|
||||
sm: 544px,
|
||||
md: 768px,
|
||||
lg: 992px,
|
||||
xl: 1200px,
|
||||
xs: ${theme.breakpoints.xs},
|
||||
sm: ${theme.breakpoints.s},
|
||||
md: ${theme.breakpoints.m},
|
||||
lg: ${theme.breakpoints.l},
|
||||
xl: ${theme.breakpoints.xl},
|
||||
) !default;
|
||||
|
||||
// Grid containers
|
||||
@@ -84,46 +84,32 @@ $enable-flex: true;
|
||||
// Typography
|
||||
// -------------------------
|
||||
|
||||
$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
$font-family-serif: Georgia, 'Times New Roman', Times, serif;
|
||||
$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
$font-family-sans-serif: ${theme.typography.fontFamily.sansSerif};
|
||||
$font-family-monospace: ${theme.typography.fontFamily.monospace};
|
||||
$font-family-base: $font-family-sans-serif !default;
|
||||
|
||||
$font-size-root: 14px !default;
|
||||
$font-size-base: 13px !default;
|
||||
$font-size-root: ${theme.typography.size.root} !default;
|
||||
$font-size-base: ${theme.typography.size.base} !default;
|
||||
|
||||
$font-size-lg: 18px !default;
|
||||
$font-size-md: 14px !default;
|
||||
$font-size-sm: 12px !default;
|
||||
$font-size-xs: 10px !default;
|
||||
$font-size-lg: ${theme.typography.size.l} !default;
|
||||
$font-size-md: ${theme.typography.size.m} !default;
|
||||
$font-size-sm: ${theme.typography.size.s} !default;
|
||||
$font-size-xs: ${theme.typography.size.xs} !default;
|
||||
|
||||
$line-height-base: 1.5 !default;
|
||||
$font-weight-semi-bold: 500;
|
||||
$line-height-base: ${theme.typography.lineHeight.l} !default;
|
||||
$font-weight-semi-bold: ${theme.typography.weight.semibold};
|
||||
|
||||
$font-size-h1: 2rem !default;
|
||||
$font-size-h2: 1.75rem !default;
|
||||
$font-size-h3: 1.5rem !default;
|
||||
$font-size-h4: 1.3rem !default;
|
||||
$font-size-h5: 1.2rem !default;
|
||||
$font-size-h6: 1rem !default;
|
||||
|
||||
$display1-size: 6rem !default;
|
||||
$display2-size: 5.5rem !default;
|
||||
$display3-size: 4.5rem !default;
|
||||
$display4-size: 3.5rem !default;
|
||||
|
||||
$display1-weight: 400 !default;
|
||||
$display2-weight: 400 !default;
|
||||
$display3-weight: 400 !default;
|
||||
$display4-weight: 400 !default;
|
||||
|
||||
$lead-font-size: 1.25rem !default;
|
||||
$lead-font-weight: 300 !default;
|
||||
$font-size-h1: ${theme.typography.heading.h1} !default;
|
||||
$font-size-h2: ${theme.typography.heading.h2} !default;
|
||||
$font-size-h3: ${theme.typography.heading.h3} !default;
|
||||
$font-size-h4: ${theme.typography.heading.h4} !default;
|
||||
$font-size-h5: ${theme.typography.heading.h5} !default;
|
||||
$font-size-h6: ${theme.typography.heading.h6} !default;
|
||||
|
||||
$headings-margin-bottom: ($spacer / 2) !default;
|
||||
$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
$headings-font-weight: 400 !default;
|
||||
$headings-line-height: 1.1 !default;
|
||||
$headings-font-weight: ${theme.typography.weight.normal} !default;
|
||||
$headings-line-height: ${theme.typography.lineHeight.s} !default;
|
||||
|
||||
$hr-border-width: $border-width !default;
|
||||
$dt-font-weight: bold !default;
|
||||
@@ -141,8 +127,8 @@ $border-radius-sm: 2px !default;
|
||||
|
||||
// Page
|
||||
|
||||
$page-sidebar-width: 11rem;
|
||||
$page-sidebar-margin: 4rem;
|
||||
$page-sidebar-width: 154px;
|
||||
$page-sidebar-margin: 56px;
|
||||
|
||||
// Links
|
||||
// -------------------------
|
||||
@@ -174,7 +160,7 @@ $input-padding-y-lg: 10px !default;
|
||||
|
||||
$input-height: 35px !default;
|
||||
|
||||
$gf-form-margin: 0.2rem;
|
||||
$gf-form-margin: 3px;
|
||||
$gf-form-input-height: 35px;
|
||||
|
||||
$cursor-disabled: not-allowed !default;
|
||||
@@ -199,13 +185,13 @@ $zindex-typeahead: 1060;
|
||||
// Buttons
|
||||
//
|
||||
|
||||
$btn-padding-x: 1rem !default;
|
||||
$btn-padding-y: 0.7rem !default;
|
||||
$btn-padding-x: 14px !default;
|
||||
$btn-padding-y: 10px !default;
|
||||
$btn-line-height: 1 !default;
|
||||
$btn-font-weight: 500 !default;
|
||||
$btn-font-weight: ${theme.typography.weight.semibold} !default;
|
||||
|
||||
$btn-padding-x-sm: 0.5rem !default;
|
||||
$btn-padding-y-sm: 0.25rem !default;
|
||||
$btn-padding-x-sm: 7px !default;
|
||||
$btn-padding-y-sm: 4px !default;
|
||||
|
||||
$btn-padding-x-lg: 21px !default;
|
||||
$btn-padding-y-lg: 11px !default;
|
||||
|
||||
@@ -5,10 +5,10 @@ const theme: GrafanaThemeCommons = {
|
||||
typography: {
|
||||
fontFamily: {
|
||||
sansSerif: "'Roboto', Helvetica, Arial, sans-serif",
|
||||
serif: "Georgia, 'Times New Roman', Times, serif",
|
||||
monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
||||
},
|
||||
size: {
|
||||
root: '14px',
|
||||
base: '13px',
|
||||
xs: '10px',
|
||||
s: '12px',
|
||||
@@ -16,12 +16,12 @@ const theme: GrafanaThemeCommons = {
|
||||
l: '18px',
|
||||
},
|
||||
heading: {
|
||||
h1: '2rem',
|
||||
h2: '1.75rem',
|
||||
h3: '1.5rem',
|
||||
h4: '1.3rem',
|
||||
h5: '1.2rem',
|
||||
h6: '1rem',
|
||||
h1: '28px',
|
||||
h2: '24px',
|
||||
h3: '21px',
|
||||
h4: '18px',
|
||||
h5: '16px',
|
||||
h6: '14px',
|
||||
},
|
||||
weight: {
|
||||
light: 300,
|
||||
@@ -35,7 +35,7 @@ const theme: GrafanaThemeCommons = {
|
||||
l: 1.5,
|
||||
},
|
||||
},
|
||||
brakpoints: {
|
||||
breakpoints: {
|
||||
xs: '0',
|
||||
s: '544px',
|
||||
m: '768px',
|
||||
@@ -44,9 +44,9 @@ const theme: GrafanaThemeCommons = {
|
||||
},
|
||||
spacing: {
|
||||
xs: '0',
|
||||
s: '0.2rem',
|
||||
m: '1rem',
|
||||
l: '1.5rem',
|
||||
s: '3px',
|
||||
m: '14px',
|
||||
l: '21px',
|
||||
gutter: '30px',
|
||||
},
|
||||
border: {
|
||||
@@ -55,6 +55,9 @@ const theme: GrafanaThemeCommons = {
|
||||
s: '3px',
|
||||
m: '5px',
|
||||
},
|
||||
width: {
|
||||
s: '1px',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export enum GrafanaThemeType {
|
||||
export interface GrafanaThemeCommons {
|
||||
name: string;
|
||||
// TODO: not sure if should be a part of theme
|
||||
brakpoints: {
|
||||
breakpoints: {
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
@@ -16,10 +16,10 @@ export interface GrafanaThemeCommons {
|
||||
typography: {
|
||||
fontFamily: {
|
||||
sansSerif: string;
|
||||
serif: string;
|
||||
monospace: string;
|
||||
};
|
||||
size: {
|
||||
root: string;
|
||||
base: string;
|
||||
xs: string;
|
||||
s: string;
|
||||
@@ -60,6 +60,9 @@ export interface GrafanaThemeCommons {
|
||||
s: string;
|
||||
m: string;
|
||||
};
|
||||
width: {
|
||||
s: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`processTableData basic processing should generate a header and fix widths 1`] = `
|
||||
Object {
|
||||
"columnMap": Object {},
|
||||
"columns": Array [
|
||||
Object {
|
||||
"text": "Column 1",
|
||||
},
|
||||
Object {
|
||||
"text": "Column 2",
|
||||
},
|
||||
Object {
|
||||
"text": "Column 3",
|
||||
},
|
||||
],
|
||||
"rows": Array [
|
||||
Array [
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
],
|
||||
Array [
|
||||
5,
|
||||
6,
|
||||
null,
|
||||
],
|
||||
],
|
||||
"type": "table",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`processTableData basic processing should read header and two rows 1`] = `
|
||||
Object {
|
||||
"columnMap": Object {},
|
||||
"columns": Array [
|
||||
Object {
|
||||
"text": "a",
|
||||
},
|
||||
Object {
|
||||
"text": "b",
|
||||
},
|
||||
Object {
|
||||
"text": "c",
|
||||
},
|
||||
],
|
||||
"rows": Array [
|
||||
Array [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
Array [
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
],
|
||||
],
|
||||
"type": "table",
|
||||
}
|
||||
`;
|
||||
20
packages/grafana-ui/src/utils/processTableData.test.ts
Normal file
20
packages/grafana-ui/src/utils/processTableData.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { parseCSV } from './processTableData';
|
||||
|
||||
describe('processTableData', () => {
|
||||
describe('basic processing', () => {
|
||||
it('should read header and two rows', () => {
|
||||
const text = 'a,b,c\n1,2,3\n4,5,6';
|
||||
expect(parseCSV(text)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a header and fix widths', () => {
|
||||
const text = '1\n2,3,4\n5,6';
|
||||
const table = parseCSV(text, {
|
||||
headerIsFirstLine: false,
|
||||
});
|
||||
expect(table.rows.length).toBe(3);
|
||||
|
||||
expect(table).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
133
packages/grafana-ui/src/utils/processTableData.ts
Normal file
133
packages/grafana-ui/src/utils/processTableData.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { TableData, Column } from '../types/index';
|
||||
|
||||
import Papa, { ParseError, ParseMeta } from 'papaparse';
|
||||
|
||||
// Subset of all parse options
|
||||
export interface TableParseOptions {
|
||||
headerIsFirstLine?: boolean; // Not a papa-parse option
|
||||
delimiter?: string; // default: ","
|
||||
newline?: string; // default: "\r\n"
|
||||
quoteChar?: string; // default: '"'
|
||||
encoding?: string; // default: ""
|
||||
comments?: boolean | string; // default: false
|
||||
}
|
||||
|
||||
export interface TableParseDetails {
|
||||
meta?: ParseMeta;
|
||||
errors?: ParseError[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes sure the header and all rows have equal length.
|
||||
*
|
||||
* @param table (immutable)
|
||||
* @returns a new table that has equal length rows, or the same
|
||||
* table if no changes were needed
|
||||
*/
|
||||
export function matchRowSizes(table: TableData): TableData {
|
||||
const { rows } = table;
|
||||
let { columns } = table;
|
||||
|
||||
let sameSize = true;
|
||||
let size = columns.length;
|
||||
rows.forEach(row => {
|
||||
if (size !== row.length) {
|
||||
sameSize = false;
|
||||
size = Math.max(size, row.length);
|
||||
}
|
||||
});
|
||||
if (sameSize) {
|
||||
return table;
|
||||
}
|
||||
|
||||
// Pad Columns
|
||||
if (size !== columns.length) {
|
||||
const diff = size - columns.length;
|
||||
columns = [...columns];
|
||||
for (let i = 0; i < diff; i++) {
|
||||
columns.push({
|
||||
text: 'Column ' + (columns.length + 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pad Rows
|
||||
const fixedRows: any[] = [];
|
||||
rows.forEach(row => {
|
||||
const diff = size - row.length;
|
||||
if (diff > 0) {
|
||||
row = [...row];
|
||||
for (let i = 0; i < diff; i++) {
|
||||
row.push(null);
|
||||
}
|
||||
}
|
||||
fixedRows.push(row);
|
||||
});
|
||||
|
||||
return {
|
||||
columns,
|
||||
rows: fixedRows,
|
||||
type: table.type,
|
||||
columnMap: table.columnMap,
|
||||
};
|
||||
}
|
||||
|
||||
function makeColumns(values: any[]): Column[] {
|
||||
return values.map((value, index) => {
|
||||
if (!value) {
|
||||
value = 'Column ' + (index + 1);
|
||||
}
|
||||
return {
|
||||
text: value.toString().trim(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CSV text into a valid TableData object
|
||||
*
|
||||
* @param text
|
||||
* @param options
|
||||
* @param details, if exists the result will be filled with debugging details
|
||||
*/
|
||||
export function parseCSV(text: string, options?: TableParseOptions, details?: TableParseDetails): TableData {
|
||||
const results = Papa.parse(text, { ...options, dynamicTyping: true, skipEmptyLines: true });
|
||||
const { data, meta, errors } = results;
|
||||
|
||||
// Fill the parse details for debugging
|
||||
if (details) {
|
||||
details.errors = errors;
|
||||
details.meta = meta;
|
||||
}
|
||||
|
||||
if (!data || data.length < 1) {
|
||||
// Show a more reasonable warning on empty input text
|
||||
if (details && !text) {
|
||||
errors.length = 0;
|
||||
errors.push({
|
||||
code: 'empty',
|
||||
message: 'Empty input text',
|
||||
type: 'warning',
|
||||
row: 0,
|
||||
});
|
||||
details.errors = errors;
|
||||
}
|
||||
return {
|
||||
columns: [],
|
||||
rows: [],
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Assume the first line is the header unless the config says its not
|
||||
const headerIsNotFirstLine = options && options.headerIsFirstLine === false;
|
||||
const header = headerIsNotFirstLine ? [] : results.data.shift();
|
||||
|
||||
return matchRowSizes({
|
||||
columns: makeColumns(header),
|
||||
rows: results.data,
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
});
|
||||
}
|
||||
@@ -36,6 +36,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
|
||||
viewData.Settings["oauth"] = enabledOAuths
|
||||
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||
viewData.Settings["loginHint"] = setting.LoginHint
|
||||
viewData.Settings["passwordHint"] = setting.PasswordHint
|
||||
viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
|
||||
|
||||
if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {
|
||||
|
||||
@@ -56,7 +56,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
|
||||
|
||||
ctx := NewEvalContext(context.Background(), testRule)
|
||||
if cmd.Settings.Get("uploadImage").MustBool(true) {
|
||||
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
||||
ctx.ImagePublicUrl = "https://grafana.com/assets/img/blog/mixed_styles.png"
|
||||
}
|
||||
ctx.IsTestRun = true
|
||||
ctx.Firing = true
|
||||
|
||||
@@ -174,6 +174,11 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
|
||||
Version: cmd.Version + 1,
|
||||
}
|
||||
|
||||
sess.UseBool("is_default")
|
||||
sess.UseBool("basic_auth")
|
||||
sess.UseBool("with_credentials")
|
||||
sess.UseBool("read_only")
|
||||
|
||||
var updateSession *xorm.Session
|
||||
if cmd.Version != 0 {
|
||||
// the reason we allow cmd.version > db.version is make it possible for people to force
|
||||
@@ -185,7 +190,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
|
||||
updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId)
|
||||
}
|
||||
|
||||
affected, err := updateSession.AllCols().Omit("created").Update(ds)
|
||||
affected, err := updateSession.Update(ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ var (
|
||||
AutoAssignOrgRole string
|
||||
VerifyEmailEnabled bool
|
||||
LoginHint string
|
||||
PasswordHint string
|
||||
DefaultTheme string
|
||||
DisableLoginForm bool
|
||||
DisableSignoutMenu bool
|
||||
@@ -656,6 +657,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
|
||||
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
|
||||
LoginHint = users.Key("login_hint").String()
|
||||
PasswordHint = users.Key("password_hint").String()
|
||||
DefaultTheme = users.Key("default_theme").String()
|
||||
ExternalUserMngLinkUrl = users.Key("external_manage_link_url").String()
|
||||
ExternalUserMngLinkName = users.Key("external_manage_link_name").String()
|
||||
|
||||
@@ -496,9 +496,6 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
|
||||
}
|
||||
|
||||
alias := model.Get("alias").MustString()
|
||||
if alias == "" {
|
||||
alias = "{{metric}}_{{stat}}"
|
||||
}
|
||||
|
||||
returnData := model.Get("returnData").MustBool(false)
|
||||
highResolution := model.Get("highResolution").MustBool(false)
|
||||
@@ -521,7 +518,11 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
|
||||
|
||||
func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string {
|
||||
if len(query.Id) > 0 && len(query.Expression) > 0 {
|
||||
return query.Id
|
||||
if len(query.Alias) > 0 {
|
||||
return query.Alias
|
||||
} else {
|
||||
return query.Id
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
|
||||
@@ -279,7 +279,7 @@ func formatDate(t time.Time, pattern string) string {
|
||||
isoYearShort := fmt.Sprintf("%d", isoYear)[2:4]
|
||||
formatted = strings.Replace(formatted, "<stdIsoYear>", fmt.Sprintf("%d", isoYear), -1)
|
||||
formatted = strings.Replace(formatted, "<stdIsoYearShort>", isoYearShort, -1)
|
||||
formatted = strings.Replace(formatted, "<stdWeekOfYear>", fmt.Sprintf("%d", isoWeek), -1)
|
||||
formatted = strings.Replace(formatted, "<stdWeekOfYear>", fmt.Sprintf("%02d", isoWeek), -1)
|
||||
|
||||
formatted = strings.Replace(formatted, "<stdUnix>", fmt.Sprintf("%d", t.Unix()), -1)
|
||||
|
||||
|
||||
@@ -76,6 +76,15 @@ func TestIndexPattern(t *testing.T) {
|
||||
So(indices, ShouldHaveLength, 1)
|
||||
So(indices[0], ShouldEqual, "2018-data")
|
||||
})
|
||||
|
||||
Convey("Should return 01 week", func() {
|
||||
from = fmt.Sprintf("%d", time.Date(2018, 1, 15, 17, 50, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond))
|
||||
to = fmt.Sprintf("%d", time.Date(2018, 1, 15, 17, 55, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond))
|
||||
indexPatternScenario(intervalWeekly, "[data-]GGGG.WW", tsdb.NewTimeRange(from, to), func(indices []string) {
|
||||
So(indices, ShouldHaveLength, 1)
|
||||
So(indices[0], ShouldEqual, "data-2018.03")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Hourly interval", t, func() {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ToggleButton: FC<ToggleButtonProps> = ({
|
||||
tooltip,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleChange = event => {
|
||||
const onClick = event => {
|
||||
event.stopPropagation();
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
@@ -46,7 +46,7 @@ export const ToggleButton: FC<ToggleButtonProps> = ({
|
||||
|
||||
const btnClassName = `btn ${className} ${selected ? 'active' : ''}`;
|
||||
const button = (
|
||||
<button className={btnClassName} onClick={handleChange}>
|
||||
<button className={btnClassName} onClick={onClick}>
|
||||
<span>{children}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { SideMenu } from './SideMenu';
|
||||
import appEvents from '../../app_events';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
jest.mock('../../app_events', () => ({
|
||||
emit: jest.fn(),
|
||||
@@ -26,7 +25,6 @@ jest.mock('app/core/services/context_srv', () => ({
|
||||
isGrafanaAdmin: false,
|
||||
isEditor: false,
|
||||
hasEditPermissionFolders: false,
|
||||
toggleSideMenu: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -54,20 +52,6 @@ describe('Render', () => {
|
||||
});
|
||||
|
||||
describe('Functions', () => {
|
||||
describe('toggle side menu', () => {
|
||||
const wrapper = setup();
|
||||
const instance = wrapper.instance() as SideMenu;
|
||||
instance.toggleSideMenu();
|
||||
|
||||
it('should call contextSrv.toggleSideMenu', () => {
|
||||
expect(contextSrv.toggleSideMenu).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit toggle sidemenu event', () => {
|
||||
expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle side menu on mobile', () => {
|
||||
const wrapper = setup();
|
||||
const instance = wrapper.instance() as SideMenu;
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import appEvents from '../../app_events';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import TopSection from './TopSection';
|
||||
import BottomSection from './BottomSection';
|
||||
import { store } from 'app/store/store';
|
||||
import config from 'app/core/config';
|
||||
|
||||
const homeUrl = config.appSubUrl || '/';
|
||||
|
||||
export class SideMenu extends PureComponent {
|
||||
toggleSideMenu = () => {
|
||||
// ignore if we just made a location change, stops hiding sidemenu on double clicks of back button
|
||||
const timeSinceLocationChanged = new Date().getTime() - store.getState().location.lastUpdated;
|
||||
if (timeSinceLocationChanged < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
contextSrv.toggleSideMenu();
|
||||
appEvents.emit('toggle-sidemenu');
|
||||
};
|
||||
|
||||
toggleSideMenuSmallBreakpoint = () => {
|
||||
appEvents.emit('toggle-sidemenu-mobile');
|
||||
};
|
||||
|
||||
render() {
|
||||
return [
|
||||
<div className="sidemenu__logo" onClick={this.toggleSideMenu} key="logo">
|
||||
<a href={homeUrl} className="sidemenu__logo" key="logo">
|
||||
<img src="public/img/grafana_icon.svg" alt="Grafana" />
|
||||
</div>,
|
||||
</a>,
|
||||
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
|
||||
<i className="fa fa-bars" />
|
||||
<span className="sidemenu__close">
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
Array [
|
||||
<div
|
||||
<a
|
||||
className="sidemenu__logo"
|
||||
href="/"
|
||||
key="logo"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<img
|
||||
alt="Grafana"
|
||||
src="public/img/grafana_icon.svg"
|
||||
/>
|
||||
</div>,
|
||||
</a>,
|
||||
<div
|
||||
className="sidemenu__logo_small_breakpoint"
|
||||
key="hamburger"
|
||||
|
||||
@@ -34,6 +34,7 @@ export class Settings {
|
||||
oauth: any;
|
||||
disableUserSignUp: boolean;
|
||||
loginHint: any;
|
||||
passwordHint: any;
|
||||
loginError: any;
|
||||
viewersCanEdit: boolean;
|
||||
editorsCanOwn: boolean;
|
||||
|
||||
@@ -25,6 +25,7 @@ export class LoginCtrl {
|
||||
$scope.disableLoginForm = config.disableLoginForm;
|
||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
||||
$scope.loginHint = config.loginHint;
|
||||
$scope.passwordHint = config.passwordHint;
|
||||
|
||||
$scope.loginMode = true;
|
||||
$scope.submitBtnText = 'Log in';
|
||||
|
||||
@@ -245,12 +245,13 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs
|
||||
}
|
||||
|
||||
const dedupedRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => {
|
||||
const rowCopy = { ...row };
|
||||
const previous = result[result.length - 1];
|
||||
if (index > 0 && isDuplicateRow(row, previous, strategy)) {
|
||||
previous.duplicates++;
|
||||
} else {
|
||||
row.duplicates = 0;
|
||||
result.push(row);
|
||||
rowCopy.duplicates = 0;
|
||||
result.push(rowCopy);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import store from 'app/core/store';
|
||||
|
||||
export class User {
|
||||
isGrafanaAdmin: any;
|
||||
@@ -29,13 +28,10 @@ export class ContextSrv {
|
||||
isSignedIn: any;
|
||||
isGrafanaAdmin: any;
|
||||
isEditor: any;
|
||||
sidemenu: any;
|
||||
sidemenuSmallBreakpoint = false;
|
||||
hasEditPermissionInFolders: boolean;
|
||||
|
||||
constructor() {
|
||||
this.sidemenu = store.getBool('grafana.sidemenu', true);
|
||||
|
||||
if (!config.bootData) {
|
||||
config.bootData = { user: {}, settings: {} };
|
||||
}
|
||||
@@ -55,11 +51,6 @@ export class ContextSrv {
|
||||
return !!(document.visibilityState === undefined || document.visibilityState === 'visible');
|
||||
}
|
||||
|
||||
toggleSideMenu() {
|
||||
this.sidemenu = !this.sidemenu;
|
||||
store.set('grafana.sidemenu', this.sidemenu);
|
||||
}
|
||||
|
||||
hasAccessToExplore() {
|
||||
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
||||
}
|
||||
|
||||
@@ -113,6 +113,34 @@ describe('dedupLogRows()', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return to non-deduped state on same log result', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'INFO 123',
|
||||
},
|
||||
{
|
||||
entry: 'WARN 123',
|
||||
},
|
||||
{
|
||||
entry: 'WARN 123',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'INFO 123',
|
||||
},
|
||||
{
|
||||
duplicates: 1,
|
||||
entry: 'WARN 123',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toEqual(logs.rows);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateFieldStats()', () => {
|
||||
|
||||
@@ -13,8 +13,19 @@ interface Props {
|
||||
export class LogLabels extends PureComponent<Props> {
|
||||
render() {
|
||||
const { getRows, labels, onClickLabel, plain } = this.props;
|
||||
return Object.keys(labels).map(key => (
|
||||
<LogLabel key={key} getRows={getRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} />
|
||||
));
|
||||
return (
|
||||
<span className="logs-labels">
|
||||
{Object.keys(labels).map(key => (
|
||||
<LogLabel
|
||||
key={key}
|
||||
getRows={getRows}
|
||||
label={key}
|
||||
value={labels[key]}
|
||||
plain={plain}
|
||||
onClickLabel={onClickLabel}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ export class LogRow extends PureComponent<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
{showLocalTime && (
|
||||
<div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
|
||||
<div className="logs-row__localtime" title={`${row.timestamp} (${row.timeFromNow})`}>
|
||||
{row.timeLocal}
|
||||
</div>
|
||||
)}
|
||||
|
||||
108
public/app/features/explore/state/selectors.test.ts
Normal file
108
public/app/features/explore/state/selectors.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { deduplicatedLogsSelector } from './selectors';
|
||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||
import { ExploreItemState } from 'app/types';
|
||||
|
||||
const state = {
|
||||
logsResult: {
|
||||
rows: [
|
||||
{
|
||||
entry: '2019-03-05T11:00:56Z sntpc sntpc[1]: offset=-0.033938, delay=0.000649',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T11:00:26Z sntpc sntpc[1]: offset=-0.033730, delay=0.000581',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T10:59:56Z sntpc sntpc[1]: offset=-0.034184, delay=0.001089',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T10:59:26Z sntpc sntpc[1]: offset=-0.033972, delay=0.000582',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T10:58:56Z sntpc sntpc[1]: offset=-0.033955, delay=0.000606',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T10:58:26Z sntpc sntpc[1]: offset=-0.034067, delay=0.000616',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T10:57:56Z sntpc sntpc[1]: offset=-0.034155, delay=0.001021',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T10:57:26Z sntpc sntpc[1]: offset=-0.035797, delay=0.000883',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T10:56:56Z sntpc sntpc[1]: offset=-0.046818, delay=0.000605',
|
||||
},
|
||||
{
|
||||
entry: '2019-03-05T10:56:26Z sntpc sntpc[1]: offset=-0.049200, delay=0.000584',
|
||||
},
|
||||
],
|
||||
},
|
||||
hiddenLogLevels: undefined,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
};
|
||||
|
||||
describe('Deduplication selector', () => {
|
||||
it('should correctly deduplicate log rows when changing strategy multiple times', () => {
|
||||
// Simulating sequence of UI actions that was causing a problem with deduplication counter being visible when unnecessary.
|
||||
// The sequence was changing dedup strategy: (none -> exact -> numbers -> signature -> none) *2 -> exact. After that the first
|
||||
// row contained information that was deduped, while it shouldn't be.
|
||||
// Problem was caused by mutating the log results entries in redux state. The memoisation hash for deduplicatedLogsSelector
|
||||
// was changing depending on duplicates information from log row state, while should be dependand on log row only.
|
||||
|
||||
let dedups = deduplicatedLogsSelector(state as ExploreItemState);
|
||||
expect(dedups.rows.length).toBe(10);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
} as ExploreItemState);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.exact,
|
||||
} as ExploreItemState);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.numbers,
|
||||
} as ExploreItemState);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.signature,
|
||||
} as ExploreItemState);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
} as ExploreItemState);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.exact,
|
||||
} as ExploreItemState);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.numbers,
|
||||
} as ExploreItemState);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.signature,
|
||||
} as ExploreItemState);
|
||||
|
||||
deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
} as ExploreItemState);
|
||||
|
||||
dedups = deduplicatedLogsSelector({
|
||||
...state,
|
||||
dedupStrategy: LogsDedupStrategy.exact,
|
||||
} as ExploreItemState);
|
||||
|
||||
// Expecting that no row has duplicates now
|
||||
expect(dedups.rows.reduce((acc, row) => acc + row.duplicates, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<input type="password" name="password" class="gf-form-input login-form-input" required ng-model="formModel.password" id="inputPassword"
|
||||
placeholder="password">
|
||||
placeholder="{{passwordHint}}">
|
||||
</div>
|
||||
<div class="login-button-group">
|
||||
<button type="submit" class="btn btn-large p-x-2" ng-if="!loggingIn" ng-click="submit();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">
|
||||
|
||||
@@ -75,27 +75,22 @@ export class GrafanaCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
function setViewModeBodyClass(body: JQuery, mode: KioskUrlValue, sidemenuOpen: boolean) {
|
||||
function setViewModeBodyClass(body: JQuery, mode: KioskUrlValue) {
|
||||
body.removeClass('view-mode--tv');
|
||||
body.removeClass('view-mode--kiosk');
|
||||
body.removeClass('view-mode--inactive');
|
||||
|
||||
switch (mode) {
|
||||
case 'tv': {
|
||||
body.removeClass('sidemenu-open');
|
||||
body.addClass('view-mode--tv');
|
||||
break;
|
||||
}
|
||||
// 1 & true for legacy states
|
||||
case '1':
|
||||
case true: {
|
||||
body.removeClass('sidemenu-open');
|
||||
body.addClass('view-mode--kiosk');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
body.toggleClass('sidemenu-open', sidemenuOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +100,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
restrict: 'E',
|
||||
controller: GrafanaCtrl,
|
||||
link: (scope, elem) => {
|
||||
let sidemenuOpen;
|
||||
const body = $('body');
|
||||
|
||||
// see https://github.com/zenorocha/clipboard.js/issues/155
|
||||
@@ -113,14 +107,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
|
||||
$('.preloader').remove();
|
||||
|
||||
sidemenuOpen = scope.contextSrv.sidemenu;
|
||||
body.toggleClass('sidemenu-open', sidemenuOpen);
|
||||
|
||||
appEvents.on('toggle-sidemenu', () => {
|
||||
sidemenuOpen = scope.contextSrv.sidemenu;
|
||||
body.toggleClass('sidemenu-open');
|
||||
});
|
||||
|
||||
appEvents.on('toggle-sidemenu-mobile', () => {
|
||||
body.toggleClass('sidemenu-open--xs');
|
||||
});
|
||||
@@ -163,7 +149,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
$('#tooltip, .tooltip').remove();
|
||||
|
||||
// check for kiosk url param
|
||||
setViewModeBodyClass(body, data.params.kiosk, sidemenuOpen);
|
||||
setViewModeBodyClass(body, data.params.kiosk);
|
||||
|
||||
// close all drops
|
||||
for (const drop of Drop.drops) {
|
||||
@@ -198,7 +184,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
}
|
||||
|
||||
$timeout(() => $location.search(search));
|
||||
setViewModeBodyClass(body, search.kiosk, sidemenuOpen);
|
||||
setViewModeBodyClass(body, search.kiosk);
|
||||
});
|
||||
|
||||
// handle in active view state class
|
||||
@@ -218,7 +204,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
if (new Date().getTime() - lastActivity > inActiveTimeLimit) {
|
||||
activeUser = false;
|
||||
body.addClass('view-mode--inactive');
|
||||
body.removeClass('sidemenu-open');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +212,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
if (!activeUser) {
|
||||
activeUser = true;
|
||||
body.removeClass('view-mode--inactive');
|
||||
body.toggleClass('sidemenu-open', sidemenuOpen);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ $enable-hover-media-query: false !default;
|
||||
// Control the default styling of most Bootstrap elements by modifying these
|
||||
// variables. Mostly focused on spacing.
|
||||
|
||||
$spacer: 1rem !default;
|
||||
$spacer: 14px !default;
|
||||
$spacer-x: $spacer !default;
|
||||
$spacer-y: $spacer !default;
|
||||
$spacers: (
|
||||
@@ -88,7 +88,6 @@ $enable-flex: true;
|
||||
// -------------------------
|
||||
|
||||
$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
$font-family-serif: Georgia, 'Times New Roman', Times, serif;
|
||||
$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
$font-family-base: $font-family-sans-serif !default;
|
||||
|
||||
@@ -103,25 +102,12 @@ $font-size-xs: 10px !default;
|
||||
$line-height-base: 1.5 !default;
|
||||
$font-weight-semi-bold: 500;
|
||||
|
||||
$font-size-h1: 2rem !default;
|
||||
$font-size-h2: 1.75rem !default;
|
||||
$font-size-h3: 1.5rem !default;
|
||||
$font-size-h4: 1.3rem !default;
|
||||
$font-size-h5: 1.2rem !default;
|
||||
$font-size-h6: 1rem !default;
|
||||
|
||||
$display1-size: 6rem !default;
|
||||
$display2-size: 5.5rem !default;
|
||||
$display3-size: 4.5rem !default;
|
||||
$display4-size: 3.5rem !default;
|
||||
|
||||
$display1-weight: 400 !default;
|
||||
$display2-weight: 400 !default;
|
||||
$display3-weight: 400 !default;
|
||||
$display4-weight: 400 !default;
|
||||
|
||||
$lead-font-size: 1.25rem !default;
|
||||
$lead-font-weight: 300 !default;
|
||||
$font-size-h1: 28px !default;
|
||||
$font-size-h2: 24px !default;
|
||||
$font-size-h3: 21px !default;
|
||||
$font-size-h4: 18px !default;
|
||||
$font-size-h5: 16px !default;
|
||||
$font-size-h6: 14px !default;
|
||||
|
||||
$headings-margin-bottom: ($spacer / 2) !default;
|
||||
$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
@@ -144,8 +130,8 @@ $border-radius-sm: 2px !default;
|
||||
|
||||
// Page
|
||||
|
||||
$page-sidebar-width: 11rem;
|
||||
$page-sidebar-margin: 4rem;
|
||||
$page-sidebar-width: 154px;
|
||||
$page-sidebar-margin: 56px;
|
||||
|
||||
// Links
|
||||
// -------------------------
|
||||
@@ -177,7 +163,7 @@ $input-padding-y-lg: 10px !default;
|
||||
|
||||
$input-height: 35px !default;
|
||||
|
||||
$gf-form-margin: 0.2rem;
|
||||
$gf-form-margin: 3px;
|
||||
$gf-form-input-height: 35px;
|
||||
|
||||
$cursor-disabled: not-allowed !default;
|
||||
@@ -202,13 +188,13 @@ $zindex-typeahead: 1060;
|
||||
// Buttons
|
||||
//
|
||||
|
||||
$btn-padding-x: 1rem !default;
|
||||
$btn-padding-y: 0.7rem !default;
|
||||
$btn-padding-x: 14px !default;
|
||||
$btn-padding-y: 10px !default;
|
||||
$btn-line-height: 1 !default;
|
||||
$btn-font-weight: 500 !default;
|
||||
|
||||
$btn-padding-x-sm: 0.5rem !default;
|
||||
$btn-padding-y-sm: 0.25rem !default;
|
||||
$btn-padding-x-sm: 7px !default;
|
||||
$btn-padding-y-sm: 4px !default;
|
||||
|
||||
$btn-padding-x-lg: 21px !default;
|
||||
$btn-padding-y-lg: 11px !default;
|
||||
|
||||
@@ -141,29 +141,6 @@ h6,
|
||||
font-size: $font-size-h6;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: $lead-font-size;
|
||||
font-weight: $lead-font-weight;
|
||||
}
|
||||
|
||||
// Type display classes
|
||||
.display-1 {
|
||||
font-size: $display1-size;
|
||||
font-weight: $display1-weight;
|
||||
}
|
||||
.display-2 {
|
||||
font-size: $display2-size;
|
||||
font-weight: $display2-weight;
|
||||
}
|
||||
.display-3 {
|
||||
font-size: $display3-size;
|
||||
font-weight: $display3-weight;
|
||||
}
|
||||
.display-4 {
|
||||
font-size: $display4-size;
|
||||
font-weight: $display4-weight;
|
||||
}
|
||||
|
||||
//
|
||||
// Horizontal rules
|
||||
//
|
||||
|
||||
@@ -157,14 +157,8 @@
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.navbar {
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.sidemenu-open {
|
||||
.navbar {
|
||||
padding-left: 25px;
|
||||
margin-left: 0;
|
||||
}
|
||||
padding-left: 20px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.navbar-page-btn {
|
||||
|
||||
@@ -63,6 +63,7 @@ $column-horizontal-spacing: 10px;
|
||||
font-size: $font-size-sm;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logs-row {
|
||||
@@ -83,16 +84,22 @@ $column-horizontal-spacing: 10px;
|
||||
|
||||
.logs-row__time {
|
||||
white-space: nowrap;
|
||||
width: 19em;
|
||||
}
|
||||
|
||||
.logs-row__localtime {
|
||||
white-space: nowrap;
|
||||
width: 12.5em;
|
||||
}
|
||||
|
||||
.logs-row__labels {
|
||||
max-width: 20%;
|
||||
width: 20%;
|
||||
line-height: 1.2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logs-row__message {
|
||||
word-break: break-all;
|
||||
min-width: 80%;
|
||||
}
|
||||
|
||||
.logs-row__match-highlight {
|
||||
@@ -112,6 +119,7 @@ $column-horizontal-spacing: 10px;
|
||||
|
||||
.logs-row__level {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
@@ -165,6 +173,7 @@ $column-horizontal-spacing: 10px;
|
||||
|
||||
.logs-row__duplicates {
|
||||
text-align: right;
|
||||
width: 4.5em;
|
||||
}
|
||||
|
||||
.logs-row__field-highlight {
|
||||
@@ -193,15 +202,20 @@ $column-horizontal-spacing: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-labels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logs-label {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
padding: 0 2px;
|
||||
background-color: $btn-inverse-bg;
|
||||
border-radius: $border-radius;
|
||||
margin: 0 4px 2px 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs-label__icon {
|
||||
@@ -211,6 +225,13 @@ $column-horizontal-spacing: 10px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.logs-label__value {
|
||||
display: inline-block;
|
||||
max-width: 20em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs-label__stats {
|
||||
position: absolute;
|
||||
top: 1.25em;
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
.sidemenu__close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
background: $side-menu-bg;
|
||||
height: auto;
|
||||
box-shadow: $side-menu-shadow;
|
||||
position: relative;
|
||||
z-index: $zindex-sidemenu;
|
||||
}
|
||||
}
|
||||
|
||||
// body class that hides sidemenu
|
||||
@@ -25,32 +33,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.sidemenu-open {
|
||||
.sidemenu {
|
||||
background: $side-menu-bg;
|
||||
height: auto;
|
||||
box-shadow: $side-menu-shadow;
|
||||
position: relative;
|
||||
z-index: $zindex-sidemenu;
|
||||
}
|
||||
|
||||
.sidemenu__top,
|
||||
.sidemenu__bottom {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidemenu__top {
|
||||
padding-top: 3rem;
|
||||
flex-grow: 1;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidemenu__bottom {
|
||||
padding-bottom: $spacer;
|
||||
}
|
||||
|
||||
.sidemenu__top,
|
||||
.sidemenu__bottom {
|
||||
display: none;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sidemenu-item {
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
.view-mode--tv {
|
||||
@extend .view-mode--inactive;
|
||||
|
||||
.sidemenu {
|
||||
position: fixed;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
.sidemenu__top,
|
||||
.sidemenu__bottom {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding-left: $side-menu-width;
|
||||
}
|
||||
|
||||
.submenu-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -79,10 +79,6 @@
|
||||
// FONTS
|
||||
// --------------------------------------------------
|
||||
|
||||
@mixin font-family-serif() {
|
||||
font-family: $font-family-serif;
|
||||
}
|
||||
|
||||
@mixin font-family-sans-serif() {
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
@@ -97,11 +93,6 @@
|
||||
line-height: $lineHeight;
|
||||
}
|
||||
|
||||
@mixin font-serif($size: $font-size-base, $weight: normal, $lineHeight: $line-height-base) {
|
||||
@include font-family-serif();
|
||||
@include font-shorthand($size, $weight, $lineHeight);
|
||||
}
|
||||
|
||||
@mixin font-sans-serif($size: $font-size-base, $weight: normal, $lineHeight: $line-height-base) {
|
||||
@include font-family-sans-serif();
|
||||
@include font-shorthand($size, $weight, $lineHeight);
|
||||
|
||||
@@ -25,20 +25,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sidemenu-open {
|
||||
.explore-toolbar-header {
|
||||
padding: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.explore-toolbar {
|
||||
background: inherit;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
height: auto;
|
||||
padding: 0px $dashboard-padding 0 25px;
|
||||
padding: 0 $dashboard-padding;
|
||||
border-bottom: 1px solid #0000;
|
||||
transition-duration: 0.35s;
|
||||
transition-timing-function: ease-in-out;
|
||||
@@ -72,11 +65,6 @@
|
||||
font-size: 18px;
|
||||
min-height: 55px;
|
||||
line-height: 55px;
|
||||
justify-content: space-between;
|
||||
margin-left: $panel-margin * 3;
|
||||
}
|
||||
|
||||
.explore-toolbar-header {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -134,20 +122,6 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 803px) {
|
||||
.sidemenu-open {
|
||||
.explore-toolbar-header-title {
|
||||
.navbar-page-btn {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.explore-toolbar-header-title {
|
||||
.navbar-page-btn {
|
||||
margin-left: $dashboard-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
display: none;
|
||||
}
|
||||
@@ -161,14 +135,6 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 544px) {
|
||||
.sidemenu-open {
|
||||
.explore-toolbar-header-title {
|
||||
.navbar-page-btn {
|
||||
margin-left: $dashboard-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.explore-toolbar-header-title {
|
||||
.navbar-page-btn {
|
||||
margin-left: $dashboard-padding;
|
||||
|
||||
@@ -4,7 +4,7 @@ type FnToSpin<T> = (options: T) => Promise<void>;
|
||||
|
||||
export const useSpinner = <T>(spinnerLabel: string, fn: FnToSpin<T>, killProcess = true) => {
|
||||
return async (options: T) => {
|
||||
const spinner = new ora(spinnerLabel);
|
||||
const spinner = ora(spinnerLabel);
|
||||
spinner.start();
|
||||
try {
|
||||
await fn(options);
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -1801,6 +1801,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.40.tgz#4314888d5cd537945d73e9ce165c04cc550144a4"
|
||||
integrity sha512-RRSjdwz63kS4u7edIwJUn8NqKLLQ6LyqF/X4+4jp38MBT3Vwetewi2N4dgJEshLbDwNgOJXNYoOwzVZUSSLhkQ==
|
||||
|
||||
"@types/papaparse@^4.5.9":
|
||||
version "4.5.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-4.5.9.tgz#ff887bd362f57cd0c87320d2de38ac232bb55e81"
|
||||
integrity sha512-8Pmxp2IEd/y58tOIsiZkCbAkcKI7InYVpwZFVKJyweCVnqnVahKXVjfSo6gvxUVykQsJvtWB+s6Kc60znVfQVw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce"
|
||||
@@ -12993,6 +13000,11 @@ pako@~1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.8.tgz#6844890aab9c635af868ad5fecc62e8acbba3ea4"
|
||||
integrity sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==
|
||||
|
||||
papaparse@^4.6.3:
|
||||
version "4.6.3"
|
||||
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-4.6.3.tgz#742e5eaaa97fa6c7e1358d2934d8f18f44aee781"
|
||||
integrity sha512-LRq7BrHC2kHPBYSD50aKuw/B/dGcg29omyJbKWY3KsYUZU69RKwaBHu13jGmCYBtOc4odsLCrFyk6imfyNubJQ==
|
||||
|
||||
parallel-transform@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"
|
||||
|
||||
Reference in New Issue
Block a user