Commit 66a13b28 by Torkel Ödegaard

Merge branch 'grafana-lib'

parents 513434ca 98d26354
......@@ -6,7 +6,9 @@ module.exports = {
},
"moduleDirectories": ["node_modules", "public"],
"roots": [
"<rootDir>/public"
"<rootDir>/public/app",
"<rootDir>/public/test",
"<rootDir>/packages"
],
"testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
......
{
"private": true,
"author": {
"name": "Torkel Ödegaard",
"company": "Grafana Labs"
......@@ -11,19 +12,21 @@
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.1.0",
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
"@types/d3": "^4.10.1",
"@types/enzyme": "^3.1.13",
"@types/jest": "^23.3.2",
"@types/jquery": "^1.10.35",
"@types/node": "^8.0.31",
"@types/react": "^16.7.6",
"@types/react-custom-scrollbars": "^4.0.5",
"@types/react-dom": "^16.0.9",
"@types/react-select": "^2.0.4",
"@types/classnames": "^2.2.6",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
"axios": "^0.17.1",
......@@ -92,6 +95,7 @@
"tslib": "^1.9.3",
"tslint": "^5.8.0",
"tslint-loader": "^3.5.3",
"tslint-react": "^3.6.0",
"typescript": "^3.0.3",
"uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "4.19.1",
......@@ -108,15 +112,25 @@
"watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js",
"build": "grunt build",
"test": "grunt test",
"lint": "tslint -c tslint.json --project tsconfig.json",
"tslint": "tslint -c tslint.json --project tsconfig.json",
"typecheck": "tsc --noEmit",
"jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"precommit": "lint-staged && grunt precommit"
},
"lint-staged": {
"*.{ts,tsx}": ["prettier --write", "git add"],
"*.scss": ["prettier --write", "git add"],
"*pkg/**/*.go": ["gofmt -w -s", "git add"]
"*.{ts,tsx}": [
"prettier --write",
"git add"
],
"*.scss": [
"prettier --write",
"git add"
],
"*pkg/**/*.go": [
"gofmt -w -s",
"git add"
]
},
"prettier": {
"trailingComma": "es5",
......@@ -126,6 +140,7 @@
"license": "Apache-2.0",
"dependencies": {
"@babel/polyfill": "^7.0.0",
"@torkelo/react-select": "2.1.1",
"angular": "1.6.6",
"angular-bindonce": "0.3.1",
"angular-native-dragdrop": "1.2.2",
......@@ -133,7 +148,7 @@
"angular-sanitize": "1.6.6",
"baron": "^3.0.3",
"brace": "^0.10.0",
"classnames": "^2.2.5",
"classnames": "^2.2.6",
"clipboard": "^1.7.1",
"d3": "^4.11.0",
"d3-scale-chromatic": "^1.3.0",
......@@ -152,10 +167,9 @@
"react-custom-scrollbars": "^4.2.1",
"react-dom": "^16.6.3",
"react-grid-layout": "0.16.6",
"react-popper": "^1.3.0",
"react-highlight-words": "0.11.0",
"react-popper": "^1.3.0",
"react-redux": "^5.0.7",
"@torkelo/react-select": "2.1.1",
"react-sizeme": "^2.3.6",
"react-table": "^6.8.6",
"react-transition-group": "^2.2.1",
......@@ -172,11 +186,19 @@
"slate-react": "^0.12.4",
"tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
"tinycolor2": "^1.4.1",
"tslint-react": "^3.6.0"
"tinycolor2": "^1.4.1"
},
"resolutions": {
"caniuse-db": "1.0.30000772",
"**/@types/react": "16.7.6"
},
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/@types/*",
"**/@types/*/**"
]
}
}
# Shared build scripts
Shared build scripts for plugins & internal packages.
{
"name": "@grafana/build",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"tslint": "echo \"Nothing to do\"",
"typecheck": "echo \"Nothing to do\""
},
"author": "",
"license": "ISC"
}
# Grafana (WIP) shared component library
Used by internal & external plugins.
{
"name": "@grafana/ui",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"tslint": "tslint -c tslint.json --project tsconfig.json",
"typecheck": "tsc --noEmit"
},
"author": "",
"license": "ISC",
"dependencies": {
"@torkelo/react-select": "2.1.1",
"classnames": "^2.2.5",
"jquery": "^3.2.1",
"lodash": "^4.17.10",
"moment": "^2.22.2",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-highlight-words": "0.11.0",
"react-popper": "^1.3.0",
"react-transition-group": "^2.2.1",
"react-virtualized": "^9.21.0"
},
"devDependencies": {
"@types/jest": "^23.3.2",
"@types/lodash": "^4.14.119",
"@types/react": "^16.7.6",
"@types/classnames": "^2.2.6",
"@types/jquery": "^1.10.35",
"typescript": "^3.2.2"
}
}
import React from 'react';
import DeleteButton from './DeleteButton';
import { DeleteButton } from './DeleteButton';
import { shallow } from 'enzyme';
describe('DeleteButton', () => {
let wrapper;
let deleted;
let wrapper: any;
let deleted: any;
beforeAll(() => {
deleted = false;
......@@ -12,7 +12,8 @@ describe('DeleteButton', () => {
function deleteItem() {
deleted = true;
}
wrapper = shallow(<DeleteButton onConfirmDelete={() => deleteItem()} />);
wrapper = shallow(<DeleteButton onConfirm={() => deleteItem()} />);
});
it('should show confirm delete when clicked', () => {
......
import React, { PureComponent } from 'react';
import React, { PureComponent, SyntheticEvent } from 'react';
export interface DeleteButtonProps {
onConfirmDelete();
interface Props {
onConfirm(): void;
}
export interface DeleteButtonStates {
interface State {
showConfirm: boolean;
}
export default class DeleteButton extends PureComponent<DeleteButtonProps, DeleteButtonStates> {
state: DeleteButtonStates = {
export class DeleteButton extends PureComponent<Props, State> {
state: State = {
showConfirm: false,
};
onClickDelete = event => {
onClickDelete = (event: SyntheticEvent) => {
if (event) {
event.preventDefault();
}
......@@ -23,7 +23,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
});
};
onClickCancel = event => {
onClickCancel = (event: SyntheticEvent) => {
if (event) {
event.preventDefault();
}
......@@ -33,7 +33,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
};
render() {
const onClickConfirm = this.props.onConfirmDelete;
const { onConfirm } = this.props;
let showConfirm;
let showDeleteButton;
......@@ -55,7 +55,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
<a className="btn btn-small" onClick={this.onClickCancel}>
Cancel
</a>
<a className="btn btn-danger btn-small" onClick={onClickConfirm}>
<a className="btn btn-danger btn-small" onClick={onConfirm}>
Confirm Delete
</a>
</span>
......
@import 'DeleteButton/DeleteButton';
export { DeleteButton } from './DeleteButton/DeleteButton';
import React, { SFC, ReactNode } from 'react';
import classNames from 'classnames';
interface Props {
children: ReactNode;
htmlFor?: string;
className?: string;
isFocused?: boolean;
isInvalid?: boolean;
}
export const GfFormLabel: SFC<Props> = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => {
const classes = classNames('gf-form-label', className, {
'gf-form-label--is-focused': isFocused,
'gf-form-label--is-invalid': isInvalid,
});
return (
<label className={classes} {...rest} htmlFor={htmlFor}>
{children}
</label>
);
};
export { GfFormLabel } from './GfFormLabel/GfFormLabel';
@import 'components/index';
export * from './components';
export * from './visualizations';
export * from './types';
export * from './utils';
export * from './forms';
export * from './series';
export * from './time';
export * from './panel';
interface JQueryPlot {
(element: HTMLElement | JQuery, data: any, options: any): void;
plugins: any[];
}
interface JQueryStatic {
plot: JQueryPlot;
}
interface JQuery {
place_tt: any;
modal: any;
tagsinput: any;
typeahead: any;
accessKey: any;
tooltip: any;
}
import { TimeSeries, LoadingState } from './series';
import { TimeRange } from './time';
export interface PanelProps<T = any> {
timeSeries: TimeSeries[];
timeRange: TimeRange;
loading: LoadingState;
options: T;
renderCounter: number;
width: number;
height: number;
}
export interface PanelOptionsProps<T = any> {
options: T;
onChange: (options: T) => void;
}
export interface PanelSize {
width: number;
height: number;
}
export interface PanelMenuItem {
type?: 'submenu' | 'divider';
text?: string;
iconClassName?: string;
onClick?: () => void;
shortcut?: string;
subMenu?: PanelMenuItem[];
}
export enum LoadingState {
NotStarted = 'NotStarted',
Loading = 'Loading',
Done = 'Done',
Error = 'Error',
}
export type TimeSeriesValue = number | null;
export type TimeSeriesPoints = TimeSeriesValue[][];
export interface TimeSeries {
target: string;
datapoints: TimeSeriesPoints;
unit?: string;
}
/** View model projection of a time series */
export interface TimeSeriesVM {
label: string;
color: string;
data: TimeSeriesValue[][];
stats: TimeSeriesStats;
}
export interface TimeSeriesStats {
total: number | null;
max: number | null;
min: number | null;
logmin: number;
avg: number | null;
current: number | null;
first: number | null;
delta: number;
diff: number | null;
range: number | null;
timeStep: number;
count: number;
allIsNull: boolean;
allIsZero: boolean;
}
export enum NullValueMode {
Null = 'null',
Ignore = 'connected',
AsZero = 'null as zero',
}
/** View model projection of many time series */
export interface TimeSeriesVMs {
[index: number]: TimeSeriesVM;
length: number;
}
import { Moment } from 'moment';
export interface RawTimeRange {
from: Moment | string;
to: Moment | string;
}
export interface TimeRange {
from: Moment;
to: Moment;
raw: RawTimeRange;
}
export interface IntervalValues {
interval: string; // 10s,5m
intervalMs: number;
}
export * from './processTimeSeries';
// Libraries
import _ from 'lodash';
// Types
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
interface Options {
timeSeries: TimeSeries[];
nullValueMode: NullValueMode;
colorPalette: string[];
}
export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: Options): TimeSeriesVMs {
const vmSeries = timeSeries.map((item, index) => {
const colorIndex = index % colorPalette.length;
const label = item.target;
const result = [];
// stat defaults
let total = 0;
let max: TimeSeriesValue = -Number.MAX_VALUE;
let min: TimeSeriesValue = Number.MAX_VALUE;
let logmin = Number.MAX_VALUE;
let avg: TimeSeriesValue = null;
let current: TimeSeriesValue = null;
let first: TimeSeriesValue = null;
let delta: TimeSeriesValue = 0;
let diff: TimeSeriesValue = null;
let range: TimeSeriesValue = null;
let timeStep = Number.MAX_VALUE;
let allIsNull = true;
let allIsZero = true;
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
const nullAsZero = nullValueMode === NullValueMode.AsZero;
let currentTime: TimeSeriesValue = null;
let currentValue: TimeSeriesValue = null;
let nonNulls = 0;
let previousTime: TimeSeriesValue = null;
let previousValue = 0;
let previousDeltaUp = true;
for (let i = 0; i < item.datapoints.length; i++) {
currentValue = item.datapoints[i][0];
currentTime = item.datapoints[i][1];
if (typeof currentTime !== 'number') {
continue;
}
if (typeof currentValue !== 'number') {
continue;
}
// Due to missing values we could have different timeStep all along the series
// so we have to find the minimum one (could occur with aggregators such as ZimSum)
if (previousTime !== null && currentTime !== null) {
const currentStep = currentTime - previousTime;
if (currentStep < timeStep) {
timeStep = currentStep;
}
}
previousTime = currentTime;
if (currentValue === null) {
if (ignoreNulls) {
continue;
}
if (nullAsZero) {
currentValue = 0;
}
}
if (currentValue !== null) {
if (_.isNumber(currentValue)) {
total += currentValue;
allIsNull = false;
nonNulls++;
}
if (currentValue > max) {
max = currentValue;
}
if (currentValue < min) {
min = currentValue;
}
if (first === null) {
first = currentValue;
} else {
if (previousValue > currentValue) {
// counter reset
previousDeltaUp = false;
if (i === item.datapoints.length - 1) {
// reset on last
delta += currentValue;
}
} else {
if (previousDeltaUp) {
delta += currentValue - previousValue; // normal increment
} else {
delta += currentValue; // account for counter reset
}
previousDeltaUp = true;
}
}
previousValue = currentValue;
if (currentValue < logmin && currentValue > 0) {
logmin = currentValue;
}
if (currentValue !== 0) {
allIsZero = false;
}
}
result.push([currentTime, currentValue]);
}
if (max === -Number.MAX_VALUE) {
max = null;
}
if (min === Number.MAX_VALUE) {
min = null;
}
if (result.length && !allIsNull) {
avg = total / nonNulls;
current = result[result.length - 1][1];
if (current === null && result.length > 1) {
current = result[result.length - 2][1];
}
}
if (max !== null && min !== null) {
range = max - min;
}
if (current !== null && first !== null) {
diff = current - first;
}
const count = result.length;
return {
data: result,
label: label,
color: colorPalette[colorIndex],
stats: {
total,
min,
max,
current,
logmin,
avg,
diff,
delta,
timeStep,
range,
count,
first,
allIsZero,
allIsNull,
},
};
});
return vmSeries;
}
// Libraries
import $ from 'jquery';
import React, { PureComponent } from 'react';
import 'vendor/flot/jquery.flot';
import 'vendor/flot/jquery.flot.time';
// Types
import { TimeRange, TimeSeriesVMs } from 'app/types';
import { TimeRange, TimeSeriesVMs } from '../../types';
interface GraphProps {
timeSeries: TimeSeriesVMs;
......@@ -24,7 +22,7 @@ export class Graph extends PureComponent<GraphProps> {
showBars: false,
};
element: HTMLElement;
element: HTMLElement | null;
componentDidUpdate() {
this.draw();
......@@ -35,6 +33,10 @@ export class Graph extends PureComponent<GraphProps> {
}
draw() {
if (this.element === null) {
return;
}
const { width, timeSeries, timeRange, showLines, showBars, showPoints } = this.props;
if (!width) {
......@@ -76,7 +78,7 @@ export class Graph extends PureComponent<GraphProps> {
max: max,
label: 'Datetime',
ticks: ticks,
timeformat: time_format(ticks, min, max),
timeformat: timeFormat(ticks, min, max),
},
grid: {
minBorderMargin: 0,
......@@ -109,7 +111,7 @@ export class Graph extends PureComponent<GraphProps> {
}
// Copied from graph.ts
function time_format(ticks, min, max) {
function timeFormat(ticks: number, min: number, max: number): string {
if (min && max && ticks) {
const range = max - min;
const secPerTick = range / ticks / 1000;
......
export { Graph } from './Graph/Graph';
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"dist"
],
"compilerOptions": {
"rootDir": ".",
"module": "esnext",
"outDir": "dist",
"declaration": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}
{
"extends": "../../tslint.json"
}
import React, { PureComponent, ReactNode, ReactElement } from 'react';
import { Label } from './Label';
import { uniqueId } from 'lodash';
interface Props {
label?: ReactNode;
labelClassName?: string;
id?: string;
children: ReactElement<any>;
}
export class Element extends PureComponent<Props> {
elementId: string = this.props.id || uniqueId('form-element-');
get elementLabel() {
const { label, labelClassName } = this.props;
if (label) {
return (
<Label htmlFor={this.elementId} className={labelClassName}>
{label}
</Label>
);
}
return null;
}
get children() {
const { children } = this.props;
return React.cloneElement(children, { id: this.elementId });
}
render() {
return (
<div className="our-custom-wrapper-class">
{this.elementLabel}
{this.children}
</div>
);
}
}
import React, { PureComponent, ReactNode } from 'react';
interface Props {
children: ReactNode;
htmlFor?: string;
className?: string;
}
export class Label extends PureComponent<Props> {
render() {
const { children, htmlFor, className } = this.props;
return (
<label className={`custom-label-class ${className || ''}`} htmlFor={htmlFor}>
{children}
</label>
);
}
}
export { Element } from './Element';
export { Input } from './Input';
export { Label } from './Label';
......@@ -69,7 +69,7 @@ function bootstrapTagsinput() {
},
});
select.on('itemAdded', event => {
select.on('itemAdded', (event: any) => {
if (scope.model.indexOf(event.item) === -1) {
scope.model.push(event.item);
if (scope.onTagsUpdated) {
......@@ -85,7 +85,7 @@ function bootstrapTagsinput() {
setColor(event.item, tagElement);
});
select.on('itemRemoved', event => {
select.on('itemRemoved', (event: any) => {
const idx = scope.model.indexOf(event.item);
if (idx !== -1) {
scope.model.splice(idx, 1);
......
......@@ -9,7 +9,8 @@ import { parse as parseDate } from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
import { DataQuery, RawTimeRange, IntervalValues, DataSourceApi } from 'app/types/series';
import { DataQuery, DataSourceApi } from 'app/types/series';
import { RawTimeRange, IntervalValues } from '@grafana/ui';
export const DEFAULT_RANGE = {
from: 'now-6h',
......
import _ from 'lodash';
import moment from 'moment';
import { RawTimeRange } from 'app/types/series';
import { RawTimeRange } from '@grafana/ui';
import * as dateMath from './datemath';
......
import React, { PureComponent } from 'react';
import Highlighter from 'react-highlight-words';
import classNames from 'classnames/bind';
import classNames from 'classnames';
import { AlertRule } from '../../types';
export interface Props {
......@@ -23,7 +23,7 @@ class AlertRuleItem extends PureComponent<Props> {
render() {
const { rule, onTogglePause } = this.props;
const stateClass = classNames({
const iconClassName = classNames({
fa: true,
'fa-play': rule.state === 'paused',
'fa-pause': rule.state !== 'paused',
......@@ -55,7 +55,7 @@ class AlertRuleItem extends PureComponent<Props> {
title="Pausing an alert rule prevents it from executing"
onClick={onTogglePause}
>
<i className={stateClass} />
<i className={iconClassName} />
</button>
<a className="btn btn-small btn-inverse alert-list__btn width-2" href={ruleUrl} title="Edit alert rule">
<i className="icon-gf icon-gf-settings" />
......
......@@ -13,7 +13,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import { DeleteButton } from '@grafana/ui';
export interface Props {
navModel: NavModel;
......@@ -224,7 +224,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
<td>{key.name}</td>
<td>{key.role}</td>
<td>
<DeleteButton onConfirmDelete={() => this.onDeleteApiKey(key)} />
<DeleteButton onConfirm={() => this.onDeleteApiKey(key)} />
</td>
</tr>
);
......
......@@ -8,7 +8,8 @@ import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource
import kbn from 'app/core/utils/kbn';
// Types
import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types';
import { DataQueryOptions, DataQueryResponse } from 'app/types';
import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui';
interface RenderProps {
loading: LoadingState;
......
......@@ -16,7 +16,8 @@ import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
// Types
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelPlugin, TimeRange } from 'app/types';
import { PanelPlugin } from 'app/types';
import { TimeRange } from '@grafana/ui';
export interface Props {
panel: PanelModel;
......
......@@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
import { PanelMenuItem } from 'app/types/panel';
import { PanelMenuItem } from '@grafana/ui';
export interface Props {
panel: PanelModel;
......
import React, { SFC } from 'react';
import { PanelMenuItem } from 'app/types/panel';
import { PanelMenuItem } from '@grafana/ui';
interface Props {
children: any;
......
// Libraries
import _ from 'lodash';
import React, { PureComponent } from 'react';
import { PanelPlugin, PanelProps } from 'app/types';
// Types
import { PanelProps } from '@grafana/ui';
import { PanelPlugin } from 'app/types';
interface Props {
pluginId: string;
......
......@@ -10,6 +10,7 @@ import { Input } from 'app/core/components/Form';
import { EventsWithValidation } from 'app/core/components/Form/Input';
import { InputStatus } from 'app/core/components/Form/Input';
import DataSourceOption from './DataSourceOption';
import { GfFormLabel } from '@grafana/ui';
// Types
import { PanelModel } from '../panel_model';
......@@ -163,7 +164,7 @@ export class QueryOptions extends PureComponent<Props, State> {
{this.renderOptions()}
<div className="gf-form">
<span className="gf-form-label">Relative time</span>
<GfFormLabel>Relative time</GfFormLabel>
<Input
type="text"
className="width-6"
......
......@@ -6,9 +6,9 @@ import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import coreModule from 'app/core/core_module';
import * as dateMath from 'app/core/utils/datemath';
// Types
import { TimeRange } from 'app/types';
// Types
import { TimeRange } from '@grafana/ui';
export class TimeSrv {
time: any;
......
......@@ -4,7 +4,7 @@ import { store } from 'app/store/store';
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelMenuItem } from 'app/types/panel';
import { PanelMenuItem } from '@grafana/ui';
export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
const onViewPanel = () => {
......
......@@ -4,7 +4,7 @@ import store from 'app/core/store';
// Models
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { TimeRange } from 'app/types/series';
import { TimeRange } from '@grafana/ui';
// Utils
import { isString as _isString } from 'lodash';
......
import React, { PureComponent } from 'react';
import classNames from 'classnames/bind';
import classNames from 'classnames';
import DataSourcesListItem from './DataSourcesListItem';
import { DataSource } from 'app/types';
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
......
......@@ -11,7 +11,8 @@ import {
QueryHintGetter,
QueryHint,
} from 'app/types/explore';
import { TimeRange, DataQuery } from 'app/types/series';
import { TimeRange } from '@grafana/ui';
import { DataQuery } from 'app/types/series';
import store from 'app/core/store';
import {
DEFAULT_RANGE,
......
......@@ -8,7 +8,7 @@ import 'vendor/flot/jquery.flot.time';
import 'vendor/flot/jquery.flot.selection';
import 'vendor/flot/jquery.flot.stack';
import { RawTimeRange } from 'app/types/series';
import { RawTimeRange } from '@grafana/ui';
import * as dateMath from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2';
......
......@@ -4,7 +4,7 @@ import Highlighter from 'react-highlight-words';
import classnames from 'classnames';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange } from 'app/types/series';
import { RawTimeRange } from '@grafana/ui';
import {
LogsDedupDescription,
LogsDedupStrategy,
......
......@@ -3,7 +3,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa
import { Emitter } from 'app/core/utils/emitter';
import { getIntervals } from 'app/core/utils/explore';
import { DataQuery } from 'app/types';
import { RawTimeRange } from 'app/types/series';
import { RawTimeRange } from '@grafana/ui';
import { getTimeSrv } from 'app/features/dashboard/time_srv';
import 'app/features/plugins/plugin_loader';
......
......@@ -7,7 +7,7 @@ import { Emitter } from 'app/core/utils/emitter';
import QueryEditor from './QueryEditor';
import QueryTransactionStatus from './QueryTransactionStatus';
import { DataSource, DataQuery } from 'app/types';
import { RawTimeRange } from 'app/types/series';
import { RawTimeRange } from '@grafana/ui';
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
......
......@@ -3,7 +3,7 @@ import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, TimeRange } from 'app/types/series';
import { RawTimeRange, TimeRange } from '@grafana/ui';
const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export const DEFAULT_RANGE = {
......
import React, { SFC } from 'react';
import classNames from 'classnames/bind';
import classNames from 'classnames';
import PluginListItem from './PluginListItem';
import { Plugin } from 'app/types';
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
......
......@@ -26,6 +26,7 @@ import * as ticks from 'app/core/utils/ticks';
import impressionSrv from 'app/core/services/impression_srv';
import builtInPlugins from './built_in_plugins';
import * as d3 from 'd3';
import * as grafanaUI from '@grafana/ui';
// rxjs
import { Observable } from 'rxjs/Observable';
......@@ -71,6 +72,7 @@ function exposeToPlugin(name: string, component: any) {
});
}
exposeToPlugin('@grafana/ui', grafanaUI);
exposeToPlugin('lodash', _);
exposeToPlugin('moment', moment);
exposeToPlugin('jquery', jquery);
......
......@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import { DeleteButton } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { NavModel, Team } from '../../types';
......@@ -58,7 +58,7 @@ export class TeamList extends PureComponent<Props, any> {
<a href={teamUrl}>{team.memberCount}</a>
</td>
<td className="text-right">
<DeleteButton onConfirmDelete={() => this.deleteTeam(team)} />
<DeleteButton onConfirm={() => this.deleteTeam(team)} />
</td>
</tr>
);
......
......@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { UserPicker } from 'app/core/components/Select/UserPicker';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import { DeleteButton } from '@grafana/ui';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember, User } from 'app/types';
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
......@@ -76,7 +76,7 @@ export class TeamMembers extends PureComponent<Props, State> {
<td>{member.email}</td>
{syncEnabled && this.renderLabels(member.labels)}
<td className="text-right">
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
<DeleteButton onConfirm={() => this.onRemoveMember(member)} />
</td>
</tr>
);
......
......@@ -124,7 +124,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -174,7 +174,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -224,7 +224,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -274,7 +274,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -324,7 +324,7 @@ exports[`Render should render teams table 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......
......@@ -204,7 +204,7 @@ exports[`Render should render team members 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -229,7 +229,7 @@ exports[`Render should render team members 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -254,7 +254,7 @@ exports[`Render should render team members 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -279,7 +279,7 @@ exports[`Render should render team members 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -304,7 +304,7 @@ exports[`Render should render team members 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -441,7 +441,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -482,7 +482,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -523,7 +523,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -564,7 +564,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......@@ -605,7 +605,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
onConfirm={[Function]}
/>
</td>
</tr>
......
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames/bind';
import classNames from 'classnames';
import { setUsersSearchQuery } from './state/actions';
import { getInviteesCount, getUsersSearchQuery } from './state/selectors';
......
......@@ -2,7 +2,8 @@ import React from 'react';
import { shallow } from 'enzyme';
import Thresholds from './Thresholds';
import { defaultProps, OptionsProps } from './module';
import { BasicGaugeColor, PanelOptionsProps } from 'app/types';
import { BasicGaugeColor } from 'app/types';
import { PanelOptionsProps } from '@grafana/ui';
const setup = (propOverrides?: object) => {
const props: PanelOptionsProps<OptionsProps> = {
......
......@@ -5,15 +5,8 @@ import ValueOptions from './ValueOptions';
import GaugeOptions from './GaugeOptions';
import Thresholds from './Thresholds';
import ValueMappings from './ValueMappings';
import {
BasicGaugeColor,
NullValueMode,
PanelOptionsProps,
PanelProps,
RangeMap,
Threshold,
ValueMap,
} from 'app/types';
import { PanelOptionsProps, PanelProps, NullValueMode } from '@grafana/ui';
import { BasicGaugeColor, RangeMap, Threshold, ValueMap } from 'app/types';
export interface OptionsProps {
baseColor: string;
......
......@@ -114,6 +114,7 @@ describe('grafanaGraph', () => {
{}
);
// @ts-ignore
$.plot = ctrl.plot = jest.fn();
scope.ctrl = ctrl;
......
......@@ -6,7 +6,7 @@ import React, { PureComponent } from 'react';
import { Switch } from 'app/core/components/Switch/Switch';
// Types
import { PanelOptionsProps } from 'app/types';
import { PanelOptionsProps } from '@grafana/ui';
import { Options } from './types';
export class GraphOptions extends PureComponent<PanelOptionsProps<Options>> {
......
// Libraries
import _ from 'lodash';
import React, { PureComponent } from 'react';
import colors from 'app/core/utils/colors';
// Components
import Graph from 'app/viz/Graph';
// Services & Utils
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
// Types
import { PanelProps, NullValueMode } from 'app/types';
// Components & Types
import { Graph, PanelProps, NullValueMode, processTimeSeries } from '@grafana/ui';
import { Options } from './types';
interface Props extends PanelProps<Options> {}
......@@ -23,9 +18,10 @@ export class GraphPanel extends PureComponent<Props> {
const { timeSeries, timeRange, width, height } = this.props;
const { showLines, showBars, showPoints } = this.props.options;
const vmSeries = getTimeSeriesVMs({
const vmSeries = processTimeSeries({
timeSeries: timeSeries,
nullValueMode: NullValueMode.Ignore,
colorPalette: colors,
});
return (
......
import React, { PureComponent } from 'react';
import { PanelProps } from 'app/types';
import { PanelProps } from '@grafana/ui';
export class Text2 extends PureComponent<PanelProps> {
constructor(props) {
......
import { Value } from 'slate';
import { DataQuery, RawTimeRange } from './series';
import { DataQuery } from './series';
import { RawTimeRange } from '@grafana/ui';
import TableModel from 'app/core/table_model';
import { LogsModel } from 'app/core/logs_model';
import { DataSourceSelectItem } from 'app/types/datasources';
......
......@@ -8,20 +8,8 @@ import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
import { Invitee, OrgUser, User, UsersState, UserState } from './user';
import { DataSource, DataSourceSelectItem, DataSourcesState } from './datasources';
import {
TimeRange,
LoadingState,
TimeSeries,
TimeSeriesVM,
TimeSeriesVMs,
TimeSeriesStats,
NullValueMode,
DataQuery,
DataQueryResponse,
DataQueryOptions,
IntervalValues,
} from './series';
import { BasicGaugeColor, MappingType, PanelProps, PanelOptionsProps, RangeMap, Threshold, ValueMap } from './panel';
import { DataQuery, DataQueryResponse, DataQueryOptions } from './series';
import { BasicGaugeColor, MappingType, RangeMap, Threshold, ValueMap } from './panel';
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
import { Organization, OrganizationState } from './organization';
import {
......@@ -68,16 +56,7 @@ export {
OrgUser,
User,
UsersState,
TimeRange,
LoadingState,
PanelPlugin,
PanelProps,
PanelOptionsProps,
TimeSeries,
TimeSeriesVM,
TimeSeriesVMs,
NullValueMode,
TimeSeriesStats,
DataQuery,
DataQueryResponse,
DataQueryOptions,
......@@ -95,7 +74,6 @@ export {
ValidationRule,
ValueMap,
RangeMap,
IntervalValues,
MappingType,
BasicGaugeColor,
};
......
interface JQueryPlot {
(element: HTMLElement | JQuery, data: any, options: any): void;
plugins: any[];
}
interface JQueryStatic {
plot: JQueryPlot;
}
interface JQuery {
place_tt: any;
modal: any;
tagsinput: any;
typeahead: any;
accessKey: any;
tooltip: any;
}
import { LoadingState, TimeSeries, TimeRange } from './series';
export interface PanelProps<T = any> {
timeSeries: TimeSeries[];
timeRange: TimeRange;
loading: LoadingState;
options: T;
renderCounter: number;
width: number;
height: number;
}
export interface PanelOptionsProps<T = any> {
options: T;
onChange: (options: T) => void;
}
export interface PanelSize {
width: number;
height: number;
}
export interface PanelMenuItem {
type?: 'submenu' | 'divider';
text?: string;
iconClassName?: string;
onClick?: () => void;
shortcut?: string;
subMenu?: PanelMenuItem[];
}
export interface Threshold {
index: number;
value: number;
......
import { ComponentClass } from 'react';
import { PanelProps, PanelOptionsProps } from './panel';
import { PanelProps, PanelOptionsProps } from '@grafana/ui';
export interface PluginExports {
Datasource?: any;
......
import { Moment } from 'moment';
import { PluginMeta } from './plugins';
export enum LoadingState {
NotStarted = 'NotStarted',
Loading = 'Loading',
Done = 'Done',
Error = 'Error',
}
export interface RawTimeRange {
from: Moment | string;
to: Moment | string;
}
export interface TimeRange {
from: Moment;
to: Moment;
raw: RawTimeRange;
}
export interface IntervalValues {
interval: string; // 10s,5m
intervalMs: number;
}
export type TimeSeriesValue = string | number | null;
export type TimeSeriesPoints = TimeSeriesValue[][];
export interface TimeSeries {
target: string;
datapoints: TimeSeriesPoints;
unit?: string;
}
/** View model projection of a time series */
export interface TimeSeriesVM {
label: string;
color: string;
data: TimeSeriesValue[][];
stats: TimeSeriesStats;
}
export interface TimeSeriesStats {
total: number;
max: number;
min: number;
logmin: number;
avg: number | null;
current: number | null;
first: number | null;
delta: number;
diff: number | null;
range: number | null;
timeStep: number;
count: number;
allIsNull: boolean;
allIsZero: boolean;
}
export enum NullValueMode {
Null = 'null',
Ignore = 'connected',
AsZero = 'null as zero',
}
/** View model projection of many time series */
export interface TimeSeriesVMs {
[index: number]: TimeSeriesVM;
length: number;
}
import { TimeSeries, TimeRange, RawTimeRange } from '@grafana/ui';
export interface DataQueryResponse {
data: TimeSeries[];
......
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { BasicGaugeColor, MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } from 'app/types';
import { BasicGaugeColor, MappingType, RangeMap, Threshold, ValueMap } from 'app/types';
import { TimeSeriesVMs } from '@grafana/ui';
import config from '../core/config';
import kbn from '../core/utils/kbn';
......
......@@ -5,7 +5,7 @@ import _ from 'lodash';
import colors from 'app/core/utils/colors';
// Types
import { TimeSeries, TimeSeriesVMs, NullValueMode } from 'app/types';
import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui';
interface Options {
timeSeries: TimeSeries[];
......
......@@ -38,6 +38,9 @@
@import 'layout/lists';
@import 'layout/page';
// LOAD @grafana/ui components
@import '../../packages/grafana-ui/src/index';
// COMPONENTS
@import 'components/scrollbar';
@import 'components/cards';
......@@ -98,7 +101,6 @@
@import 'components/form_select_box';
@import 'components/panel_editor';
@import 'components/toolbar';
@import 'components/delete_button';
@import 'components/add_data_source.scss';
@import 'components/page_loader';
@import 'components/thresholds';
......
......@@ -10,16 +10,26 @@ module.exports = function (grunt) {
grunt.registerTask('test', [
'sasslint',
'exec:tsc',
'exec:tslint',
'exec:jest',
'tslint',
'typecheck',
"exec:jest",
'no-only-tests'
]);
grunt.registerTask('tslint', [
'newer:exec:tslintPackages',
'newer:exec:tslintRoot',
]);
grunt.registerTask('typecheck', [
'newer:exec:typecheckPackages',
'newer:exec:typecheckRoot',
]);
grunt.registerTask('precommit', [
'sasslint',
'newer:exec:tslint',
'newer:exec:tsc',
'newer:sasslint',
'typecheck',
'tslint',
'no-only-tests'
]);
......
......@@ -2,12 +2,20 @@ module.exports = function (config, grunt) {
'use strict';
return {
tslint: {
command: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json',
tslintPackages: {
command: 'yarn workspaces run tslint',
src: ['packages/**/*.ts*'],
},
tslintRoot: {
command: 'yarn run tslint',
src: ['public/app/**/*.ts*'],
},
tsc: {
command: 'yarn tsc --noEmit',
typecheckPackages: {
command: 'yarn workspaces run typecheck',
src: ['packages/**/*.ts*'],
},
typecheckRoot: {
command: 'yarn run typecheck',
src: ['public/app/**/*.ts*'],
},
jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2',
......
......@@ -4,6 +4,7 @@ module.exports = function(config) {
options: {
configFile: 'public/sass/.sass-lint.yml',
},
// src: ['public/sass#<{(||)}>#*'],
target: [
'public/sass/*.scss',
'public/sass/components/*.scss',
......
module.exports = function(config, grunt) {
'use strict'
// dummy to avoid template compile error
return {
source: {
files: {
src: ""
}
}
};
};
......@@ -26,6 +26,7 @@
"noUnusedLocals": true,
"baseUrl": "public",
"pretty": true,
"typeRoots": ["node_modules/@types", "types"],
"paths": {
"app": ["app"]
}
......
......@@ -804,6 +804,11 @@
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.9.tgz#b5990152604c2ada749b7f88cab3476f21f39d7b"
integrity sha512-q6LuBI0t5u04f0Q4/R+cGBqIbZMtJkVvCSF+nTfFBBdQqQvJR/mNHeWjRkszyLl7oyf2rDoKUYMEjTw5AV0hiw==
"@types/classnames@^2.2.6":
version "2.2.6"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.6.tgz#dbe8a666156d556ed018e15a4c65f08937c3f628"
integrity sha512-XHcYvVdbtAxVstjKxuULYqYaWIzHR15yr1pZj4fnGChuBVJlIAp9StJna0ZJNSgxPh4Nac2FL4JM3M11Tm6fqQ==
"@types/d3-array@*":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.3.tgz#dd141e3ba311485fffbf0792a1b01a7f2ec12dc1"
......@@ -1029,6 +1034,16 @@
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.4.tgz#cc43ae176a91dcb1504839b0b9d6659386cf0af5"
integrity sha512-46jSw0QMerCRkhJZbOwPA0Eb9T1p74HtECsfa0GXdgjkenSGhgvK96w+e2PEPu4GF0/brUK5WQKq/rUQQFyAxA==
"@types/jquery@^1.10.35":
version "1.10.35"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-1.10.35.tgz#4e5c2b1e5b3bf0b863efb8c5e70081f52e6c9518"
integrity sha512-SVtqEcudm7yjkTwoRA1gC6CNMhGDdMx4Pg8BPdiqI7bXXdCn1BPmtxgeWYQOgDxrq53/5YTlhq5ULxBEAlWIBg==
"@types/lodash@^4.14.119":
version "4.14.119"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39"
integrity sha512-Z3TNyBL8Vd/M9D9Ms2S3LmFq2sSMzahodD6rCS9V2N44HUMINb75jNkSuwAx7eo2ufqTdfOdtGQpNbieUjPQmw==
"@types/node@*":
version "10.11.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.11.4.tgz#e8bd933c3f78795d580ae41d86590bfc1f4f389d"
......@@ -1083,7 +1098,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@16.7.6", "@types/react@^16.1.0", "@types/react@^16.7.6":
"@types/react@*", "@types/react@^16.1.0", "@types/react@^16.7.6":
version "16.7.6"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040"
integrity sha512-QBUfzftr/8eg/q3ZRgf/GaDP6rTYc7ZNem+g4oZM38C9vXyV8AWRWaTQuW5yCoZTsfHrN7b3DeEiUnqH9SrnpA==
......@@ -3153,7 +3168,7 @@ caniuse-api@^1.5.2:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-db@1.0.30000772, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000772"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b"
integrity sha1-UarokXaChureSj2DGep21qAbUSs=
......@@ -14544,6 +14559,11 @@ typescript@^3.0.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.1.tgz#3362ba9dd1e482ebb2355b02dfe8bcd19a2c7c96"
integrity sha512-Veu0w4dTc/9wlWNf2jeRInNodKlcdLgemvPsrNpfu5Pq39sgfFjvIIgTsvUHCoLBnMhPoUA+tFxsXjU6VexVRQ==
typescript@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.2.tgz#fe8101c46aa123f8353523ebdcf5730c2ae493e5"
integrity sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==
ua-parser-js@^0.7.18:
version "0.7.19"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment