mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into grafana-ui/select
This commit is contained in:
@@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
build-all:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.1
|
||||
- image: grafana/build-container:1.2.2
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -200,51 +200,51 @@ jobs:
|
||||
- dist/grafana*
|
||||
|
||||
grafana-docker-master:
|
||||
docker:
|
||||
- image: docker:stable-git
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- setup_remote_docker
|
||||
- run: docker info
|
||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||
- run: docker run --privileged linuxkit/binfmt:v0.6
|
||||
- run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker
|
||||
- run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
|
||||
- run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||
- run: rm packaging/docker/grafana-latest.linux-*.tar.gz
|
||||
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||
- run: cd packaging/docker && ./build-enterprise.sh "master"
|
||||
|
||||
|
||||
grafana-docker-pr:
|
||||
docker:
|
||||
- image: docker:stable-git
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- setup_remote_docker
|
||||
- run: docker info
|
||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||
- run: docker run --privileged linuxkit/binfmt:v0.6
|
||||
- run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker
|
||||
- run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
|
||||
|
||||
grafana-docker-release:
|
||||
docker:
|
||||
- image: docker:stable-git
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- setup_remote_docker
|
||||
- run: docker info
|
||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||
- run: docker run --privileged linuxkit/binfmt:v0.6
|
||||
- run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker
|
||||
- run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
|
||||
- run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||
- run: rm packaging/docker/grafana-latest.linux-*.tar.gz
|
||||
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||
- run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
|
||||
|
||||
build-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.1
|
||||
- image: grafana/build-container:1.2.2
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
|
||||
build-all-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.1
|
||||
- image: grafana/build-container:1.2.2
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
|
||||
* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
|
||||
* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
|
||||
* **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach)
|
||||
|
||||
### Bug fixes
|
||||
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
||||
|
||||
2
build.go
2
build.go
@@ -164,6 +164,8 @@ func makeLatestDistCopies() {
|
||||
"_amd64.deb": "dist/grafana_latest_amd64.deb",
|
||||
".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm",
|
||||
".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
|
||||
".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz",
|
||||
".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz",
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
|
||||
@@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
|
||||
"list": []
|
||||
},
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 16,
|
||||
"schemaVersion": 17,
|
||||
"version": 0,
|
||||
"links": []
|
||||
}
|
||||
|
||||
@@ -292,9 +292,11 @@ The `direction` controls how the panels will be arranged.
|
||||
|
||||
By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width
|
||||
of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated
|
||||
panel. Each panel will never be smaller that the provided `Min width` if you have many selected values.
|
||||
panel.
|
||||
|
||||
By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
|
||||
Set `Max per row` to tell grafana how many panels per row you want at most. It defaults to *4* if you don't set anything.
|
||||
|
||||
By choosing `vertical` the panels will be arranged from top to bottom in a column. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
|
||||
|
||||
Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
|
||||
You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "^1.3.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"react-virtualized": "^9.21.0"
|
||||
"react-virtualized": "^9.21.0",
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||
"tinycolor2": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/classnames": "^2.2.6",
|
||||
@@ -33,6 +36,8 @@
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-test-renderer": "^16.0.3",
|
||||
"@types/tether-drop": "^1.4.8",
|
||||
"@types/tinycolor2": "^1.4.1",
|
||||
"react-test-renderer": "^16.7.0",
|
||||
"typescript": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { ColorPalette } from '../components/colorpicker/ColorPalette';
|
||||
import { ColorPalette } from './ColorPalette';
|
||||
|
||||
describe('CollorPalette', () => {
|
||||
it('renders correctly', () => {
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { sortedColors } from 'app/core/utils/colors';
|
||||
import { sortedColors } from '../../utils';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
@@ -9,13 +9,13 @@ export interface Props {
|
||||
export class ColorPalette extends React.Component<Props, any> {
|
||||
paletteColors: string[];
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.paletteColors = sortedColors;
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
onColorSelect(color) {
|
||||
onColorSelect(color: string) {
|
||||
return () => {
|
||||
this.props.onColorSelect(color);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Drop from 'tether-drop';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
@@ -10,7 +9,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
export class ColorPicker extends React.Component<Props, any> {
|
||||
pickerElem: HTMLElement;
|
||||
pickerElem: HTMLElement | null;
|
||||
colorPickerDrop: any;
|
||||
|
||||
openColorPicker = () => {
|
||||
@@ -20,7 +19,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
const drop = new Drop({
|
||||
target: this.pickerElem,
|
||||
target: this.pickerElem as Element,
|
||||
content: dropContentElem,
|
||||
position: 'top center',
|
||||
classes: 'drop-popover',
|
||||
@@ -28,6 +27,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
||||
hoverCloseDelay: 200,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||
attachment: 'bottom center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
onColorSelect = color => {
|
||||
onColorSelect = (color: string) => {
|
||||
this.props.onChange(color);
|
||||
};
|
||||
|
||||
@@ -59,8 +59,3 @@ export class ColorPicker extends React.Component<Props, any> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
react2AngularDirective('colorPicker', ColorPicker, [
|
||||
'color',
|
||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
@@ -14,7 +14,7 @@ export interface Props {
|
||||
export class ColorPickerPopover extends React.Component<Props, any> {
|
||||
pickerNavElem: any;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 'palette',
|
||||
@@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component<Props, any> {
|
||||
};
|
||||
}
|
||||
|
||||
setPickerNavElem(elem) {
|
||||
setPickerNavElem(elem: any) {
|
||||
this.pickerNavElem = $(elem);
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
setColor(color: string) {
|
||||
const newColor = tinycolor(color);
|
||||
if (newColor.isValid()) {
|
||||
this.setState({
|
||||
color: newColor.toString(),
|
||||
colorString: newColor.toString(),
|
||||
});
|
||||
this.setState({ color: newColor.toString(), colorString: newColor.toString() });
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
}
|
||||
|
||||
sampleColorSelected(color) {
|
||||
sampleColorSelected(color: string) {
|
||||
this.setColor(color);
|
||||
}
|
||||
|
||||
spectrumColorSelected(color) {
|
||||
spectrumColorSelected(color: any) {
|
||||
const rgbColor = color.toRgbString();
|
||||
this.setColor(rgbColor);
|
||||
}
|
||||
|
||||
onColorStringChange(e) {
|
||||
onColorStringChange(e: any) {
|
||||
const colorString = e.target.value;
|
||||
this.setState({
|
||||
colorString: colorString,
|
||||
});
|
||||
this.setState({ colorString: colorString });
|
||||
|
||||
const newColor = tinycolor(colorString);
|
||||
if (newColor.isValid()) {
|
||||
// Update only color state
|
||||
const newColorString = newColor.toString();
|
||||
this.setState({
|
||||
color: newColorString,
|
||||
});
|
||||
this.setState({ color: newColorString });
|
||||
this.props.onColorSelect(newColorString);
|
||||
}
|
||||
}
|
||||
|
||||
onColorStringBlur(e) {
|
||||
onColorStringBlur(e: any) {
|
||||
const colorString = e.target.value;
|
||||
this.setColor(colorString);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.pickerNavElem.find('li:first').addClass('active');
|
||||
this.pickerNavElem.on('show', e => {
|
||||
this.pickerNavElem.on('show', (e: any) => {
|
||||
// use href attr (#name => name)
|
||||
const tab = e.target.hash.slice(1);
|
||||
this.setState({
|
||||
tab: tab,
|
||||
});
|
||||
this.setState({ tab: tab });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
||||
onToggleAxis: () => {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: SeriesColorPickerProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
||||
remove: true,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||
attachment: 'bottom center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
|
||||
export interface SeriesColorPickerPopoverProps {
|
||||
color: string;
|
||||
@@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPic
|
||||
|
||||
interface AxisSelectorProps {
|
||||
yaxis: number;
|
||||
onToggleAxis: () => void;
|
||||
onToggleAxis?: () => void;
|
||||
}
|
||||
|
||||
interface AxisSelectorState {
|
||||
@@ -30,7 +29,7 @@ interface AxisSelectorState {
|
||||
}
|
||||
|
||||
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
|
||||
constructor(props) {
|
||||
constructor(props: AxisSelectorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
yaxis: this.props.yaxis,
|
||||
@@ -42,8 +41,11 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
||||
this.setState({
|
||||
yaxis: this.state.yaxis === 2 ? 1 : 2,
|
||||
});
|
||||
|
||||
if (this.props.onToggleAxis) {
|
||||
this.props.onToggleAxis();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
|
||||
@@ -62,9 +64,3 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
|
||||
'series',
|
||||
'onColorChange',
|
||||
'onToggleAxis',
|
||||
]);
|
||||
@@ -13,17 +13,17 @@ export class SpectrumPicker extends React.Component<Props, any> {
|
||||
elem: any;
|
||||
isMoving: boolean;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
||||
this.setComponentElem = this.setComponentElem.bind(this);
|
||||
}
|
||||
|
||||
setComponentElem(elem) {
|
||||
setComponentElem(elem: any) {
|
||||
this.elem = $(elem);
|
||||
}
|
||||
|
||||
onSpectrumMove(color) {
|
||||
onSpectrumMove(color: any) {
|
||||
this.isMoving = true;
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component<Props, any> {
|
||||
this.elem.spectrum('set', this.props.color);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
componentWillUpdate(nextProps: any) {
|
||||
// If user move pointer over spectrum field this produce 'move' event and component
|
||||
// may update props.color. We don't want to update spectrum color in this case, so we can use
|
||||
// isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { SFC } from 'react';
|
||||
|
||||
interface LoadingPlaceholderProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
|
||||
<div className="gf-form-group">
|
||||
{text} <i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
@@ -8,3 +8,8 @@ export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
|
||||
export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
||||
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
||||
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||
|
||||
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker } from './ColorPicker/ColorPicker';
|
||||
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
|
||||
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
|
||||
|
||||
93
packages/grafana-ui/src/utils/colors.ts
Normal file
93
packages/grafana-ui/src/utils/colors.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import _ from 'lodash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export const PALETTE_ROWS = 4;
|
||||
export const PALETTE_COLUMNS = 14;
|
||||
export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
|
||||
export const OK_COLOR = 'rgba(11, 237, 50, 1)';
|
||||
export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)';
|
||||
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
|
||||
export const PENDING_COLOR = 'rgba(247, 149, 32, 1)';
|
||||
export const REGION_FILL_ALPHA = 0.09;
|
||||
|
||||
export const colors = [
|
||||
'#7EB26D', // 0: pale green
|
||||
'#EAB839', // 1: mustard
|
||||
'#6ED0E0', // 2: light blue
|
||||
'#EF843C', // 3: orange
|
||||
'#E24D42', // 4: red
|
||||
'#1F78C1', // 5: ocean
|
||||
'#BA43A9', // 6: purple
|
||||
'#705DA0', // 7: violet
|
||||
'#508642', // 8: dark green
|
||||
'#CCA300', // 9: dark sand
|
||||
'#447EBC',
|
||||
'#C15C17',
|
||||
'#890F02',
|
||||
'#0A437C',
|
||||
'#6D1F62',
|
||||
'#584477',
|
||||
'#B7DBAB',
|
||||
'#F4D598',
|
||||
'#70DBED',
|
||||
'#F9BA8F',
|
||||
'#F29191',
|
||||
'#82B5D8',
|
||||
'#E5A8E2',
|
||||
'#AEA2E0',
|
||||
'#629E51',
|
||||
'#E5AC0E',
|
||||
'#64B0C8',
|
||||
'#E0752D',
|
||||
'#BF1B00',
|
||||
'#0A50A1',
|
||||
'#962D82',
|
||||
'#614D93',
|
||||
'#9AC48A',
|
||||
'#F2C96D',
|
||||
'#65C5DB',
|
||||
'#F9934E',
|
||||
'#EA6460',
|
||||
'#5195CE',
|
||||
'#D683CE',
|
||||
'#806EB7',
|
||||
'#3F6833',
|
||||
'#967302',
|
||||
'#2F575E',
|
||||
'#99440A',
|
||||
'#58140C',
|
||||
'#052B51',
|
||||
'#511749',
|
||||
'#3F2B5B',
|
||||
'#E0F9D7',
|
||||
'#FCEACA',
|
||||
'#CFFAFF',
|
||||
'#F9E2D2',
|
||||
'#FCE2DE',
|
||||
'#BADFF4',
|
||||
'#F9D9F9',
|
||||
'#DEDAF7',
|
||||
];
|
||||
|
||||
function sortColorsByHue(hexColors: string[]) {
|
||||
const hslColors = _.map(hexColors, hexToHsl);
|
||||
|
||||
const sortedHSLColors = _.sortBy(hslColors, ['h']);
|
||||
const chunkedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS);
|
||||
const sortedChunkedHSLColors = _.map(chunkedHSLColors, chunk => {
|
||||
return _.sortBy(chunk, 'l');
|
||||
});
|
||||
const flattenedZippedSortedChunkedHSLColors = _.flattenDeep(_.zip(...sortedChunkedHSLColors));
|
||||
|
||||
return _.map(flattenedZippedSortedChunkedHSLColors, hslToHex);
|
||||
}
|
||||
|
||||
function hexToHsl(color: string) {
|
||||
return tinycolor(color).toHsl();
|
||||
}
|
||||
|
||||
function hslToHex(color: any) {
|
||||
return tinycolor(color).toHexString();
|
||||
}
|
||||
|
||||
export let sortedColors = sortColorsByHue(colors);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './processTimeSeries';
|
||||
export * from './colors';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM debian:stretch-slim
|
||||
ARG BASE_IMAGE=debian:stretch-slim
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz"
|
||||
|
||||
@@ -10,7 +11,8 @@ COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
|
||||
|
||||
RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
|
||||
|
||||
FROM debian:stretch-slim
|
||||
ARG BASE_IMAGE=debian:stretch-slim
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG GF_UID="472"
|
||||
ARG GF_GID="472"
|
||||
|
||||
@@ -8,6 +8,5 @@ docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
|
||||
./push_to_docker_hub.sh "$_grafana_version"
|
||||
|
||||
if echo "$_grafana_version" | grep -q "^master-"; then
|
||||
apk add --no-cache curl
|
||||
./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
|
||||
fi
|
||||
|
||||
@@ -1,25 +1,49 @@
|
||||
#!/bin/sh
|
||||
|
||||
_grafana_tag=$1
|
||||
_grafana_tag=${1:-}
|
||||
_docker_repo=${2:-grafana/grafana}
|
||||
|
||||
# If the tag starts with v, treat this as a official release
|
||||
if echo "$_grafana_tag" | grep -q "^v"; then
|
||||
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
||||
_docker_repo=${2:-grafana/grafana}
|
||||
else
|
||||
_grafana_version=$_grafana_tag
|
||||
_docker_repo=${2:-grafana/grafana-dev}
|
||||
fi
|
||||
|
||||
echo "Building ${_docker_repo}:${_grafana_version}"
|
||||
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
# Build grafana image for a specific arch
|
||||
docker_build () {
|
||||
base_image=$1
|
||||
grafana_tgz=$2
|
||||
tag=$3
|
||||
|
||||
docker build \
|
||||
--tag "${_docker_repo}:${_grafana_version}" \
|
||||
--build-arg BASE_IMAGE=${base_image} \
|
||||
--build-arg GRAFANA_TGZ=${grafana_tgz} \
|
||||
--tag "${tag}" \
|
||||
--no-cache=true .
|
||||
}
|
||||
|
||||
# Tag docker images of all architectures
|
||||
docker_tag_all () {
|
||||
repo=$1
|
||||
tag=$2
|
||||
docker tag "${_docker_repo}:${_grafana_version}" "${repo}:${tag}"
|
||||
docker tag "${_docker_repo}-arm32v7-linux:${_grafana_version}" "${repo}-arm32v7-linux:${tag}"
|
||||
docker tag "${_docker_repo}-arm64v8-linux:${_grafana_version}" "${repo}-arm64v8-linux:${tag}"
|
||||
}
|
||||
|
||||
docker_build "debian:stretch-slim" "grafana-latest.linux-x64.tar.gz" "${_docker_repo}:${_grafana_version}"
|
||||
docker_build "arm32v7/debian:stretch-slim" "grafana-latest.linux-armv7.tar.gz" "${_docker_repo}-arm32v7-linux:${_grafana_version}"
|
||||
docker_build "arm64v8/debian:stretch-slim" "grafana-latest.linux-arm64.tar.gz" "${_docker_repo}-arm64v8-linux:${_grafana_version}"
|
||||
|
||||
# Tag as 'latest' for official release; otherwise tag as grafana/grafana:master
|
||||
if echo "$_grafana_tag" | grep -q "^v"; then
|
||||
docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest"
|
||||
docker_tag_all "${_docker_repo}" "latest"
|
||||
else
|
||||
docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master"
|
||||
docker_tag_all "${_docker_repo}" "master"
|
||||
docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana-dev:${_grafana_version}"
|
||||
fi
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
_grafana_tag=$1
|
||||
_grafana_tag=${1:-}
|
||||
_docker_repo=${2:-grafana/grafana}
|
||||
|
||||
# If the tag starts with v, treat this as a official release
|
||||
if echo "$_grafana_tag" | grep -q "^v"; then
|
||||
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
||||
_docker_repo=${2:-grafana/grafana}
|
||||
else
|
||||
_grafana_version=$_grafana_tag
|
||||
_docker_repo=${2:-grafana/grafana-dev}
|
||||
fi
|
||||
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
echo "pushing ${_docker_repo}:${_grafana_version}"
|
||||
docker push "${_docker_repo}:${_grafana_version}"
|
||||
|
||||
|
||||
docker_push_all () {
|
||||
repo=$1
|
||||
tag=$2
|
||||
|
||||
# Push each image individually
|
||||
docker push "${repo}:${tag}"
|
||||
docker push "${repo}-arm32v7-linux:${tag}"
|
||||
docker push "${repo}-arm64v8-linux:${tag}"
|
||||
|
||||
# Create and push a multi-arch manifest
|
||||
docker manifest create "${repo}:${tag}" \
|
||||
"${repo}:${tag}" \
|
||||
"${repo}-arm32v7-linux:${tag}" \
|
||||
"${repo}-arm64v8-linux:${tag}"
|
||||
|
||||
docker manifest push "${repo}:${tag}"
|
||||
}
|
||||
|
||||
if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
|
||||
echo "pushing ${_docker_repo}:latest"
|
||||
docker push "${_docker_repo}:latest"
|
||||
docker_push_all "${_docker_repo}" "latest"
|
||||
docker_push_all "${_docker_repo}" "${_grafana_version}"
|
||||
elif echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -q "beta"; then
|
||||
docker_push_all "${_docker_repo}" "${_grafana_version}"
|
||||
elif echo "$_grafana_tag" | grep -q "master"; then
|
||||
echo "pushing grafana/grafana:master"
|
||||
docker push grafana/grafana:master
|
||||
docker_push_all "${_docker_repo}" "master"
|
||||
docker push "grafana/grafana-dev:${_grafana_version}"
|
||||
fi
|
||||
|
||||
@@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard {
|
||||
func NewDashboardFolder(title string) *Dashboard {
|
||||
folder := NewDashboard(title)
|
||||
folder.IsFolder = true
|
||||
folder.Data.Set("schemaVersion", 16)
|
||||
folder.Data.Set("schemaVersion", 17)
|
||||
folder.Data.Set("version", 0)
|
||||
folder.IsFolder = true
|
||||
return folder
|
||||
|
||||
@@ -112,7 +112,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
|
||||
frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
|
||||
if err != nil {
|
||||
return nil, ValidationError{Reason: "Could not parse frequency"}
|
||||
return nil, ValidationError{Reason: err.Error()}
|
||||
}
|
||||
|
||||
rawFor := jsonAlert.Get("for").MustString()
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFrequencyCannotBeZeroOrLess = errors.New(`"evaluate every" cannot be zero or below`)
|
||||
ErrFrequencyCouldNotBeParsed = errors.New(`"evaluate every" field could not be parsed`)
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
@@ -76,7 +81,7 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
|
||||
matches := ValueFormatRegex.FindAllString(str, 1)
|
||||
|
||||
if len(matches) <= 0 {
|
||||
return 0, fmt.Errorf("Frequency could not be parsed")
|
||||
return 0, ErrFrequencyCouldNotBeParsed
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(matches[0])
|
||||
@@ -84,6 +89,10 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if value == 0 {
|
||||
return 0, ErrFrequencyCannotBeZeroOrLess
|
||||
}
|
||||
|
||||
unit := UnitFormatRegex.FindAllString(str, 1)[0]
|
||||
|
||||
if val, ok := unitMultiplier[unit]; ok {
|
||||
@@ -101,7 +110,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
model.PanelId = ruleDef.PanelId
|
||||
model.Name = ruleDef.Name
|
||||
model.Message = ruleDef.Message
|
||||
model.Frequency = ruleDef.Frequency
|
||||
model.State = ruleDef.State
|
||||
model.LastStateChange = ruleDef.NewStateDate
|
||||
model.For = ruleDef.For
|
||||
@@ -109,6 +117,13 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
|
||||
model.StateChanges = ruleDef.StateChanges
|
||||
|
||||
model.Frequency = ruleDef.Frequency
|
||||
// frequency cannot be zero since that would not execute the alert rule.
|
||||
// so we fallback to 60 seconds if `Freqency` is missing
|
||||
if model.Frequency == 0 {
|
||||
model.Frequency = 60
|
||||
}
|
||||
|
||||
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
||||
jsonModel := simplejson.NewFromAny(v)
|
||||
id, err := jsonModel.Get("id").Int64()
|
||||
|
||||
@@ -14,6 +14,36 @@ func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
|
||||
return &ConditionResult{}, nil
|
||||
}
|
||||
|
||||
func TestAlertRuleFrequencyParsing(t *testing.T) {
|
||||
tcs := []struct {
|
||||
input string
|
||||
err error
|
||||
result int64
|
||||
}{
|
||||
{input: "10s", result: 10},
|
||||
{input: "10m", result: 600},
|
||||
{input: "1h", result: 3600},
|
||||
{input: "1o", result: 1},
|
||||
{input: "0s", err: ErrFrequencyCannotBeZeroOrLess},
|
||||
{input: "0m", err: ErrFrequencyCannotBeZeroOrLess},
|
||||
{input: "0h", err: ErrFrequencyCannotBeZeroOrLess},
|
||||
{input: "0", err: ErrFrequencyCannotBeZeroOrLess},
|
||||
{input: "-1s", err: ErrFrequencyCouldNotBeParsed},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
r, err := getTimeDurationStringToSeconds(tc.input)
|
||||
if err != tc.err {
|
||||
t.Errorf("expected error: '%v' got: '%v'", tc.err, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r != tc.result {
|
||||
t.Errorf("expected result: %d got %d", tc.result, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertRuleModel(t *testing.T) {
|
||||
Convey("Testing alert rule", t, func() {
|
||||
|
||||
@@ -21,26 +51,6 @@ func TestAlertRuleModel(t *testing.T) {
|
||||
return &FakeCondition{}, nil
|
||||
})
|
||||
|
||||
Convey("Can parse seconds", func() {
|
||||
seconds, _ := getTimeDurationStringToSeconds("10s")
|
||||
So(seconds, ShouldEqual, 10)
|
||||
})
|
||||
|
||||
Convey("Can parse minutes", func() {
|
||||
seconds, _ := getTimeDurationStringToSeconds("10m")
|
||||
So(seconds, ShouldEqual, 600)
|
||||
})
|
||||
|
||||
Convey("Can parse hours", func() {
|
||||
seconds, _ := getTimeDurationStringToSeconds("1h")
|
||||
So(seconds, ShouldEqual, 3600)
|
||||
})
|
||||
|
||||
Convey("defaults to seconds", func() {
|
||||
seconds, _ := getTimeDurationStringToSeconds("1o")
|
||||
So(seconds, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("should return err for empty string", func() {
|
||||
_, err := getTimeDurationStringToSeconds("")
|
||||
So(err, ShouldNotBeNil)
|
||||
@@ -89,5 +99,35 @@ func TestAlertRuleModel(t *testing.T) {
|
||||
So(len(alertRule.Notifications), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("can construct alert rule model with invalid frequency", func() {
|
||||
json := `
|
||||
{
|
||||
"name": "name2",
|
||||
"description": "desc2",
|
||||
"noDataMode": "critical",
|
||||
"enabled": true,
|
||||
"frequency": "0s",
|
||||
"conditions": [ { "type": "test", "prop": 123 } ],
|
||||
"notifications": []
|
||||
}`
|
||||
|
||||
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
||||
So(jsonErr, ShouldBeNil)
|
||||
|
||||
alert := &m.Alert{
|
||||
Id: 1,
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
PanelId: 1,
|
||||
Frequency: 0,
|
||||
|
||||
Settings: alertJSON,
|
||||
}
|
||||
|
||||
alertRule, err := NewRuleFromDBAlert(alert)
|
||||
So(err, ShouldBeNil)
|
||||
So(alertRule.Frequency, ShouldEqual, 60)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SearchResult } from './components/search/SearchResult';
|
||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||
import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
|
||||
|
||||
export function registerAngularDirectives() {
|
||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||
@@ -19,4 +20,13 @@ export function registerAngularDirectives() {
|
||||
['onChange', { watchDepth: 'reference' }],
|
||||
['tagOptions', { watchDepth: 'reference' }],
|
||||
]);
|
||||
react2AngularDirective('colorPicker', ColorPicker, [
|
||||
'color',
|
||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
|
||||
'series',
|
||||
'onColorChange',
|
||||
'onToggleAxis',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@ import './partials';
|
||||
import './components/jsontree/jsontree';
|
||||
import './components/code_editor/code_editor';
|
||||
import './utils/outline';
|
||||
import './components/colorpicker/ColorPicker';
|
||||
import './components/colorpicker/SeriesColorPickerPopover';
|
||||
import './components/colorpicker/spectrum_picker';
|
||||
import './services/search_srv';
|
||||
import './services/ng_react';
|
||||
import { colors } from '@grafana/ui/';
|
||||
|
||||
import { searchDirective } from './components/search/search';
|
||||
import { infoPopover } from './components/info_popover';
|
||||
@@ -36,7 +35,6 @@ import 'app/core/services/all';
|
||||
import './filters/filters';
|
||||
import coreModule from './core_module';
|
||||
import appEvents from './app_events';
|
||||
import colors from './utils/colors';
|
||||
import { assignModelProperties } from './utils/model_utils';
|
||||
import { contextSrv } from './services/context_srv';
|
||||
import { KeybindingSrv } from './services/keybindingSrv';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
import { TimeSeries } from 'app/core/core';
|
||||
import colors, { getThemeColor } from 'app/core/utils/colors';
|
||||
import { getThemeColor } from 'app/core/utils/colors';
|
||||
|
||||
/**
|
||||
* Mapping of log level abbreviation to canonical log level.
|
||||
|
||||
8
public/app/core/specs/factors.test.ts
Normal file
8
public/app/core/specs/factors.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import getFactors from 'app/core/utils/factors';
|
||||
|
||||
describe('factors', () => {
|
||||
it('should return factors for 12', () => {
|
||||
const factors = getFactors(12);
|
||||
expect(factors).toEqual([1, 2, 3, 4, 6, 12]);
|
||||
});
|
||||
});
|
||||
@@ -1,99 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export const PALETTE_ROWS = 4;
|
||||
export const PALETTE_COLUMNS = 14;
|
||||
export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
|
||||
export const OK_COLOR = 'rgba(11, 237, 50, 1)';
|
||||
export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)';
|
||||
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
|
||||
export const PENDING_COLOR = 'rgba(247, 149, 32, 1)';
|
||||
export const REGION_FILL_ALPHA = 0.09;
|
||||
|
||||
const colors = [
|
||||
'#7EB26D', // 0: pale green
|
||||
'#EAB839', // 1: mustard
|
||||
'#6ED0E0', // 2: light blue
|
||||
'#EF843C', // 3: orange
|
||||
'#E24D42', // 4: red
|
||||
'#1F78C1', // 5: ocean
|
||||
'#BA43A9', // 6: purple
|
||||
'#705DA0', // 7: violet
|
||||
'#508642', // 8: dark green
|
||||
'#CCA300', // 9: dark sand
|
||||
'#447EBC',
|
||||
'#C15C17',
|
||||
'#890F02',
|
||||
'#0A437C',
|
||||
'#6D1F62',
|
||||
'#584477',
|
||||
'#B7DBAB',
|
||||
'#F4D598',
|
||||
'#70DBED',
|
||||
'#F9BA8F',
|
||||
'#F29191',
|
||||
'#82B5D8',
|
||||
'#E5A8E2',
|
||||
'#AEA2E0',
|
||||
'#629E51',
|
||||
'#E5AC0E',
|
||||
'#64B0C8',
|
||||
'#E0752D',
|
||||
'#BF1B00',
|
||||
'#0A50A1',
|
||||
'#962D82',
|
||||
'#614D93',
|
||||
'#9AC48A',
|
||||
'#F2C96D',
|
||||
'#65C5DB',
|
||||
'#F9934E',
|
||||
'#EA6460',
|
||||
'#5195CE',
|
||||
'#D683CE',
|
||||
'#806EB7',
|
||||
'#3F6833',
|
||||
'#967302',
|
||||
'#2F575E',
|
||||
'#99440A',
|
||||
'#58140C',
|
||||
'#052B51',
|
||||
'#511749',
|
||||
'#3F2B5B',
|
||||
'#E0F9D7',
|
||||
'#FCEACA',
|
||||
'#CFFAFF',
|
||||
'#F9E2D2',
|
||||
'#FCE2DE',
|
||||
'#BADFF4',
|
||||
'#F9D9F9',
|
||||
'#DEDAF7',
|
||||
];
|
||||
|
||||
export function sortColorsByHue(hexColors) {
|
||||
const hslColors = _.map(hexColors, hexToHsl);
|
||||
|
||||
let sortedHSLColors = _.sortBy(hslColors, ['h']);
|
||||
sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS);
|
||||
sortedHSLColors = _.map(sortedHSLColors, chunk => {
|
||||
return _.sortBy(chunk, 'l');
|
||||
});
|
||||
sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors));
|
||||
|
||||
return _.map(sortedHSLColors, hslToHex);
|
||||
}
|
||||
|
||||
export function hexToHsl(color) {
|
||||
return tinycolor(color).toHsl();
|
||||
}
|
||||
|
||||
export function hslToHex(color) {
|
||||
return tinycolor(color).toHexString();
|
||||
}
|
||||
|
||||
export function getThemeColor(dark: string, light: string): string {
|
||||
return config.bootData.user.lightTheme ? light : dark;
|
||||
}
|
||||
|
||||
export let sortedColors = sortColorsByHue(colors);
|
||||
export default colors;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
import { renderUrl } from 'app/core/utils/url';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import store from 'app/core/store';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import { parse as parseDate } from 'app/core/utils/datemath';
|
||||
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
|
||||
5
public/app/core/utils/factors.ts
Normal file
5
public/app/core/utils/factors.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Returns the factors of a number
|
||||
// Example getFactors(12) -> [1, 2, 3, 4, 6, 12]
|
||||
export default function getFactors(num: number): number[] {
|
||||
return Array.from(new Array(num + 1), (_, i) => i).filter(i => num % i === 0);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Libraries
|
||||
import React, { PureComponent, SFC } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & Utils
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
@@ -14,7 +14,7 @@ import 'app/features/alerting/AlertTabCtrl';
|
||||
// Types
|
||||
import { DashboardModel } from '../dashboard/dashboard_model';
|
||||
import { PanelModel } from '../dashboard/panel_model';
|
||||
import { TestRuleButton } from './TestRuleButton';
|
||||
import { TestRuleResult } from './TestRuleResult';
|
||||
|
||||
interface Props {
|
||||
angularPanel?: AngularComponent;
|
||||
@@ -22,16 +22,6 @@ interface Props {
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
interface LoadingPlaceholderProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
|
||||
<div className="gf-form-group">
|
||||
{text} <i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export class AlertTab extends PureComponent<Props> {
|
||||
element: any;
|
||||
component: AngularComponent;
|
||||
@@ -120,14 +110,14 @@ export class AlertTab extends PureComponent<Props> {
|
||||
};
|
||||
};
|
||||
|
||||
renderTestRuleButton = () => {
|
||||
renderTestRuleResult = () => {
|
||||
const { panel, dashboard } = this.props;
|
||||
return <TestRuleButton panelId={panel.id} dashboard={dashboard} LoadingPlaceholder={LoadingPlaceholder} />;
|
||||
return <TestRuleResult panelId={panel.id} dashboard={dashboard} />;
|
||||
};
|
||||
|
||||
testRule = (): EditorToolbarView => ({
|
||||
title: 'Test Rule',
|
||||
render: () => this.renderTestRuleButton(),
|
||||
render: () => this.renderTestRuleResult(),
|
||||
});
|
||||
|
||||
onAddAlert = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DashboardModel } from '../dashboard/dashboard_model';
|
||||
import { Props, TestRuleButton } from './TestRuleButton';
|
||||
import { Props, TestRuleResult } from './TestRuleResult';
|
||||
|
||||
jest.mock('app/core/services/backend_srv', () => ({
|
||||
getBackendSrv: () => ({
|
||||
@@ -13,14 +13,13 @@ const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
panelId: 1,
|
||||
dashboard: new DashboardModel({ panels: [{ id: 1 }] }),
|
||||
LoadingPlaceholder: {},
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<TestRuleButton {...props} />);
|
||||
const wrapper = shallow(<TestRuleResult {...props} />);
|
||||
|
||||
return { wrapper, instance: wrapper.instance() as TestRuleButton };
|
||||
return { wrapper, instance: wrapper.instance() as TestRuleResult };
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
@@ -2,11 +2,11 @@ import React, { PureComponent } from 'react';
|
||||
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DashboardModel } from '../dashboard/dashboard_model';
|
||||
import { LoadingPlaceholder } from '@grafana/ui/src';
|
||||
|
||||
export interface Props {
|
||||
panelId: number;
|
||||
dashboard: DashboardModel;
|
||||
LoadingPlaceholder: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -14,7 +14,7 @@ interface State {
|
||||
testRuleResponse: {};
|
||||
}
|
||||
|
||||
export class TestRuleButton extends PureComponent<Props, State> {
|
||||
export class TestRuleResult extends PureComponent<Props, State> {
|
||||
readonly state: State = {
|
||||
isLoading: false,
|
||||
testRuleResponse: {},
|
||||
@@ -27,13 +27,14 @@ export class TestRuleButton extends PureComponent<Props, State> {
|
||||
async testRule() {
|
||||
const { panelId, dashboard } = this.props;
|
||||
const payload = { dashboard: dashboard.getSaveModelClone(), panelId };
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
const testRuleResponse = await getBackendSrv().post(`/api/alerts/test`, payload);
|
||||
this.setState(prevState => ({ ...prevState, isLoading: false, testRuleResponse }));
|
||||
this.setState({ isLoading: false, testRuleResponse });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { testRuleResponse, isLoading } = this.state;
|
||||
const { LoadingPlaceholder } = this.props;
|
||||
|
||||
if (isLoading === true) {
|
||||
return <LoadingPlaceholder text="Evaluating rule" />;
|
||||
@@ -1,13 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<JSONFormatter
|
||||
config={
|
||||
Object {
|
||||
"animateOpen": true,
|
||||
}
|
||||
}
|
||||
json={Object {}}
|
||||
open={3}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<Component
|
||||
text="Evaluating rule"
|
||||
/>
|
||||
`;
|
||||
@@ -1,8 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { MetricsPanelCtrl } from 'app/plugins/sdk';
|
||||
import { AnnotationEvent } from './event';
|
||||
import {
|
||||
OK_COLOR,
|
||||
ALERTING_COLOR,
|
||||
@@ -10,7 +8,10 @@ import {
|
||||
PENDING_COLOR,
|
||||
DEFAULT_ANNOTATION_COLOR,
|
||||
REGION_FILL_ALPHA,
|
||||
} from 'app/core/utils/colors';
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { MetricsPanelCtrl } from 'app/plugins/sdk';
|
||||
import { AnnotationEvent } from './event';
|
||||
|
||||
export class EventManager {
|
||||
event: AnnotationEvent;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from 'app/core/constants';
|
||||
import { PanelModel } from './panel_model';
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import getFactors from 'app/core/utils/factors';
|
||||
|
||||
export class DashboardMigrator {
|
||||
dashboard: DashboardModel;
|
||||
@@ -21,7 +22,7 @@ export class DashboardMigrator {
|
||||
let i, j, k, n;
|
||||
const oldVersion = this.dashboard.schemaVersion;
|
||||
const panelUpgrades = [];
|
||||
this.dashboard.schemaVersion = 16;
|
||||
this.dashboard.schemaVersion = 17;
|
||||
|
||||
if (oldVersion === this.dashboard.schemaVersion) {
|
||||
return;
|
||||
@@ -368,6 +369,24 @@ export class DashboardMigrator {
|
||||
this.upgradeToGridLayout(old);
|
||||
}
|
||||
|
||||
if (oldVersion < 17) {
|
||||
panelUpgrades.push(panel => {
|
||||
if (panel.minSpan) {
|
||||
const max = GRID_COLUMN_COUNT / panel.minSpan;
|
||||
const factors = getFactors(GRID_COLUMN_COUNT);
|
||||
// find the best match compared to factors
|
||||
// (ie. [1,2,3,4,6,12,24] for 24 columns)
|
||||
panel.maxPerRow =
|
||||
factors[
|
||||
_.findIndex(factors, o => {
|
||||
return o > max;
|
||||
}) - 1
|
||||
];
|
||||
}
|
||||
delete panel.minSpan;
|
||||
});
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
|
||||
|
||||
import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||
import { DEFAULT_ANNOTATION_COLOR } from 'app/core/utils/colors';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import sortByKeys from 'app/core/utils/sort_by_keys';
|
||||
@@ -442,7 +442,7 @@ export class DashboardModel {
|
||||
}
|
||||
|
||||
const selectedOptions = this.getSelectedVariableOptions(variable);
|
||||
const minWidth = panel.minSpan || 6;
|
||||
const maxPerRow = panel.maxPerRow || 4;
|
||||
let xPos = 0;
|
||||
let yPos = panel.gridPos.y;
|
||||
|
||||
@@ -462,7 +462,7 @@ export class DashboardModel {
|
||||
} else {
|
||||
// set width based on how many are selected
|
||||
// assumed the repeated panels should take up full row width
|
||||
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth);
|
||||
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, GRID_COLUMN_COUNT / maxPerRow);
|
||||
copy.gridPos.x = xPos;
|
||||
copy.gridPos.y = yPos;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Libraries
|
||||
import React, { PureComponent, SFC } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Components
|
||||
@@ -36,12 +36,6 @@ interface State {
|
||||
isAddingMixed: boolean;
|
||||
}
|
||||
|
||||
interface LoadingPlaceholderProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
|
||||
|
||||
export class QueriesTab extends PureComponent<Props, State> {
|
||||
element: HTMLElement;
|
||||
component: AngularComponent;
|
||||
@@ -134,7 +128,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
|
||||
renderQueryInspector = () => {
|
||||
const { panel } = this.props;
|
||||
return <QueryInspector panel={panel} LoadingPlaceholder={LoadingPlaceholder} />;
|
||||
return <QueryInspector panel={panel} />;
|
||||
};
|
||||
|
||||
renderHelp = () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
|
||||
interface DsQuery {
|
||||
isLoading: boolean;
|
||||
@@ -10,7 +11,6 @@ interface DsQuery {
|
||||
|
||||
interface Props {
|
||||
panel: any;
|
||||
LoadingPlaceholder: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { response, isLoading } = this.state.dsQuery;
|
||||
const { LoadingPlaceholder } = this.props;
|
||||
const { isMocking } = this.state;
|
||||
const openNodes = this.getNrOfOpenNodes();
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export class PanelModel {
|
||||
repeatPanelId?: number;
|
||||
repeatDirection?: string;
|
||||
repeatedByRow?: boolean;
|
||||
minSpan?: number;
|
||||
maxPerRow?: number;
|
||||
collapsed?: boolean;
|
||||
panels?: any;
|
||||
soloMode?: boolean;
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('DashboardModel', () => {
|
||||
});
|
||||
|
||||
it('dashboard schema version should be set to latest', () => {
|
||||
expect(model.schemaVersion).toBe(16);
|
||||
expect(model.schemaVersion).toBe(17);
|
||||
});
|
||||
|
||||
it('graph thresholds should be migrated', () => {
|
||||
@@ -364,14 +364,6 @@ describe('DashboardModel', () => {
|
||||
expect(dashboard.panels.length).toBe(2);
|
||||
});
|
||||
|
||||
it('minSpan should be twice', () => {
|
||||
model.rows = [createRow({ height: 8 }, [[6]])];
|
||||
model.rows[0].panels[0] = { minSpan: 12 };
|
||||
|
||||
const dashboard = new DashboardModel(model);
|
||||
expect(dashboard.panels[0].minSpan).toBe(24);
|
||||
});
|
||||
|
||||
it('should assign id', () => {
|
||||
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
|
||||
model.rows[0].panels[0] = {};
|
||||
@@ -380,6 +372,16 @@ describe('DashboardModel', () => {
|
||||
expect(dashboard.panels[0].id).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when migrating from minSpan to maxPerRow', () => {
|
||||
it('maxPerRow should be correct', () => {
|
||||
const model = {
|
||||
panels: [{ minSpan: 8 }],
|
||||
};
|
||||
const dashboard = new DashboardModel(model);
|
||||
expect(dashboard.panels[0].maxPerRow).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createRow(options, panelDescriptions: any[]) {
|
||||
|
||||
@@ -143,12 +143,9 @@ export function applyPanelTimeOverrides(panel: PanelModel, timeRange: TimeRange)
|
||||
const timeShift = '-' + timeShiftInterpolated;
|
||||
newTimeData.timeInfo += ' timeshift ' + timeShift;
|
||||
newTimeData.timeRange = {
|
||||
from: dateMath.parseDateMath(timeShift, timeRange.from, false),
|
||||
to: dateMath.parseDateMath(timeShift, timeRange.to, true),
|
||||
raw: {
|
||||
from: timeRange.from,
|
||||
to: timeRange.to,
|
||||
},
|
||||
from: dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false),
|
||||
to: dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true),
|
||||
raw: newTimeData.timeRange.raw,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import Remarkable from 'remarkable';
|
||||
import config from 'app/core/config';
|
||||
import { profiler } from 'app/core/core';
|
||||
import { Emitter } from 'app/core/core';
|
||||
import getFactors from 'app/core/utils/factors';
|
||||
import {
|
||||
duplicatePanel,
|
||||
copyPanel as copyPanelUtil,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
sharePanel as sharePanelUtil,
|
||||
} from 'app/features/dashboard/utils/panel';
|
||||
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
|
||||
|
||||
export class PanelCtrl {
|
||||
panel: any;
|
||||
@@ -32,6 +33,7 @@ export class PanelCtrl {
|
||||
events: Emitter;
|
||||
timing: any;
|
||||
loading: boolean;
|
||||
maxPanelsPerRowOptions: number[];
|
||||
|
||||
constructor($scope, $injector) {
|
||||
this.$injector = $injector;
|
||||
@@ -92,6 +94,7 @@ export class PanelCtrl {
|
||||
if (!this.editModeInitiated) {
|
||||
this.editModeInitiated = true;
|
||||
this.events.emit('init-edit-mode', null);
|
||||
this.maxPanelsPerRowOptions = getFactors(GRID_COLUMN_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,16 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
|
||||
<span class="gf-form-label width-9">Min width</span>
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]">
|
||||
<span class="gf-form-label width-9">Max per row</span>
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.maxPerRow" ng-options="f for f in [2,3,4,6,12,24]">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="gf-form-hint">
|
||||
<div class="gf-form-hint-text muted">
|
||||
Note: You may need to change the variable selection to see this in action.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
function renderTagCondition(tag, index) {
|
||||
let str = '';
|
||||
@@ -43,7 +44,7 @@ export class InfluxQueryBuilder {
|
||||
} else if (type === 'MEASUREMENTS') {
|
||||
query = 'SHOW MEASUREMENTS';
|
||||
if (withMeasurementFilter) {
|
||||
query += ' WITH MEASUREMENT =~ /' + withMeasurementFilter + '/';
|
||||
query += ' WITH MEASUREMENT =~ /' + kbn.regexEscape(withMeasurementFilter) + '/';
|
||||
}
|
||||
} else if (type === 'FIELDS') {
|
||||
measurement = this.target.measurement;
|
||||
|
||||
@@ -50,6 +50,12 @@ describe('InfluxQueryBuilder', () => {
|
||||
expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ LIMIT 100');
|
||||
});
|
||||
|
||||
it('should escape the regex value in measurement query', () => {
|
||||
const builder = new InfluxQueryBuilder({ measurement: '', tags: [] });
|
||||
const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'abc/edf/');
|
||||
expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /abc\\/edf\\// LIMIT 100');
|
||||
});
|
||||
|
||||
it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', () => {
|
||||
const builder = new InfluxQueryBuilder({
|
||||
measurement: '',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
||||
import { ColorPicker } from '@grafana/ui';
|
||||
import { BasicGaugeColor, Threshold } from 'app/types';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { TimeSeries } from 'app/core/core';
|
||||
import { SeriesColorPicker } from 'app/core/components/colorpicker/SeriesColorPicker';
|
||||
import { SeriesColorPicker } from '@grafana/ui';
|
||||
|
||||
export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total'];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import colors from 'app/core/utils/colors';
|
||||
|
||||
export class DataProcessor {
|
||||
constructor(private panel) {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
// Components & Types
|
||||
import { Graph, PanelProps, NullValueMode, processTimeSeries } from '@grafana/ui';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import Drop from 'tether-drop';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import Drop from 'tether-drop';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv';
|
||||
import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
// Utils
|
||||
import colors from 'app/core/utils/colors';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui';
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
],
|
||||
"rows": [],
|
||||
"schemaVersion": 16,
|
||||
"schemaVersion": 17,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
|
||||
@@ -59,7 +59,7 @@ go run build.go ${OPT} build-frontend
|
||||
source /etc/profile.d/rvm.sh
|
||||
|
||||
echo "Packaging"
|
||||
go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only latest
|
||||
go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only
|
||||
#removing amd64 phantomjs bin for armv7/arm64 packages
|
||||
rm tools/phantomjs/phantomjs
|
||||
go run build.go -goos linux -pkg-arch armv7 ${OPT} package-only
|
||||
@@ -80,3 +80,4 @@ else
|
||||
fi
|
||||
go run build.go -goos windows -pkg-arch amd64 ${OPT} package-only
|
||||
|
||||
go run build.go latest
|
||||
@@ -8,6 +8,8 @@ set -e
|
||||
|
||||
EXTRA_OPTS="$@"
|
||||
|
||||
CCARMV7=arm-linux-gnueabihf-gcc
|
||||
CCARM64=aarch64-linux-gnu-gcc
|
||||
CCX64=/tmp/x86_64-centos6-linux-gnu/bin/x86_64-centos6-linux-gnu-gcc
|
||||
|
||||
GOPATH=/go
|
||||
@@ -26,6 +28,9 @@ fi
|
||||
|
||||
echo "Build arguments: $OPT"
|
||||
|
||||
go run build.go -goarch armv7 -cc ${CCARMV7} ${OPT} build
|
||||
go run build.go -goarch arm64 -cc ${CCARM64} ${OPT} build
|
||||
|
||||
CC=${CCX64} go run build.go ${OPT} build
|
||||
|
||||
yarn install --pure-lockfile --no-progress
|
||||
@@ -43,4 +48,8 @@ go run build.go ${OPT} build-frontend
|
||||
source /etc/profile.d/rvm.sh
|
||||
|
||||
echo "Packaging"
|
||||
go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only latest
|
||||
go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only
|
||||
go run build.go -goos linux -pkg-arch armv7 ${OPT} package-only
|
||||
go run build.go -goos linux -pkg-arch arm64 ${OPT} package-only
|
||||
|
||||
go run build.go latest
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@@ -1118,6 +1118,23 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-0.2.5.tgz#2443fc12da514c81346b1a665675559cee21fa75"
|
||||
integrity sha512-dEoVvo/I9QFomyhY+4Q6Qk+I+dhG59TYceZgC6Q0mCifVPErx6Y83PNTKGDS5e9h9Eti6q0S2mm16BU6iQK+3w==
|
||||
|
||||
"@types/tether-drop@^1.4.8":
|
||||
version "1.4.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/tether-drop/-/tether-drop-1.4.8.tgz#8d64288e673259d1bc28518250b80b5ef43af0bc"
|
||||
integrity sha512-QzrJDUxnLoqACUm7opxGOwa9mgMBlkyb7hHYWApMLM3ywWif4pWraTiotooiG3ePZmnTe8wQj2nx7GWMX4pb+w==
|
||||
dependencies:
|
||||
"@types/tether" "*"
|
||||
|
||||
"@types/tether@*":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/tether/-/tether-1.4.4.tgz#0fde1ccbd2f1fad74f8f465fe6227ff3b7bff634"
|
||||
integrity sha512-6qhsFJVMuMqaQRVyQVi3zUBLfKYyryktL0ZP0Z3zegzeQ7WKm0PZNCdl3JsaitJbzqaoQ9qsFKMfaj5MiMfcSQ==
|
||||
|
||||
"@types/tinycolor2@^1.4.1":
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.1.tgz#2f5670c9d1d6e558897a810ed284b44918fc1253"
|
||||
integrity sha512-25L/RL5tqZkquKXVHM1fM2bd23qjfbcPpAZ2N/H05Y45g3UEi+Hw8CbDV28shKY8gH1SHiLpZSxPI1lacqdpGg==
|
||||
|
||||
"@types/uglify-js@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.3.tgz#801a5ca1dc642861f47c46d14b700ed2d610840b"
|
||||
|
||||
Reference in New Issue
Block a user