merge master

This commit is contained in:
ryan
2019-03-11 21:16:51 -07:00
50 changed files with 744 additions and 277 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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",

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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()}

View File

@@ -53,7 +53,7 @@
}
.panel-options-group__title {
font-size: 1.1rem;
font-size: 16px;
position: relative;
top: 1px;
}

View File

@@ -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>
);
});

View File

@@ -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();
});
});

View 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} &nbsp;
{hasErrors ? <i className="fa fa-exclamation-triangle" /> : <i className="fa fa-check-circle" />}
</footer>
</div>
)}
</AutoSizer>
);
}
}
export default TableInputCSV;

View 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;
}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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',
},
},
};

View File

@@ -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;
};
};
}

View File

@@ -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",
}
`;

View 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();
});
});
});

View 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: {},
});
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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{}

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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">

View File

@@ -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"

View File

@@ -34,6 +34,7 @@ export class Settings {
oauth: any;
disableUserSignUp: boolean;
loginHint: any;
passwordHint: any;
loginError: any;
viewersCanEdit: boolean;
editorsCanOwn: boolean;

View File

@@ -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';

View File

@@ -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;
}, []);

View File

@@ -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;
}

View File

@@ -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()', () => {

View File

@@ -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>
);
}
}

View File

@@ -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>
)}

View 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);
});
});

View File

@@ -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}">

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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
//

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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"