mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-26 06:04:05 -08:00
upgrade react-script to v4
Signed-off-by: Augustin Husson <husson.augustin@gmail.com>
This commit is contained in:
parent
9de62707b3
commit
5bcf2e6511
|
@ -6,7 +6,6 @@
|
||||||
"plugin:prettier/recommended"
|
"plugin:prettier/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/camelcase": "warn",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": ["off"],
|
"@typescript-eslint/explicit-function-return-type": ["off"],
|
||||||
"eol-last": [
|
"eol-last": [
|
||||||
"error",
|
"error",
|
||||||
|
@ -26,7 +25,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": ["prettier"],
|
||||||
"prettier"
|
"ignorePatterns": ["src/vendor/**"]
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
38196
web/ui/react-app/package-lock.json
generated
38196
web/ui/react-app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -24,7 +24,6 @@
|
||||||
"codemirror-promql": "^0.17.0",
|
"codemirror-promql": "^0.17.0",
|
||||||
"css.escape": "^1.5.1",
|
"css.escape": "^1.5.1",
|
||||||
"downshift": "^3.4.8",
|
"downshift": "^3.4.8",
|
||||||
"enzyme-to-json": "^3.4.3",
|
|
||||||
"i": "^0.3.6",
|
"i": "^0.3.6",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"jquery.flot.tooltip": "^0.9.0",
|
"jquery.flot.tooltip": "^0.9.0",
|
||||||
|
@ -37,20 +36,18 @@
|
||||||
"react-dom": "^16.7.0",
|
"react-dom": "^16.7.0",
|
||||||
"react-resize-detector": "^5.0.7",
|
"react-resize-detector": "^5.0.7",
|
||||||
"react-router-dom": "^5.2.1",
|
"react-router-dom": "^5.2.1",
|
||||||
"react-scripts": "3.4.4",
|
|
||||||
"react-test-renderer": "^16.9.0",
|
"react-test-renderer": "^16.9.0",
|
||||||
"reactstrap": "^8.9.0",
|
"reactstrap": "^8.9.0",
|
||||||
"sanitize-html": "^2.3.3",
|
"sanitize-html": "^2.3.3",
|
||||||
"sass": "1.32.10",
|
"sass": "1.32.10",
|
||||||
"tempusdominus-bootstrap-4": "^5.1.2",
|
"tempusdominus-bootstrap-4": "^5.1.2",
|
||||||
"tempusdominus-core": "^5.0.3",
|
"tempusdominus-core": "^5.0.3",
|
||||||
"typescript": "^3.3.3",
|
|
||||||
"use-media": "^1.4.0"
|
"use-media": "^1.4.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test --runInBand",
|
"test": "react-scripts test --runInBand --resetMocks=false",
|
||||||
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
|
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"",
|
"lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"",
|
||||||
|
@ -84,23 +81,18 @@
|
||||||
"@types/reactstrap": "^8.7.2",
|
"@types/reactstrap": "^8.7.2",
|
||||||
"@types/sanitize-html": "^1.20.2",
|
"@types/sanitize-html": "^1.20.2",
|
||||||
"@types/sinon": "^9.0.4",
|
"@types/sinon": "^9.0.4",
|
||||||
"@typescript-eslint/eslint-plugin": "2.x",
|
|
||||||
"@typescript-eslint/parser": "2.x",
|
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.10.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.1",
|
"enzyme-adapter-react-16": "^1.15.1",
|
||||||
"eslint": "6.x",
|
"enzyme-to-json": "^3.4.3",
|
||||||
"eslint-config-prettier": "^6.4.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-config-react-app": "^5.0.2",
|
"eslint-config-react-app": "^6.0.0",
|
||||||
"eslint-plugin-flowtype": "4.x",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-import": "2.x",
|
|
||||||
"eslint-plugin-jsx-a11y": "6.x",
|
|
||||||
"eslint-plugin-prettier": "^3.1.1",
|
|
||||||
"eslint-plugin-react": "7.x",
|
|
||||||
"eslint-plugin-react-hooks": "2.x",
|
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"mutationobserver-shim": "^0.3.7",
|
"mutationobserver-shim": "^0.3.7",
|
||||||
"prettier": "^1.18.2",
|
"prettier": "^2.3.2",
|
||||||
"sinon": "^9.0.3"
|
"react-scripts": "4.0.3",
|
||||||
|
"sinon": "^9.0.3",
|
||||||
|
"typescript": "^3.3.3"
|
||||||
},
|
},
|
||||||
"proxy": "http://localhost:9090",
|
"proxy": "http://localhost:9090",
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('App', () => {
|
||||||
TargetsPage,
|
TargetsPage,
|
||||||
TSDBStatusPage,
|
TSDBStatusPage,
|
||||||
PanelListPage,
|
PanelListPage,
|
||||||
].forEach(component => {
|
].forEach((component) => {
|
||||||
const c = app.find(component);
|
const c = app.find(component);
|
||||||
expect(c).toHaveLength(1);
|
expect(c).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ const MockCmp: React.FC = () => <div className="mock" />;
|
||||||
describe('Checkbox', () => {
|
describe('Checkbox', () => {
|
||||||
it('renders with subcomponents', () => {
|
it('renders with subcomponents', () => {
|
||||||
const checkBox = shallow(<Checkbox />);
|
const checkBox = shallow(<Checkbox />);
|
||||||
[FormGroup, Input, Label].forEach(component => expect(checkBox.find(component)).toHaveLength(1));
|
[FormGroup, Input, Label].forEach((component) => expect(checkBox.find(component)).toHaveLength(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes down the correct FormGroup props', () => {
|
it('passes down the correct FormGroup props', () => {
|
||||||
|
|
|
@ -23,11 +23,6 @@ describe('ToggleMoreLess', () => {
|
||||||
|
|
||||||
it('renders a show less btn if clicked', () => {
|
it('renders a show less btn if clicked', () => {
|
||||||
tggleBtn.find(Button).simulate('click');
|
tggleBtn.find(Button).simulate('click');
|
||||||
expect(
|
expect(tggleBtn.find(Button).render().text()).toEqual('show less');
|
||||||
tggleBtn
|
|
||||||
.find(Button)
|
|
||||||
.render()
|
|
||||||
.text()
|
|
||||||
).toEqual('show less');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,17 +23,17 @@ export const StartingContent: FC<StartingContentProps> = ({ status, isUnexpected
|
||||||
<div className="text-center m-3">
|
<div className="text-center m-3">
|
||||||
<div className="m-4">
|
<div className="m-4">
|
||||||
<h2>Starting up...</h2>
|
<h2>Starting up...</h2>
|
||||||
{status?.max! > 0 ? (
|
{status && status.max > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Replaying WAL ({status?.current}/{status?.max})
|
Replaying WAL ({status.current}/{status.max})
|
||||||
</p>
|
</p>
|
||||||
<Progress
|
<Progress
|
||||||
animated
|
animated
|
||||||
value={status?.current! - status?.min! + 1}
|
value={status.current - status.min + 1}
|
||||||
min={status?.min}
|
min={status.min}
|
||||||
max={status?.max! - status?.min! + 1}
|
max={status.max - status.min + 1}
|
||||||
color={status?.max === status?.current ? 'success' : undefined}
|
color={status.max === status.current ? 'success' : undefined}
|
||||||
style={{ width: '10%', margin: 'auto' }}
|
style={{ width: '10%', margin: 'auto' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,7 +43,9 @@ export const StartingContent: FC<StartingContentProps> = ({ status, isUnexpected
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const withStartingIndicator = <T extends {}>(Page: ComponentType<T>): FC<T> => ({ ...rest }) => {
|
export const withStartingIndicator =
|
||||||
|
<T extends Record<string, unknown>>(Page: ComponentType<T>): FC<T> =>
|
||||||
|
({ ...rest }) => {
|
||||||
const pathPrefix = usePathPrefix();
|
const pathPrefix = usePathPrefix();
|
||||||
const { ready, walReplayStatus, isUnexpected } = useFetchReadyInterval(pathPrefix);
|
const { ready, walReplayStatus, isUnexpected } = useFetchReadyInterval(pathPrefix);
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,11 @@ interface StatusIndicatorProps {
|
||||||
componentTitle?: string;
|
componentTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const withStatusIndicator = <T extends {}>(Component: ComponentType<T>): FC<StatusIndicatorProps & T> => ({
|
export const withStatusIndicator =
|
||||||
error,
|
<T extends Record<string, any>>( // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
isLoading,
|
Component: ComponentType<T>
|
||||||
customErrorMsg,
|
): FC<StatusIndicatorProps & T> =>
|
||||||
componentTitle,
|
({ error, isLoading, customErrorMsg, componentTitle, ...rest }) => {
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Alert color="danger">
|
<Alert color="danger">
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
|
|
||||||
const PathPrefixContext = React.createContext('');
|
const PathPrefixContext = React.createContext('');
|
||||||
|
|
||||||
function usePathPrefix() {
|
function usePathPrefix(): string {
|
||||||
return React.useContext(PathPrefixContext);
|
return React.useContext(PathPrefixContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,6 @@ export const ThemeContext = React.createContext<ThemeCtx>({
|
||||||
setTheme: (s: themeSetting) => {},
|
setTheme: (s: themeSetting) => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useTheme = () => {
|
export const useTheme = (): ThemeCtx => {
|
||||||
return React.useContext(ThemeContext);
|
return React.useContext(ThemeContext);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import jquery from 'jquery';
|
import jquery from 'jquery';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(window as any).jQuery = jquery;
|
(window as any).jQuery = jquery;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(window as any).moment = require('moment');
|
(window as any).moment = require('moment');
|
||||||
|
|
|
@ -10,19 +10,15 @@ export interface FetchState<T> {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchStateReady {
|
|
||||||
ready: boolean;
|
|
||||||
isUnexpected: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FetchStateReadyInterval {
|
export interface FetchStateReadyInterval {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
isUnexpected: boolean;
|
isUnexpected: boolean;
|
||||||
walReplayStatus: WALReplayStatus;
|
walReplayStatus: WALReplayStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFetch = <T extends {}>(url: string, options?: RequestInit): FetchState<T> => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const useFetch = <T extends Record<string, any>>(url: string, options?: RequestInit): FetchState<T> => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const [response, setResponse] = useState<APIResponse<T>>({ status: 'start fetching' } as any);
|
const [response, setResponse] = useState<APIResponse<T>>({ status: 'start fetching' } as any);
|
||||||
const [error, setError] = useState<Error>();
|
const [error, setError] = useState<Error>();
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
@ -54,6 +50,7 @@ let wasReady = false;
|
||||||
export const useFetchReadyInterval = (pathPrefix: string, options?: RequestInit): FetchStateReadyInterval => {
|
export const useFetchReadyInterval = (pathPrefix: string, options?: RequestInit): FetchStateReadyInterval => {
|
||||||
const [ready, setReady] = useState<boolean>(false);
|
const [ready, setReady] = useState<boolean>(false);
|
||||||
const [isUnexpected, setIsUnexpected] = useState<boolean>(false);
|
const [isUnexpected, setIsUnexpected] = useState<boolean>(false);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const [walReplayStatus, setWALReplayStatus] = useState<WALReplayStatus>({} as any);
|
const [walReplayStatus, setWALReplayStatus] = useState<WALReplayStatus>({} as any);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -21,7 +21,7 @@ describe('AlertsContent', () => {
|
||||||
{ selector: '#inactive-toggler', propName: 'inactive' },
|
{ selector: '#inactive-toggler', propName: 'inactive' },
|
||||||
{ selector: '#pending-toggler', propName: 'pending' },
|
{ selector: '#pending-toggler', propName: 'pending' },
|
||||||
{ selector: '#firing-toggler', propName: 'firing' },
|
{ selector: '#firing-toggler', propName: 'firing' },
|
||||||
].forEach(testCase => {
|
].forEach((testCase) => {
|
||||||
it(`toggles the ${testCase.propName} checkbox from true to false when clicked and back to true when clicked again`, () => {
|
it(`toggles the ${testCase.propName} checkbox from true to false when clicked and back to true when clicked again`, () => {
|
||||||
expect(wrapper.find(testCase.selector).prop('checked')).toBe(true);
|
expect(wrapper.find(testCase.selector).prop('checked')).toBe(true);
|
||||||
wrapper.find(testCase.selector).simulate('change', { target: { checked: false } });
|
wrapper.find(testCase.selector).simulate('change', { target: { checked: false } });
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { isPresent } from '../../utils';
|
||||||
import { Rule } from '../../types/types';
|
import { Rule } from '../../types/types';
|
||||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type RuleState = keyof RuleStatus<any>;
|
export type RuleState = keyof RuleStatus<any>;
|
||||||
|
|
||||||
export interface RuleStatus<T> {
|
export interface RuleStatus<T> {
|
||||||
|
@ -83,7 +84,7 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
{groups.map((group, i) => {
|
{groups.map((group, i) => {
|
||||||
const hasFilterOn = group.rules.some(rule => filter[rule.state]);
|
const hasFilterOn = group.rules.some((rule) => filter[rule.state]);
|
||||||
return hasFilterOn ? (
|
return hasFilterOn ? (
|
||||||
<Fragment key={i}>
|
<Fragment key={i}>
|
||||||
<GroupInfo rules={group.rules}>
|
<GroupInfo rules={group.rules}>
|
||||||
|
@ -108,6 +109,7 @@ interface GroupInfoProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupInfo: FC<GroupInfoProps> = ({ rules, children }) => {
|
export const GroupInfo: FC<GroupInfoProps> = ({ rules, children }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const statesCounter = rules.reduce<any>(
|
const statesCounter = rules.reduce<any>(
|
||||||
(acc, r) => {
|
(acc, r) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -18,7 +18,7 @@ const Alerts: FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (response.data && response.data.groups) {
|
if (response.data && response.data.groups) {
|
||||||
response.data.groups.forEach(el => el.rules.forEach(r => ruleStatsCount[r.state]++));
|
response.data.groups.forEach((el) => el.rules.forEach((r) => ruleStatsCount[r.state]++));
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AlertsWithStatusIndicator {...response.data} statsCount={ruleStatsCount} error={error} isLoading={isLoading} />;
|
return <AlertsWithStatusIndicator {...response.data} statsCount={ruleStatsCount} error={error} isLoading={isLoading} />;
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const ConfigContent: FC<ConfigContentProps> = ({ error, data }) => {
|
||||||
<h2>
|
<h2>
|
||||||
Configuration
|
Configuration
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={config!}
|
text={config ? config : ''}
|
||||||
onCopy={(_, result) => {
|
onCopy={(_, result) => {
|
||||||
setCopied(result);
|
setCopied(result);
|
||||||
setTimeout(setCopied, 1500);
|
setTimeout(setCopied, 1500);
|
||||||
|
|
|
@ -67,11 +67,7 @@ describe('Flags', () => {
|
||||||
|
|
||||||
it('is sorted by flag by default', (): void => {
|
it('is sorted by flag by default', (): void => {
|
||||||
const w = shallow(<FlagsContent data={sampleFlagsResponse} />);
|
const w = shallow(<FlagsContent data={sampleFlagsResponse} />);
|
||||||
const td = w
|
const td = w.find('tbody').find('td').find('span').first();
|
||||||
.find('tbody')
|
|
||||||
.find('td')
|
|
||||||
.find('span')
|
|
||||||
.first();
|
|
||||||
expect(td.html()).toBe('<span>--alertmanager.notification-queue-capacity</span>');
|
expect(td.html()).toBe('<span>--alertmanager.notification-queue-capacity</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -82,11 +78,7 @@ describe('Flags', () => {
|
||||||
.find('td')
|
.find('td')
|
||||||
.filterWhere((td): boolean => td.hasClass('Flag'));
|
.filterWhere((td): boolean => td.hasClass('Flag'));
|
||||||
th.simulate('click');
|
th.simulate('click');
|
||||||
const td = w
|
const td = w.find('tbody').find('td').find('span').first();
|
||||||
.find('tbody')
|
|
||||||
.find('td')
|
|
||||||
.find('span')
|
|
||||||
.first();
|
|
||||||
expect(td.html()).toBe('<span>--web.user-assets</span>');
|
expect(td.html()).toBe('<span>--web.user-assets</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -97,7 +89,7 @@ describe('Flags', () => {
|
||||||
const tds = w
|
const tds = w
|
||||||
.find('tbody')
|
.find('tbody')
|
||||||
.find('td')
|
.find('td')
|
||||||
.filterWhere(code => code.hasClass('flag-item'));
|
.filterWhere((code) => code.hasClass('flag-item'));
|
||||||
expect(tds.length).toEqual(3);
|
expect(tds.length).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,7 +100,7 @@ describe('Flags', () => {
|
||||||
const tds = w
|
const tds = w
|
||||||
.find('tbody')
|
.find('tbody')
|
||||||
.find('td')
|
.find('td')
|
||||||
.filterWhere(code => code.hasClass('flag-value'));
|
.filterWhere((code) => code.hasClass('flag-value'));
|
||||||
expect(tds.length).toEqual(1);
|
expect(tds.length).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,10 +21,9 @@ interface FlagsProps {
|
||||||
data?: FlagMap;
|
data?: FlagMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
const compareAlphaFn = (keys: boolean, reverse: boolean) => (
|
const compareAlphaFn =
|
||||||
[k1, v1]: [string, string],
|
(keys: boolean, reverse: boolean) =>
|
||||||
[k2, v2]: [string, string]
|
([k1, v1]: [string, string], [k2, v2]: [string, string]): number => {
|
||||||
): number => {
|
|
||||||
const a = keys ? k1 : v1;
|
const a = keys ? k1 : v1;
|
||||||
const b = keys ? k2 : v2;
|
const b = keys ? k2 : v2;
|
||||||
const reverser = reverse ? -1 : 1;
|
const reverser = reverse ? -1 : 1;
|
||||||
|
|
|
@ -33,14 +33,14 @@ describe('CMExpressionInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a search icon when it is not loading', () => {
|
it('renders a search icon when it is not loading', () => {
|
||||||
const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'prepend');
|
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'prepend');
|
||||||
const icon = addon.find(FontAwesomeIcon);
|
const icon = addon.find(FontAwesomeIcon);
|
||||||
expect(icon.prop('icon')).toEqual(faSearch);
|
expect(icon.prop('icon')).toEqual(faSearch);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a loading icon when it is loading', () => {
|
it('renders a loading icon when it is loading', () => {
|
||||||
const expressionInput = mount(<CMExpressionInput {...expressionInputProps} loading={true} />);
|
const expressionInput = mount(<CMExpressionInput {...expressionInputProps} loading={true} />);
|
||||||
const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'prepend');
|
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'prepend');
|
||||||
const icon = addon.find(FontAwesomeIcon);
|
const icon = addon.find(FontAwesomeIcon);
|
||||||
expect(icon.prop('icon')).toEqual(faSpinner);
|
expect(icon.prop('icon')).toEqual(faSpinner);
|
||||||
expect(icon.prop('spin')).toBe(true);
|
expect(icon.prop('spin')).toBe(true);
|
||||||
|
@ -52,11 +52,8 @@ describe('CMExpressionInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an execute button', () => {
|
it('renders an execute button', () => {
|
||||||
const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'append');
|
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'append');
|
||||||
const button = addon
|
const button = addon.find(Button).find('.execute-btn').first();
|
||||||
.find(Button)
|
|
||||||
.find('.execute-btn')
|
|
||||||
.first();
|
|
||||||
expect(button.prop('color')).toEqual('primary');
|
expect(button.prop('color')).toEqual('primary');
|
||||||
expect(button.text()).toEqual('Execute');
|
expect(button.text()).toEqual('Execute');
|
||||||
});
|
});
|
||||||
|
@ -65,7 +62,7 @@ describe('CMExpressionInput', () => {
|
||||||
const spyExecuteQuery = jest.fn();
|
const spyExecuteQuery = jest.fn();
|
||||||
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
||||||
const wrapper = mount(<CMExpressionInput {...props} />);
|
const wrapper = mount(<CMExpressionInput {...props} />);
|
||||||
const btn = wrapper.find(Button).filterWhere(btn => btn.hasClass('execute-btn'));
|
const btn = wrapper.find(Button).filterWhere((btn) => btn.hasClass('execute-btn'));
|
||||||
btn.simulate('click');
|
btn.simulate('click');
|
||||||
expect(spyExecuteQuery).toHaveBeenCalledTimes(1);
|
expect(spyExecuteQuery).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class HistoryCompleteStrategy implements CompleteStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
promQL(context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null {
|
promQL(context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null {
|
||||||
return Promise.resolve(this.complete.promQL(context)).then(res => {
|
return Promise.resolve(this.complete.promQL(context)).then((res) => {
|
||||||
const { state, pos } = context;
|
const { state, pos } = context;
|
||||||
const tree = syntaxTree(state).resolve(pos, -1);
|
const tree = syntaxTree(state).resolve(pos, -1);
|
||||||
const start = res != null ? res.from : tree.from;
|
const start = res != null ? res.from : tree.from;
|
||||||
|
@ -61,7 +61,7 @@ export class HistoryCompleteStrategy implements CompleteStrategy {
|
||||||
const historyItems: CompletionResult = {
|
const historyItems: CompletionResult = {
|
||||||
from: start,
|
from: start,
|
||||||
to: pos,
|
to: pos,
|
||||||
options: this.queryHistory.map(q => ({
|
options: this.queryHistory.map((q) => ({
|
||||||
label: q.length < 80 ? q : q.slice(0, 76).concat('...'),
|
label: q.length < 80 ? q : q.slice(0, 76).concat('...'),
|
||||||
detail: 'past query',
|
detail: 'past query',
|
||||||
apply: q,
|
apply: q,
|
||||||
|
|
|
@ -69,12 +69,7 @@ describe('DataTable', () => {
|
||||||
const table = dataTable.find(Table);
|
const table = dataTable.find(Table);
|
||||||
table.find('tr').forEach((row, idx) => {
|
table.find('tr').forEach((row, idx) => {
|
||||||
expect(row.find(SeriesName)).toHaveLength(1);
|
expect(row.find(SeriesName)).toHaveLength(1);
|
||||||
expect(
|
expect(row.find('td').at(1).text()).toEqual(`${idx}`);
|
||||||
row
|
|
||||||
.find('td')
|
|
||||||
.at(1)
|
|
||||||
.text()
|
|
||||||
).toEqual(`${idx}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -83,7 +78,7 @@ describe('DataTable', () => {
|
||||||
const dataTableProps: QueryResult = {
|
const dataTableProps: QueryResult = {
|
||||||
data: {
|
data: {
|
||||||
resultType: 'vector',
|
resultType: 'vector',
|
||||||
result: Array.from(Array(10001).keys()).map(i => {
|
result: Array.from(Array(10001).keys()).map((i) => {
|
||||||
return {
|
return {
|
||||||
metric: {
|
metric: {
|
||||||
__name__: `metric_name_${i}`,
|
__name__: `metric_name_${i}`,
|
||||||
|
@ -104,12 +99,7 @@ describe('DataTable', () => {
|
||||||
|
|
||||||
it('renders a warning', () => {
|
it('renders a warning', () => {
|
||||||
const alerts = dataTable.find(Alert);
|
const alerts = dataTable.find(Alert);
|
||||||
expect(
|
expect(alerts.first().render().text()).toEqual('Warning: Fetched 10001 metrics, only displaying first 10000.');
|
||||||
alerts
|
|
||||||
.first()
|
|
||||||
.render()
|
|
||||||
.text()
|
|
||||||
).toEqual('Warning: Fetched 10001 metrics, only displaying first 10000.');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -117,7 +107,7 @@ describe('DataTable', () => {
|
||||||
const dataTableProps: QueryResult = {
|
const dataTableProps: QueryResult = {
|
||||||
data: {
|
data: {
|
||||||
resultType: 'vector',
|
resultType: 'vector',
|
||||||
result: Array.from(Array(1001).keys()).map(i => {
|
result: Array.from(Array(1001).keys()).map((i) => {
|
||||||
return {
|
return {
|
||||||
metric: {
|
metric: {
|
||||||
__name__: `metric_name_${i}`,
|
__name__: `metric_name_${i}`,
|
||||||
|
@ -133,12 +123,9 @@ describe('DataTable', () => {
|
||||||
|
|
||||||
it('renders a warning', () => {
|
it('renders a warning', () => {
|
||||||
const alerts = dataTable.find(Alert);
|
const alerts = dataTable.find(Alert);
|
||||||
expect(
|
expect(alerts.first().render().text()).toEqual(
|
||||||
alerts
|
'Notice: Showing more than 1000 series, turning off label formatting for performance reasons.'
|
||||||
.first()
|
);
|
||||||
.render()
|
|
||||||
.text()
|
|
||||||
).toEqual('Notice: Showing more than 1000 series, turning off label formatting for performance reasons.');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -62,8 +62,7 @@ const DataTable: FC<QueryResult> = ({ data }) => {
|
||||||
const doFormat = data.result.length <= maxFormattableSize;
|
const doFormat = data.result.length <= maxFormattableSize;
|
||||||
switch (data.resultType) {
|
switch (data.resultType) {
|
||||||
case 'vector':
|
case 'vector':
|
||||||
rows = (limitSeries(data.result) as InstantSample[]).map(
|
rows = (limitSeries(data.result) as InstantSample[]).map((s: InstantSample, index: number): ReactNode => {
|
||||||
(s: InstantSample, index: number): ReactNode => {
|
|
||||||
return (
|
return (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>
|
<td>
|
||||||
|
@ -72,14 +71,13 @@ const DataTable: FC<QueryResult> = ({ data }) => {
|
||||||
<td>{s.value[1]}</td>
|
<td>{s.value[1]}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
limited = rows.length !== data.result.length;
|
limited = rows.length !== data.result.length;
|
||||||
break;
|
break;
|
||||||
case 'matrix':
|
case 'matrix':
|
||||||
rows = (limitSeries(data.result) as RangeSamples[]).map((s, index) => {
|
rows = (limitSeries(data.result) as RangeSamples[]).map((s, index) => {
|
||||||
const valueText = s.values
|
const valueText = s.values
|
||||||
.map(v => {
|
.map((v) => {
|
||||||
return v[1] + ' @' + v[0];
|
return v[1] + ' @' + v[0];
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
|
@ -47,14 +47,14 @@ describe('ExpressionInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a search icon when it is not loading', () => {
|
it('renders a search icon when it is not loading', () => {
|
||||||
const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'prepend');
|
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'prepend');
|
||||||
const icon = addon.find(FontAwesomeIcon);
|
const icon = addon.find(FontAwesomeIcon);
|
||||||
expect(icon.prop('icon')).toEqual(faSearch);
|
expect(icon.prop('icon')).toEqual(faSearch);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a loading icon when it is loading', () => {
|
it('renders a loading icon when it is loading', () => {
|
||||||
const expressionInput = mount(<ExpressionInput {...expressionInputProps} loading={true} />);
|
const expressionInput = mount(<ExpressionInput {...expressionInputProps} loading={true} />);
|
||||||
const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'prepend');
|
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'prepend');
|
||||||
const icon = addon.find(FontAwesomeIcon);
|
const icon = addon.find(FontAwesomeIcon);
|
||||||
expect(icon.prop('icon')).toEqual(faSpinner);
|
expect(icon.prop('icon')).toEqual(faSpinner);
|
||||||
expect(icon.prop('spin')).toBe(true);
|
expect(icon.prop('spin')).toBe(true);
|
||||||
|
@ -75,7 +75,7 @@ describe('ExpressionInput', () => {
|
||||||
const downshift = expressionInput.find(Downshift);
|
const downshift = expressionInput.find(Downshift);
|
||||||
const input = downshift.find(Input);
|
const input = downshift.find(Input);
|
||||||
downshift.setState({ isOpen: false });
|
downshift.setState({ isOpen: false });
|
||||||
['Home', 'End', 'ArrowUp', 'ArrowDown'].forEach(key => {
|
['Home', 'End', 'ArrowUp', 'ArrowDown'].forEach((key) => {
|
||||||
const event = getKeyEvent(key);
|
const event = getKeyEvent(key);
|
||||||
input.simulate('keydown', event);
|
input.simulate('keydown', event);
|
||||||
const nativeEvent = event.nativeEvent as any;
|
const nativeEvent = event.nativeEvent as any;
|
||||||
|
@ -122,7 +122,7 @@ describe('ExpressionInput', () => {
|
||||||
const spyExecuteQuery = jest.fn();
|
const spyExecuteQuery = jest.fn();
|
||||||
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
const props = { ...expressionInputProps, executeQuery: spyExecuteQuery };
|
||||||
const wrapper = mount(<ExpressionInput {...props} />);
|
const wrapper = mount(<ExpressionInput {...props} />);
|
||||||
const btn = wrapper.find(Button).filterWhere(btn => btn.hasClass('execute-btn'));
|
const btn = wrapper.find(Button).filterWhere((btn) => btn.hasClass('execute-btn'));
|
||||||
btn.simulate('click');
|
btn.simulate('click');
|
||||||
expect(spyExecuteQuery).toHaveBeenCalledTimes(1);
|
expect(spyExecuteQuery).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -226,7 +226,7 @@ describe('ExpressionInput', () => {
|
||||||
const downshift = expressionInput.find(Downshift);
|
const downshift = expressionInput.find(Downshift);
|
||||||
const input = downshift.find(Input);
|
const input = downshift.find(Input);
|
||||||
downshift.setState({ isOpen: true });
|
downshift.setState({ isOpen: true });
|
||||||
['ArrowUp', 'ArrowDown'].forEach(key => {
|
['ArrowUp', 'ArrowDown'].forEach((key) => {
|
||||||
const event = getKeyEvent(key);
|
const event = getKeyEvent(key);
|
||||||
input.simulate('keydown', event);
|
input.simulate('keydown', event);
|
||||||
const nativeEvent = event.nativeEvent as any;
|
const nativeEvent = event.nativeEvent as any;
|
||||||
|
@ -248,7 +248,7 @@ describe('ExpressionInput', () => {
|
||||||
const ul = downshift.find('ul');
|
const ul = downshift.find('ul');
|
||||||
expect(ul.prop('className')).toEqual('card list-group');
|
expect(ul.prop('className')).toEqual('card list-group');
|
||||||
const items = ul.find('li');
|
const items = ul.find('li');
|
||||||
expect(items.map(item => item.text()).join(', ')).toEqual(
|
expect(items.map((item) => item.text()).join(', ')).toEqual(
|
||||||
'node_cpu_guest_seconds_total, node_cpu_seconds_total, instance:node_cpu_utilisation:rate1m'
|
'node_cpu_guest_seconds_total, node_cpu_seconds_total, instance:node_cpu_utilisation:rate1m'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -256,11 +256,8 @@ describe('ExpressionInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an execute Button', () => {
|
it('renders an execute Button', () => {
|
||||||
const addon = expressionInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === 'append');
|
const addon = expressionInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === 'append');
|
||||||
const button = addon
|
const button = addon.find(Button).find('.execute-btn').first();
|
||||||
.find(Button)
|
|
||||||
.find('.execute-btn')
|
|
||||||
.first();
|
|
||||||
expect(button.prop('color')).toEqual('primary');
|
expect(button.prop('color')).toEqual('primary');
|
||||||
expect(button.text()).toEqual('Execute');
|
expect(button.text()).toEqual('Execute');
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Button, InputGroup, InputGroupAddon, InputGroupText, Input } from 'reactstrap';
|
import { Button, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||||
|
|
||||||
import Downshift, { ControllerStateAndHelpers } from 'downshift';
|
import Downshift, { ControllerStateAndHelpers } from 'downshift';
|
||||||
import sanitizeHTML from 'sanitize-html';
|
import sanitizeHTML from 'sanitize-html';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSearch, faSpinner, faGlobeEurope } from '@fortawesome/free-solid-svg-icons';
|
import { faGlobeEurope, faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||||
import MetricsExplorer from './MetricsExplorer';
|
import MetricsExplorer from './MetricsExplorer';
|
||||||
import { Fuzzy, FuzzyResult } from '@nexucis/fuzzy';
|
import { Fuzzy, FuzzyResult } from '@nexucis/fuzzy';
|
||||||
|
|
||||||
|
@ -37,34 +37,38 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount(): void {
|
||||||
this.setHeight();
|
this.setHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeight = () => {
|
setHeight = (): void => {
|
||||||
const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current!;
|
if (this.exprInputRef.current) {
|
||||||
|
const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current;
|
||||||
const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate.
|
const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate.
|
||||||
this.setState({ height: scrollHeight + offset });
|
this.setState({ height: scrollHeight + offset });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleInput = () => {
|
handleInput = (): void => {
|
||||||
this.setValue(this.exprInputRef.current!.value);
|
if (this.exprInputRef.current) {
|
||||||
|
this.setValue(this.exprInputRef.current.value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setValue = (value: string) => {
|
setValue = (value: string): void => {
|
||||||
const { onExpressionChange } = this.props;
|
const { onExpressionChange } = this.props;
|
||||||
onExpressionChange(value);
|
onExpressionChange(value);
|
||||||
this.setState({ height: 'auto' }, this.setHeight);
|
this.setState({ height: 'auto' }, this.setHeight);
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ExpressionInputProps) {
|
componentDidUpdate(prevProps: ExpressionInputProps): void {
|
||||||
const { value } = this.props;
|
const { value } = this.props;
|
||||||
if (value !== prevProps.value) {
|
if (value !== prevProps.value) {
|
||||||
this.setValue(value);
|
this.setValue(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||||
const { executeQuery } = this.props;
|
const { executeQuery } = this.props;
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
executeQuery();
|
executeQuery();
|
||||||
|
@ -72,11 +76,12 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getSearchMatches = (input: string, expressions: string[]) => {
|
getSearchMatches = (input: string, expressions: string[]): FuzzyResult[] => {
|
||||||
return fuz.filter(input.replace(/ /g, ''), expressions);
|
return fuz.filter(input.replace(/ /g, ''), expressions);
|
||||||
};
|
};
|
||||||
|
|
||||||
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>): JSX.Element | null => {
|
||||||
const { inputValue = '', closeMenu, highlightedIndex } = downshift;
|
const { inputValue = '', closeMenu, highlightedIndex } = downshift;
|
||||||
const autocompleteSections = {
|
const autocompleteSections = {
|
||||||
'Query History': this.props.queryHistory,
|
'Query History': this.props.queryHistory,
|
||||||
|
@ -84,9 +89,9 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
};
|
};
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const sections =
|
const sections =
|
||||||
inputValue!.length && this.props.enableAutocomplete
|
inputValue?.length && this.props.enableAutocomplete
|
||||||
? Object.entries(autocompleteSections).reduce((acc, [title, items]) => {
|
? Object.entries(autocompleteSections).reduce((acc, [title, items]) => {
|
||||||
const matches = this.getSearchMatches(inputValue!, items);
|
const matches = this.getSearchMatches(inputValue, items);
|
||||||
return !matches.length
|
return !matches.length
|
||||||
? acc
|
? acc
|
||||||
: [
|
: [
|
||||||
|
@ -94,7 +99,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
<ul className="autosuggest-dropdown-list" key={title}>
|
<ul className="autosuggest-dropdown-list" key={title}>
|
||||||
<li className="autosuggest-dropdown-header">{title}</li>
|
<li className="autosuggest-dropdown-header">{title}</li>
|
||||||
{matches
|
{matches
|
||||||
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
|
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is slow.
|
||||||
.map((result: FuzzyResult) => {
|
.map((result: FuzzyResult) => {
|
||||||
const itemProps = downshift.getItemProps({
|
const itemProps = downshift.getItemProps({
|
||||||
key: result.original,
|
key: result.original,
|
||||||
|
@ -131,19 +136,19 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
openMetricsExplorer = () => {
|
openMetricsExplorer = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showMetricsExplorer: true,
|
showMetricsExplorer: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
updateShowMetricsExplorer = (show: boolean) => {
|
updateShowMetricsExplorer = (show: boolean): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showMetricsExplorer: show,
|
showMetricsExplorer: show,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
insertAtCursor = (value: string) => {
|
insertAtCursor = (value: string): void => {
|
||||||
if (!this.exprInputRef.current) return;
|
if (!this.exprInputRef.current) return;
|
||||||
|
|
||||||
const startPosition = this.exprInputRef.current.selectionStart;
|
const startPosition = this.exprInputRef.current.selectionStart;
|
||||||
|
@ -161,13 +166,13 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
this.setValue(newValue);
|
this.setValue(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { executeQuery, value } = this.props;
|
const { executeQuery, value } = this.props;
|
||||||
const { height } = this.state;
|
const { height } = this.state;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Downshift onSelect={this.setValue}>
|
<Downshift onSelect={this.setValue}>
|
||||||
{downshift => (
|
{(downshift) => (
|
||||||
<div>
|
<div>
|
||||||
<InputGroup className="expression-input">
|
<InputGroup className="expression-input">
|
||||||
<InputGroupAddon addonType="prepend">
|
<InputGroupAddon addonType="prepend">
|
||||||
|
@ -191,11 +196,13 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
case 'End':
|
case 'End':
|
||||||
// We want to be able to jump to the beginning/end of the input field.
|
// We want to be able to jump to the beginning/end of the input field.
|
||||||
// By default, Downshift otherwise jumps to the first/last suggestion item instead.
|
// By default, Downshift otherwise jumps to the first/last suggestion item instead.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(event.nativeEvent as any).preventDownshiftDefault = true;
|
(event.nativeEvent as any).preventDownshiftDefault = true;
|
||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
if (!downshift.isOpen) {
|
if (!downshift.isOpen) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(event.nativeEvent as any).preventDownshiftDefault = true;
|
(event.nativeEvent as any).preventDownshiftDefault = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -203,13 +210,14 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
||||||
downshift.closeMenu();
|
downshift.closeMenu();
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
if (!downshift.isOpen) {
|
if (!downshift.isOpen && this.exprInputRef.current) {
|
||||||
this.exprInputRef.current!.blur();
|
this.exprInputRef.current.blur();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} as any)}
|
} as any)}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -78,9 +78,9 @@ describe('Graph', () => {
|
||||||
};
|
};
|
||||||
it('renders a graph with props', () => {
|
it('renders a graph with props', () => {
|
||||||
const graph = shallow(<Graph {...props} />);
|
const graph = shallow(<Graph {...props} />);
|
||||||
const div = graph.find('div').filterWhere(elem => elem.prop('className') === 'graph-test');
|
const div = graph.find('div').filterWhere((elem) => elem.prop('className') === 'graph-test');
|
||||||
const resize = div.find(ReactResizeDetector);
|
const resize = div.find(ReactResizeDetector);
|
||||||
const innerdiv = div.find('div').filterWhere(elem => elem.prop('className') === 'graph-chart');
|
const innerdiv = div.find('div').filterWhere((elem) => elem.prop('className') === 'graph-chart');
|
||||||
expect(resize.prop('handleWidth')).toBe(true);
|
expect(resize.prop('handleWidth')).toBe(true);
|
||||||
expect(div).toHaveLength(1);
|
expect(div).toHaveLength(1);
|
||||||
expect(innerdiv).toHaveLength(1);
|
expect(innerdiv).toHaveLength(1);
|
||||||
|
@ -264,10 +264,7 @@ describe('Graph', () => {
|
||||||
);
|
);
|
||||||
(graph.instance() as any).plot(); // create chart
|
(graph.instance() as any).plot(); // create chart
|
||||||
const spyPlotSetAndDraw = jest.spyOn(graph.instance() as any, 'plotSetAndDraw');
|
const spyPlotSetAndDraw = jest.spyOn(graph.instance() as any, 'plotSetAndDraw');
|
||||||
graph
|
graph.find('.legend-item').at(0).simulate('mouseover');
|
||||||
.find('.legend-item')
|
|
||||||
.at(0)
|
|
||||||
.simulate('mouseover');
|
|
||||||
expect(spyPlotSetAndDraw).toHaveBeenCalledTimes(1);
|
expect(spyPlotSetAndDraw).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
it('should call spyPlotSetAndDraw with chartDate from state as default value', () => {
|
it('should call spyPlotSetAndDraw with chartDate from state as default value', () => {
|
||||||
|
|
|
@ -42,6 +42,7 @@ export interface GraphExemplar {
|
||||||
seriesLabels: { [key: string]: string };
|
seriesLabels: { [key: string]: string };
|
||||||
labels: { [key: string]: string };
|
labels: { [key: string]: string };
|
||||||
data: number[][];
|
data: number[][];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
points: any; // This is used to specify the symbol.
|
points: any; // This is used to specify the symbol.
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
@ -67,7 +68,7 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
selectedExemplarLabels: { exemplar: {}, series: {} },
|
selectedExemplarLabels: { exemplar: {}, series: {} },
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: GraphProps) {
|
componentDidUpdate(prevProps: GraphProps): void {
|
||||||
const { data, stacked, useLocalTime, showExemplars } = this.props;
|
const { data, stacked, useLocalTime, showExemplars } = this.props;
|
||||||
if (prevProps.data !== data) {
|
if (prevProps.data !== data) {
|
||||||
this.selectedSeriesIndexes = [];
|
this.selectedSeriesIndexes = [];
|
||||||
|
@ -102,7 +103,7 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount(): void {
|
||||||
this.plot();
|
this.plot();
|
||||||
|
|
||||||
$(`.graph-${this.props.id}`).bind('plotclick', (event, pos, item) => {
|
$(`.graph-${this.props.id}`).bind('plotclick', (event, pos, item) => {
|
||||||
|
@ -130,11 +131,13 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount(): void {
|
||||||
this.destroyPlot();
|
this.destroyPlot();
|
||||||
}
|
}
|
||||||
|
|
||||||
plot = (data: (GraphSeries | GraphExemplar)[] = [...this.state.chartData.series, ...this.state.chartData.exemplars]) => {
|
plot = (
|
||||||
|
data: (GraphSeries | GraphExemplar)[] = [...this.state.chartData.series, ...this.state.chartData.exemplars]
|
||||||
|
): void => {
|
||||||
if (!this.chartRef.current) {
|
if (!this.chartRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -143,7 +146,7 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
this.$chart = $.plot($(this.chartRef.current), data, getOptions(this.props.stacked, this.props.useLocalTime));
|
this.$chart = $.plot($(this.chartRef.current), data, getOptions(this.props.stacked, this.props.useLocalTime));
|
||||||
};
|
};
|
||||||
|
|
||||||
destroyPlot = () => {
|
destroyPlot = (): void => {
|
||||||
if (isPresent(this.$chart)) {
|
if (isPresent(this.$chart)) {
|
||||||
this.$chart.destroy();
|
this.$chart.destroy();
|
||||||
}
|
}
|
||||||
|
@ -151,21 +154,21 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
|
|
||||||
plotSetAndDraw(
|
plotSetAndDraw(
|
||||||
data: (GraphSeries | GraphExemplar)[] = [...this.state.chartData.series, ...this.state.chartData.exemplars]
|
data: (GraphSeries | GraphExemplar)[] = [...this.state.chartData.series, ...this.state.chartData.exemplars]
|
||||||
) {
|
): void {
|
||||||
if (isPresent(this.$chart)) {
|
if (isPresent(this.$chart)) {
|
||||||
this.$chart.setData(data);
|
this.$chart.setData(data);
|
||||||
this.$chart.draw();
|
this.$chart.draw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSeriesSelect = (selected: number[], selectedIndex: number) => {
|
handleSeriesSelect = (selected: number[], selectedIndex: number): void => {
|
||||||
const { chartData } = this.state;
|
const { chartData } = this.state;
|
||||||
this.plot(
|
this.plot(
|
||||||
this.selectedSeriesIndexes.length === 1 && this.selectedSeriesIndexes.includes(selectedIndex)
|
this.selectedSeriesIndexes.length === 1 && this.selectedSeriesIndexes.includes(selectedIndex)
|
||||||
? [...chartData.series.map(toHoverColor(selectedIndex, this.props.stacked)), ...chartData.exemplars]
|
? [...chartData.series.map(toHoverColor(selectedIndex, this.props.stacked)), ...chartData.exemplars]
|
||||||
: [
|
: [
|
||||||
...chartData.series.filter((_, i) => selected.includes(i)),
|
...chartData.series.filter((_, i) => selected.includes(i)),
|
||||||
...chartData.exemplars.filter(exemplar => {
|
...chartData.exemplars.filter((exemplar) => {
|
||||||
series: for (const i in selected) {
|
series: for (const i in selected) {
|
||||||
for (const name in chartData.series[selected[i]].labels) {
|
for (const name in chartData.series[selected[i]].labels) {
|
||||||
if (exemplar.seriesLabels[name] !== chartData.series[selected[i]].labels[name]) {
|
if (exemplar.seriesLabels[name] !== chartData.series[selected[i]].labels[name]) {
|
||||||
|
@ -181,7 +184,7 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
this.selectedSeriesIndexes = selected;
|
this.selectedSeriesIndexes = selected;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSeriesHover = (index: number) => () => {
|
handleSeriesHover = (index: number) => (): void => {
|
||||||
if (this.rafID) {
|
if (this.rafID) {
|
||||||
cancelAnimationFrame(this.rafID);
|
cancelAnimationFrame(this.rafID);
|
||||||
}
|
}
|
||||||
|
@ -193,18 +196,18 @@ class Graph extends PureComponent<GraphProps, GraphState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleLegendMouseOut = () => {
|
handleLegendMouseOut = (): void => {
|
||||||
cancelAnimationFrame(this.rafID);
|
cancelAnimationFrame(this.rafID);
|
||||||
this.plotSetAndDraw();
|
this.plotSetAndDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleResize = () => {
|
handleResize = (): void => {
|
||||||
if (isPresent(this.$chart)) {
|
if (isPresent(this.$chart)) {
|
||||||
this.plot(this.$chart.getData() as (GraphSeries | GraphExemplar)[]);
|
this.plot(this.$chart.getData() as (GraphSeries | GraphExemplar)[]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { chartData, selectedExemplarLabels } = this.state;
|
const { chartData, selectedExemplarLabels } = this.state;
|
||||||
const selectedLabels = selectedExemplarLabels as {
|
const selectedLabels = selectedExemplarLabels as {
|
||||||
exemplar: { [key: string]: string };
|
exemplar: { [key: string]: string };
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
|
||||||
import GraphControls from './GraphControls';
|
import GraphControls from './GraphControls';
|
||||||
import { Button, ButtonGroup, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap';
|
import { Button, ButtonGroup, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSquare, faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
|
||||||
import TimeInput from './TimeInput';
|
import TimeInput from './TimeInput';
|
||||||
|
|
||||||
const defaultGraphControlProps = {
|
const defaultGraphControlProps = {
|
||||||
|
@ -59,9 +59,9 @@ describe('GraphControls', () => {
|
||||||
title: 'Increase range',
|
title: 'Increase range',
|
||||||
icon: faPlus,
|
icon: faPlus,
|
||||||
},
|
},
|
||||||
].forEach(testCase => {
|
].forEach((testCase) => {
|
||||||
const controls = shallow(<GraphControls {...defaultGraphControlProps} />);
|
const controls = shallow(<GraphControls {...defaultGraphControlProps} />);
|
||||||
const addon = controls.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === testCase.position);
|
const addon = controls.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === testCase.position);
|
||||||
const button = addon.find(Button);
|
const button = addon.find(Button);
|
||||||
const icon = button.find(FontAwesomeIcon);
|
const icon = button.find(FontAwesomeIcon);
|
||||||
expect(button.prop('title')).toEqual(testCase.title);
|
expect(button.prop('title')).toEqual(testCase.title);
|
||||||
|
@ -109,7 +109,7 @@ describe('GraphControls', () => {
|
||||||
|
|
||||||
it('renders a resolution Input with props', () => {
|
it('renders a resolution Input with props', () => {
|
||||||
const controls = shallow(<GraphControls {...defaultGraphControlProps} />);
|
const controls = shallow(<GraphControls {...defaultGraphControlProps} />);
|
||||||
const input = controls.find(Input).filterWhere(input => input.prop('className') === 'resolution-input');
|
const input = controls.find(Input).filterWhere((input) => input.prop('className') === 'resolution-input');
|
||||||
expect(input.prop('placeholder')).toEqual('Res. (s)');
|
expect(input.prop('placeholder')).toEqual('Res. (s)');
|
||||||
expect(input.prop('defaultValue')).toEqual('10');
|
expect(input.prop('defaultValue')).toEqual('10');
|
||||||
expect(input.prop('innerRef')).toEqual({ current: null });
|
expect(input.prop('innerRef')).toEqual({ current: null });
|
||||||
|
@ -140,10 +140,10 @@ describe('GraphControls', () => {
|
||||||
icon: faChartArea,
|
icon: faChartArea,
|
||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
].forEach(testCase => {
|
].forEach((testCase) => {
|
||||||
const controls = shallow(<GraphControls {...defaultGraphControlProps} />);
|
const controls = shallow(<GraphControls {...defaultGraphControlProps} />);
|
||||||
const group = controls.find(ButtonGroup);
|
const group = controls.find(ButtonGroup);
|
||||||
const btn = group.find(Button).filterWhere(btn => btn.prop('title') === testCase.title);
|
const btn = group.find(Button).filterWhere((btn) => btn.prop('title') === testCase.title);
|
||||||
expect(btn.prop('active')).toEqual(testCase.active);
|
expect(btn.prop('active')).toEqual(testCase.active);
|
||||||
const icon = btn.find(FontAwesomeIcon);
|
const icon = btn.find(FontAwesomeIcon);
|
||||||
expect(icon.prop('icon')).toEqual(testCase.icon);
|
expect(icon.prop('icon')).toEqual(testCase.icon);
|
||||||
|
@ -160,14 +160,14 @@ describe('GraphControls', () => {
|
||||||
title: 'Show stacked graph',
|
title: 'Show stacked graph',
|
||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
].forEach(testCase => {
|
].forEach((testCase) => {
|
||||||
const results: boolean[] = [];
|
const results: boolean[] = [];
|
||||||
const onChange = (stacked: boolean): void => {
|
const onChange = (stacked: boolean): void => {
|
||||||
results.push(stacked);
|
results.push(stacked);
|
||||||
};
|
};
|
||||||
const controls = shallow(<GraphControls {...defaultGraphControlProps} onChangeStacking={onChange} />);
|
const controls = shallow(<GraphControls {...defaultGraphControlProps} onChangeStacking={onChange} />);
|
||||||
const group = controls.find(ButtonGroup);
|
const group = controls.find(ButtonGroup);
|
||||||
const btn = group.find(Button).filterWhere(btn => btn.prop('title') === testCase.title);
|
const btn = group.find(Button).filterWhere((btn) => btn.prop('title') === testCase.title);
|
||||||
const onClick = btn.prop('onClick');
|
const onClick = btn.prop('onClick');
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick({} as React.MouseEvent);
|
onClick({} as React.MouseEvent);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Button, ButtonGroup, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap';
|
import { Button, ButtonGroup, Form, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faPlus, faMinus, faChartArea, faChartLine } from '@fortawesome/free-solid-svg-icons';
|
import { faChartArea, faChartLine, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
import TimeInput from './TimeInput';
|
import TimeInput from './TimeInput';
|
||||||
import { parseDuration, formatDuration } from '../../utils';
|
import { formatDuration, parseDuration } from '../../utils';
|
||||||
|
|
||||||
interface GraphControlsProps {
|
interface GraphControlsProps {
|
||||||
range: number;
|
range: number;
|
||||||
|
@ -46,7 +46,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||||
182 * 24 * 60 * 60,
|
182 * 24 * 60 * 60,
|
||||||
365 * 24 * 60 * 60,
|
365 * 24 * 60 * 60,
|
||||||
730 * 24 * 60 * 60,
|
730 * 24 * 60 * 60,
|
||||||
].map(s => s * 1000);
|
].map((s) => s * 1000);
|
||||||
|
|
||||||
onChangeRangeInput = (rangeText: string): void => {
|
onChangeRangeInput = (rangeText: string): void => {
|
||||||
const range = parseDuration(rangeText);
|
const range = parseDuration(rangeText);
|
||||||
|
@ -58,7 +58,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
changeRangeInput = (range: number): void => {
|
changeRangeInput = (range: number): void => {
|
||||||
this.rangeRef.current!.value = formatDuration(range);
|
this.setCurrentRangeValue(formatDuration(range));
|
||||||
};
|
};
|
||||||
|
|
||||||
increaseRange = (): void => {
|
increaseRange = (): void => {
|
||||||
|
@ -81,18 +81,24 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: GraphControlsProps) {
|
componentDidUpdate(prevProps: GraphControlsProps): void {
|
||||||
if (prevProps.range !== this.props.range) {
|
if (prevProps.range !== this.props.range) {
|
||||||
this.changeRangeInput(this.props.range);
|
this.changeRangeInput(this.props.range);
|
||||||
}
|
}
|
||||||
if (prevProps.resolution !== this.props.resolution) {
|
if (prevProps.resolution !== this.props.resolution) {
|
||||||
this.resolutionRef.current!.value = this.props.resolution !== null ? this.props.resolution.toString() : '';
|
this.setCurrentRangeValue(this.props.resolution !== null ? this.props.resolution.toString() : '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
setCurrentRangeValue(value: string): void {
|
||||||
|
if (this.rangeRef.current) {
|
||||||
|
this.rangeRef.current.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Form inline className="graph-controls" onSubmit={e => e.preventDefault()}>
|
<Form inline className="graph-controls" onSubmit={(e) => e.preventDefault()}>
|
||||||
<InputGroup className="range-input" size="sm">
|
<InputGroup className="range-input" size="sm">
|
||||||
<InputGroupAddon addonType="prepend">
|
<InputGroupAddon addonType="prepend">
|
||||||
<Button title="Decrease range" onClick={this.decreaseRange}>
|
<Button title="Decrease range" onClick={this.decreaseRange}>
|
||||||
|
@ -103,9 +109,13 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||||
<Input
|
<Input
|
||||||
defaultValue={formatDuration(this.props.range)}
|
defaultValue={formatDuration(this.props.range)}
|
||||||
innerRef={this.rangeRef}
|
innerRef={this.rangeRef}
|
||||||
onBlur={() => this.onChangeRangeInput(this.rangeRef.current!.value)}
|
onBlur={() => {
|
||||||
|
if (this.rangeRef.current) {
|
||||||
|
this.onChangeRangeInput(this.rangeRef.current.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) =>
|
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||||
e.key === 'Enter' && this.onChangeRangeInput(this.rangeRef.current!.value)
|
e.key === 'Enter' && this.rangeRef.current && this.onChangeRangeInput(this.rangeRef.current.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -130,8 +140,10 @@ class GraphControls extends Component<GraphControlsProps> {
|
||||||
defaultValue={this.props.resolution !== null ? this.props.resolution.toString() : ''}
|
defaultValue={this.props.resolution !== null ? this.props.resolution.toString() : ''}
|
||||||
innerRef={this.resolutionRef}
|
innerRef={this.resolutionRef}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
const res = parseInt(this.resolutionRef.current!.value);
|
if (this.resolutionRef.current) {
|
||||||
|
const res = parseInt(this.resolutionRef.current.value);
|
||||||
this.props.onChangeResolution(res ? res : null);
|
this.props.onChangeResolution(res ? res : null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
bsSize="sm"
|
bsSize="sm"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -60,7 +60,7 @@ describe('GraphHelpers', () => {
|
||||||
{ input: 2e-24, output: '2.00y' },
|
{ input: 2e-24, output: '2.00y' },
|
||||||
{ input: 2e-25, output: '0.20y' },
|
{ input: 2e-25, output: '0.20y' },
|
||||||
{ input: 2e-26, output: '0.02y' },
|
{ input: 2e-26, output: '0.02y' },
|
||||||
].map(t => {
|
].map((t) => {
|
||||||
expect(formatValue(t.input)).toBe(t.output);
|
expect(formatValue(t.input)).toBe(t.output);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -80,7 +80,7 @@ describe('GraphHelpers', () => {
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
getColors(data)
|
getColors(data)
|
||||||
.map(c => c.toString())
|
.map((c) => c.toString())
|
||||||
.join(',')
|
.join(',')
|
||||||
).toEqual(
|
).toEqual(
|
||||||
'rgb(237,194,64),rgb(175,216,248),rgb(203,75,75),rgb(77,167,77),rgb(148,64,237),rgb(189,155,51),rgb(140,172,198)'
|
'rgb(237,194,64),rgb(175,216,248),rgb(203,75,75),rgb(77,167,77),rgb(148,64,237),rgb(189,155,51),rgb(140,172,198)'
|
||||||
|
|
|
@ -53,7 +53,7 @@ export const formatValue = (y: number | null): string => {
|
||||||
throw Error("couldn't format a value, this is a bug");
|
throw Error("couldn't format a value, this is a bug");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHoverColor = (color: string, opacity: number, stacked: boolean) => {
|
export const getHoverColor = (color: string, opacity: number, stacked: boolean): string => {
|
||||||
const { r, g, b } = $.color.parse(color);
|
const { r, g, b } = $.color.parse(color);
|
||||||
if (!stacked) {
|
if (!stacked) {
|
||||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||||
|
@ -67,7 +67,12 @@ export const getHoverColor = (color: string, opacity: number, stacked: boolean)
|
||||||
return `rgb(${Math.round(base + opacity * r)},${Math.round(base + opacity * g)},${Math.round(base + opacity * b)})`;
|
return `rgb(${Math.round(base + opacity * r)},${Math.round(base + opacity * g)},${Math.round(base + opacity * b)})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toHoverColor = (index: number, stacked: boolean) => (series: GraphSeries, i: number) => ({
|
export const toHoverColor =
|
||||||
|
(index: number, stacked: boolean) =>
|
||||||
|
(
|
||||||
|
series: GraphSeries,
|
||||||
|
i: number
|
||||||
|
): { color: string; data: (number | null)[][]; index: number; labels: { [p: string]: string } } => ({
|
||||||
...series,
|
...series,
|
||||||
color: getHoverColor(series.color, i !== index ? 0.3 : 1, stacked),
|
color: getHoverColor(series.color, i !== index ? 0.3 : 1, stacked),
|
||||||
});
|
});
|
||||||
|
@ -113,8 +118,8 @@ export const getOptions = (stacked: boolean, useLocalTime: boolean): jquery.flot
|
||||||
${Object.keys(labels).length === 0 ? '<div class="mb-1 font-italic">no labels</div>' : ''}
|
${Object.keys(labels).length === 0 ? '<div class="mb-1 font-italic">no labels</div>' : ''}
|
||||||
${labels['__name__'] ? `<div class="mb-1"><strong>${labels['__name__']}</strong></div>` : ''}
|
${labels['__name__'] ? `<div class="mb-1"><strong>${labels['__name__']}</strong></div>` : ''}
|
||||||
${Object.keys(labels)
|
${Object.keys(labels)
|
||||||
.filter(k => k !== '__name__')
|
.filter((k) => k !== '__name__')
|
||||||
.map(k => `<div class="mb-1"><strong>${k}</strong>: ${escapeHTML(labels[k])}</div>`)
|
.map((k) => `<div class="mb-1"><strong>${k}</strong>: ${escapeHTML(labels[k])}</div>`)
|
||||||
.join('')}
|
.join('')}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
@ -154,7 +159,10 @@ export const getOptions = (stacked: boolean, useLocalTime: boolean): jquery.flot
|
||||||
};
|
};
|
||||||
|
|
||||||
// This was adapted from Flot's color generation code.
|
// This was adapted from Flot's color generation code.
|
||||||
export const getColors = (data: { resultType: string; result: Array<{ metric: Metric; values: [number, string][] }> }) => {
|
export const getColors = (data: {
|
||||||
|
resultType: string;
|
||||||
|
result: Array<{ metric: Metric; values: [number, string][] }>;
|
||||||
|
}): Color[] => {
|
||||||
const colorPool = ['#edc240', '#afd8f8', '#cb4b4b', '#4da74d', '#9440ed'];
|
const colorPool = ['#edc240', '#afd8f8', '#cb4b4b', '#4da74d', '#9440ed'];
|
||||||
const colorPoolSize = colorPool.length;
|
const colorPoolSize = colorPool.length;
|
||||||
let variation = 0;
|
let variation = 0;
|
||||||
|
@ -180,6 +188,7 @@ export const getColors = (data: { resultType: string; result: Array<{ metric: Me
|
||||||
|
|
||||||
export const normalizeData = ({ queryParams, data, exemplars, stacked }: GraphProps): GraphData => {
|
export const normalizeData = ({ queryParams, data, exemplars, stacked }: GraphProps): GraphData => {
|
||||||
const colors = getColors(data);
|
const colors = getColors(data);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const { startTime, endTime, resolution } = queryParams!;
|
const { startTime, endTime, resolution } = queryParams!;
|
||||||
|
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
|
@ -233,7 +242,7 @@ export const normalizeData = ({ queryParams, data, exemplars, stacked }: GraphPr
|
||||||
index,
|
index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
exemplars: Object.values(buckets).flatMap(bucket => {
|
exemplars: Object.values(buckets).flatMap((bucket) => {
|
||||||
if (bucket.length === 1) {
|
if (bucket.length === 1) {
|
||||||
return bucket[0];
|
return bucket[0];
|
||||||
}
|
}
|
||||||
|
@ -256,7 +265,7 @@ export const normalizeData = ({ queryParams, data, exemplars, stacked }: GraphPr
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseValue = (value: string) => {
|
export const parseValue = (value: string): null | number => {
|
||||||
const val = parseFloat(value);
|
const val = parseFloat(value);
|
||||||
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
|
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
|
||||||
// can't be graphed, so show them as gaps (null).
|
// can't be graphed, so show them as gaps (null).
|
||||||
|
@ -290,7 +299,7 @@ const exemplarSymbol = (ctx: CanvasRenderingContext2D, x: number, y: number) =>
|
||||||
const stdDeviation = (sum: number, values: number[]): number => {
|
const stdDeviation = (sum: number, values: number[]): number => {
|
||||||
const avg = sum / values.length;
|
const avg = sum / values.length;
|
||||||
let squaredAvg = 0;
|
let squaredAvg = 0;
|
||||||
values.map(value => (squaredAvg += (value - avg) ** 2));
|
values.map((value) => (squaredAvg += (value - avg) ** 2));
|
||||||
squaredAvg = squaredAvg / values.length;
|
squaredAvg = squaredAvg / values.length;
|
||||||
return Math.sqrt(squaredAvg);
|
return Math.sqrt(squaredAvg);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { QueryParams, ExemplarData } from '../../types/types';
|
||||||
import { isPresent } from '../../utils';
|
import { isPresent } from '../../utils';
|
||||||
|
|
||||||
interface GraphTabContentProps {
|
interface GraphTabContentProps {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data: any;
|
data: any;
|
||||||
exemplars: ExemplarData;
|
exemplars: ExemplarData;
|
||||||
stacked: boolean;
|
stacked: boolean;
|
||||||
|
|
|
@ -18,12 +18,14 @@ export class Legend extends PureComponent<LegendProps, LegendState> {
|
||||||
state = {
|
state = {
|
||||||
selectedIndexes: [] as number[],
|
selectedIndexes: [] as number[],
|
||||||
};
|
};
|
||||||
componentDidUpdate(prevProps: LegendProps) {
|
componentDidUpdate(prevProps: LegendProps): void {
|
||||||
if (this.props.shouldReset && prevProps.shouldReset !== this.props.shouldReset) {
|
if (this.props.shouldReset && prevProps.shouldReset !== this.props.shouldReset) {
|
||||||
this.setState({ selectedIndexes: [] });
|
this.setState({ selectedIndexes: [] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleSeriesSelect = (index: number) => (ev: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
handleSeriesSelect =
|
||||||
|
(index: number) =>
|
||||||
|
(ev: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
|
||||||
// TODO: add proper event type
|
// TODO: add proper event type
|
||||||
const { selectedIndexes } = this.state;
|
const { selectedIndexes } = this.state;
|
||||||
|
|
||||||
|
@ -31,7 +33,7 @@ export class Legend extends PureComponent<LegendProps, LegendState> {
|
||||||
if (ev.ctrlKey || ev.metaKey) {
|
if (ev.ctrlKey || ev.metaKey) {
|
||||||
const { chartData } = this.props;
|
const { chartData } = this.props;
|
||||||
if (selectedIndexes.includes(index)) {
|
if (selectedIndexes.includes(index)) {
|
||||||
selected = selectedIndexes.filter(idx => idx !== index);
|
selected = selectedIndexes.filter((idx) => idx !== index);
|
||||||
} else {
|
} else {
|
||||||
selected =
|
selected =
|
||||||
// Flip the logic - In case none is selected ctrl + click should deselect clicked series.
|
// Flip the logic - In case none is selected ctrl + click should deselect clicked series.
|
||||||
|
@ -47,7 +49,7 @@ export class Legend extends PureComponent<LegendProps, LegendState> {
|
||||||
this.props.onSeriesToggle(selected, index);
|
this.props.onSeriesToggle(selected, index);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { chartData, onLegendMouseOut, onHover } = this.props;
|
const { chartData, onLegendMouseOut, onHover } = this.props;
|
||||||
const { selectedIndexes } = this.state;
|
const { selectedIndexes } = this.state;
|
||||||
const canUseHover = chartData.length > 1 && selectedIndexes.length === 0;
|
const canUseHover = chartData.length > 1 && selectedIndexes.length === 0;
|
||||||
|
|
|
@ -8,22 +8,22 @@ interface Props {
|
||||||
insertAtCursor(value: string): void;
|
insertAtCursor(value: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetricsExplorer extends Component<Props, {}> {
|
class MetricsExplorer extends Component<Props> {
|
||||||
handleMetricClick = (query: string) => {
|
handleMetricClick = (query: string): void => {
|
||||||
this.props.insertAtCursor(query);
|
this.props.insertAtCursor(query);
|
||||||
this.props.updateShow(false);
|
this.props.updateShow(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggle = () => {
|
toggle = (): void => {
|
||||||
this.props.updateShow(!this.props.show);
|
this.props.updateShow(!this.props.show);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={this.props.show} toggle={this.toggle} className="metrics-explorer">
|
<Modal isOpen={this.props.show} toggle={this.toggle} className="metrics-explorer">
|
||||||
<ModalHeader toggle={this.toggle}>Metrics Explorer</ModalHeader>
|
<ModalHeader toggle={this.toggle}>Metrics Explorer</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{this.props.metrics.map(metric => (
|
{this.props.metrics.map((metric) => (
|
||||||
<p key={metric} className="metric" onClick={this.handleMetricClick.bind(this, metric)}>
|
<p key={metric} className="metric" onClick={this.handleMetricClick.bind(this, metric)}>
|
||||||
{metric}
|
{metric}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -75,7 +75,7 @@ describe('Panel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a TabPane with a TimeInput and a DataTable when in table mode', () => {
|
it('renders a TabPane with a TimeInput and a DataTable when in table mode', () => {
|
||||||
const tab = panel.find(TabPane).filterWhere(tab => tab.prop('tabId') === 'table');
|
const tab = panel.find(TabPane).filterWhere((tab) => tab.prop('tabId') === 'table');
|
||||||
const timeInput = tab.find(TimeInput);
|
const timeInput = tab.find(TimeInput);
|
||||||
expect(timeInput.prop('time')).toEqual(defaultProps.options.endTime);
|
expect(timeInput.prop('time')).toEqual(defaultProps.options.endTime);
|
||||||
expect(timeInput.prop('range')).toEqual(defaultProps.options.range);
|
expect(timeInput.prop('range')).toEqual(defaultProps.options.range);
|
||||||
|
|
|
@ -31,6 +31,7 @@ interface PanelProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PanelState {
|
interface PanelState {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data: any; // TODO: Type data.
|
data: any; // TODO: Type data.
|
||||||
exemplars: ExemplarData;
|
exemplars: ExemplarData;
|
||||||
lastQueryParams: QueryParams | null;
|
lastQueryParams: QueryParams | null;
|
||||||
|
@ -84,7 +85,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate({ options: prevOpts }: PanelProps) {
|
componentDidUpdate({ options: prevOpts }: PanelProps): void {
|
||||||
const { endTime, range, resolution, showExemplars, type } = this.props.options;
|
const { endTime, range, resolution, showExemplars, type } = this.props.options;
|
||||||
if (
|
if (
|
||||||
prevOpts.endTime !== endTime ||
|
prevOpts.endTime !== endTime ||
|
||||||
|
@ -97,10 +98,11 @@ class Panel extends Component<PanelProps, PanelState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount(): void {
|
||||||
this.executeQuery();
|
this.executeQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
executeQuery = async (): Promise<any> => {
|
executeQuery = async (): Promise<any> => {
|
||||||
const { exprInputValue: expr } = this.state;
|
const { exprInputValue: expr } = this.state;
|
||||||
const queryStart = Date.now();
|
const queryStart = Date.now();
|
||||||
|
@ -151,7 +153,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
}).then(resp => resp.json());
|
}).then((resp) => resp.json());
|
||||||
|
|
||||||
if (query.status !== 'success') {
|
if (query.status !== 'success') {
|
||||||
throw new Error(query.error || 'invalid response JSON');
|
throw new Error(query.error || 'invalid response JSON');
|
||||||
|
@ -163,7 +165,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
}).then(resp => resp.json());
|
}).then((resp) => resp.json());
|
||||||
|
|
||||||
if (exemplars.status !== 'success') {
|
if (exemplars.status !== 'success') {
|
||||||
throw new Error(exemplars.error || 'invalid response JSON');
|
throw new Error(exemplars.error || 'invalid response JSON');
|
||||||
|
@ -210,7 +212,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setOptions(opts: object): void {
|
setOptions(opts: Record<string, unknown>): void {
|
||||||
const newOpts = { ...this.props.options, ...opts };
|
const newOpts = { ...this.props.options, ...opts };
|
||||||
this.props.onOptionsChanged(newOpts);
|
this.props.onOptionsChanged(newOpts);
|
||||||
}
|
}
|
||||||
|
@ -230,15 +232,15 @@ class Panel extends Component<PanelProps, PanelState> {
|
||||||
return this.props.options.endTime;
|
return this.props.options.endTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChangeEndTime = (endTime: number | null) => {
|
handleChangeEndTime = (endTime: number | null): void => {
|
||||||
this.setOptions({ endTime: endTime });
|
this.setOptions({ endTime: endTime });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChangeResolution = (resolution: number | null) => {
|
handleChangeResolution = (resolution: number | null): void => {
|
||||||
this.setOptions({ resolution: resolution });
|
this.setOptions({ resolution: resolution });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChangeType = (type: PanelType) => {
|
handleChangeType = (type: PanelType): void => {
|
||||||
if (this.props.options.type === type) {
|
if (this.props.options.type === type) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -247,19 +249,19 @@ class Panel extends Component<PanelProps, PanelState> {
|
||||||
this.setOptions({ type: type });
|
this.setOptions({ type: type });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChangeStacking = (stacked: boolean) => {
|
handleChangeStacking = (stacked: boolean): void => {
|
||||||
this.setOptions({ stacked: stacked });
|
this.setOptions({ stacked: stacked });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChangeShowExemplars = (show: boolean) => {
|
handleChangeShowExemplars = (show: boolean): void => {
|
||||||
this.setOptions({ showExemplars: show });
|
this.setOptions({ showExemplars: show });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleTimeRangeSelection = (startTime: number, endTime: number) => {
|
handleTimeRangeSelection = (startTime: number, endTime: number): void => {
|
||||||
this.setOptions({ range: endTime - startTime, endTime: endTime });
|
this.setOptions({ range: endTime - startTime, endTime: endTime });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { pastQueries, metricNames, options } = this.props;
|
const { pastQueries, metricNames, options } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { API_PATH } from '../../constants/constants';
|
||||||
|
|
||||||
export type PanelMeta = { key: string; options: PanelOptions; id: string };
|
export type PanelMeta = { key: string; options: PanelOptions; id: string };
|
||||||
|
|
||||||
export const updateURL = (nextPanels: PanelMeta[]) => {
|
export const updateURL = (nextPanels: PanelMeta[]): void => {
|
||||||
const query = encodePanelOptionsToQueryString(nextPanels);
|
const query = encodePanelOptionsToQueryString(nextPanels);
|
||||||
window.history.pushState({}, '', query);
|
window.history.pushState({}, '', query);
|
||||||
};
|
};
|
||||||
|
@ -91,8 +91,8 @@ export const PanelListContent: FC<PanelListContentProps> = ({
|
||||||
key={id}
|
key={id}
|
||||||
id={id}
|
id={id}
|
||||||
options={options}
|
options={options}
|
||||||
onOptionsChanged={opts =>
|
onOptionsChanged={(opts) =>
|
||||||
callAll(setPanels, updateURL)(panels.map(p => (id === p.id ? { ...p, options: opts } : p)))
|
callAll(setPanels, updateURL)(panels.map((p) => (id === p.id ? { ...p, options: opts } : p)))
|
||||||
}
|
}
|
||||||
removePanel={() =>
|
removePanel={() =>
|
||||||
callAll(
|
callAll(
|
||||||
|
|
|
@ -6,7 +6,7 @@ export interface QueryStats {
|
||||||
resultSeries: number;
|
resultSeries: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryStatsView: FC<QueryStats> = props => {
|
const QueryStatsView: FC<QueryStats> = (props) => {
|
||||||
const { loadTime, resolution, resultSeries } = props;
|
const { loadTime, resolution, resultSeries } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -55,7 +55,7 @@ describe('SeriesName', () => {
|
||||||
const child = seriesName.childAt(i);
|
const child = seriesName.childAt(i);
|
||||||
const text = child
|
const text = child
|
||||||
.children()
|
.children()
|
||||||
.map(ch => ch.text())
|
.map((ch) => ch.text())
|
||||||
.join('');
|
.join('');
|
||||||
switch (child.children().length) {
|
switch (child.children().length) {
|
||||||
case 1:
|
case 1:
|
||||||
|
|
|
@ -29,7 +29,7 @@ const SeriesName: FC<SeriesNameProps> = ({ labels, format }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span className="legend-metric-name">{labels!.__name__ || ''}</span>
|
<span className="legend-metric-name">{labels ? labels.__name__ : ''}</span>
|
||||||
<span className="legend-label-brace">{'{'}</span>
|
<span className="legend-label-brace">{'{'}</span>
|
||||||
{labelNodes}
|
{labelNodes}
|
||||||
<span className="legend-label-brace">{'}'}</span>
|
<span className="legend-label-brace">{'}'}</span>
|
||||||
|
@ -46,7 +46,7 @@ const SeriesName: FC<SeriesNameProps> = ({ labels, format }) => {
|
||||||
}
|
}
|
||||||
// Return a simple text node. This is much faster to scroll through
|
// Return a simple text node. This is much faster to scroll through
|
||||||
// for longer lists (hundreds of items).
|
// for longer lists (hundreds of items).
|
||||||
return <>{metricToSeriesName(labels!)}</>;
|
return <>{metricToSeriesName(labels)}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SeriesName;
|
export default SeriesName;
|
||||||
|
|
|
@ -39,11 +39,11 @@ describe('TimeInput', () => {
|
||||||
title: 'Increase time',
|
title: 'Increase time',
|
||||||
icon: faChevronRight,
|
icon: faChevronRight,
|
||||||
},
|
},
|
||||||
].forEach(button => {
|
].forEach((button) => {
|
||||||
const onChangeTime = sinon.spy();
|
const onChangeTime = sinon.spy();
|
||||||
const timeInput = shallow(<TimeInput {...timeInputProps} onChangeTime={onChangeTime} />);
|
const timeInput = shallow(<TimeInput {...timeInputProps} onChangeTime={onChangeTime} />);
|
||||||
const addon = timeInput.find(InputGroupAddon).filterWhere(addon => addon.prop('addonType') === button.position);
|
const addon = timeInput.find(InputGroupAddon).filterWhere((addon) => addon.prop('addonType') === button.position);
|
||||||
const btn = addon.find(Button).filterWhere(btn => btn.prop('title') === button.title);
|
const btn = addon.find(Button).filterWhere((btn) => btn.prop('title') === button.title);
|
||||||
const icon = btn.find(FontAwesomeIcon);
|
const icon = btn.find(FontAwesomeIcon);
|
||||||
expect(icon.prop('icon')).toEqual(button.icon);
|
expect(icon.prop('icon')).toEqual(button.icon);
|
||||||
expect(icon.prop('fixedWidth')).toBe(true);
|
expect(icon.prop('fixedWidth')).toBe(true);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Button, InputGroup, InputGroupAddon, Input } from 'reactstrap';
|
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||||
|
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
@ -11,11 +11,11 @@ import '../../../node_modules/tempusdominus-bootstrap-4/build/css/tempusdominus-
|
||||||
import { dom, library } from '@fortawesome/fontawesome-svg-core';
|
import { dom, library } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import {
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faCalendarCheck,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faCalendarCheck,
|
|
||||||
faArrowUp,
|
|
||||||
faArrowDown,
|
|
||||||
faTimes,
|
faTimes,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
@ -33,13 +33,14 @@ interface TimeInputProps {
|
||||||
|
|
||||||
class TimeInput extends Component<TimeInputProps> {
|
class TimeInput extends Component<TimeInputProps> {
|
||||||
private timeInputRef = React.createRef<HTMLInputElement>();
|
private timeInputRef = React.createRef<HTMLInputElement>();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
private $time: any = null;
|
private $time: any = null;
|
||||||
|
|
||||||
getBaseTime = (): number => {
|
getBaseTime = (): number => {
|
||||||
return this.props.time || moment().valueOf();
|
return this.props.time || moment().valueOf();
|
||||||
};
|
};
|
||||||
|
|
||||||
calcShiftRange = () => this.props.range / 2;
|
calcShiftRange = (): number => this.props.range / 2;
|
||||||
|
|
||||||
increaseTime = (): void => {
|
increaseTime = (): void => {
|
||||||
const time = this.getBaseTime() + this.calcShiftRange();
|
const time = this.getBaseTime() + this.calcShiftRange();
|
||||||
|
@ -59,8 +60,11 @@ class TimeInput extends Component<TimeInputProps> {
|
||||||
return this.props.useLocalTime ? moment.tz.guess() : 'UTC';
|
return this.props.useLocalTime ? moment.tz.guess() : 'UTC';
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount(): void {
|
||||||
this.$time = $(this.timeInputRef.current!);
|
if (!this.timeInputRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$time = $(this.timeInputRef.current);
|
||||||
|
|
||||||
this.$time.datetimepicker({
|
this.$time.datetimepicker({
|
||||||
icons: {
|
icons: {
|
||||||
|
@ -78,20 +82,21 @@ class TimeInput extends Component<TimeInputProps> {
|
||||||
defaultDate: this.props.time,
|
defaultDate: this.props.time,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
this.$time.on('change.datetimepicker', (e: any) => {
|
this.$time.on('change.datetimepicker', (e: any) => {
|
||||||
// The end time can also be set by dragging a section on the graph,
|
// The end time can also be set by dragging a section on the graph,
|
||||||
// and that value will have decimal places.
|
// and that value will have decimal places.
|
||||||
if (e.date && e.date.valueOf() !== Math.trunc(this.props.time?.valueOf()!)) {
|
if (e.date && e.date.valueOf() !== Math.trunc(this.props.time?.valueOf() || NaN)) {
|
||||||
this.props.onChangeTime(e.date.valueOf());
|
this.props.onChangeTime(e.date.valueOf());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount(): void {
|
||||||
this.$time.datetimepicker('destroy');
|
this.$time.datetimepicker('destroy');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: TimeInputProps) {
|
componentDidUpdate(prevProps: TimeInputProps): void {
|
||||||
const { time, useLocalTime } = this.props;
|
const { time, useLocalTime } = this.props;
|
||||||
if (prevProps.time !== time) {
|
if (prevProps.time !== time) {
|
||||||
this.$time.datetimepicker('date', time ? moment(time) : null);
|
this.$time.datetimepicker('date', time ? moment(time) : null);
|
||||||
|
@ -101,7 +106,7 @@ class TimeInput extends Component<TimeInputProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<InputGroup className="time-input" size="sm">
|
<InputGroup className="time-input" size="sm">
|
||||||
<InputGroupAddon addonType="prepend">
|
<InputGroupAddon addonType="prepend">
|
||||||
|
@ -115,7 +120,7 @@ class TimeInput extends Component<TimeInputProps> {
|
||||||
innerRef={this.timeInputRef}
|
innerRef={this.timeInputRef}
|
||||||
onFocus={() => this.$time.datetimepicker('show')}
|
onFocus={() => this.$time.datetimepicker('show')}
|
||||||
onBlur={() => this.$time.datetimepicker('hide')}
|
onBlur={() => this.$time.datetimepicker('hide')}
|
||||||
onKeyDown={e => ['Escape', 'Enter'].includes(e.key) && this.$time.datetimepicker('hide')}
|
onKeyDown={(e) => ['Escape', 'Enter'].includes(e.key) && this.$time.datetimepicker('hide')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* CAUTION: While the datetimepicker also has an option to show a 'clear' button,
|
{/* CAUTION: While the datetimepicker also has an option to show a 'clear' button,
|
||||||
|
|
|
@ -22,7 +22,7 @@ export interface RulesMap {
|
||||||
groups: RuleGroup[];
|
groups: RuleGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphExpressionLink: FC<{ expr: string; text: string; title: string }> = props => {
|
const GraphExpressionLink: FC<{ expr: string; text: string; title: string }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<strong>{props.title}:</strong>
|
<strong>{props.title}:</strong>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useFetch } from '../../hooks/useFetch';
|
import { useFetch } from '../../hooks/useFetch';
|
||||||
import { LabelsTable } from './LabelsTable';
|
import { LabelsTable } from './LabelsTable';
|
||||||
import { Target, Labels, DroppedTarget } from '../targets/target';
|
import { DroppedTarget, Labels, Target } from '../targets/target';
|
||||||
|
|
||||||
import { withStatusIndicator } from '../../components/withStatusIndicator';
|
import { withStatusIndicator } from '../../components/withStatusIndicator';
|
||||||
import { mapObjEntries } from '../../utils';
|
import { mapObjEntries } from '../../utils';
|
||||||
|
@ -19,7 +19,10 @@ export interface TargetLabels {
|
||||||
isDropped: boolean;
|
isDropped: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const processSummary = (activeTargets: Target[], droppedTargets: DroppedTarget[]) => {
|
export const processSummary = (
|
||||||
|
activeTargets: Target[],
|
||||||
|
droppedTargets: DroppedTarget[]
|
||||||
|
): Record<string, { active: number; total: number }> => {
|
||||||
const targets: Record<string, { active: number; total: number }> = {};
|
const targets: Record<string, { active: number; total: number }> = {};
|
||||||
|
|
||||||
// Get targets of each type along with the total and active end points
|
// Get targets of each type along with the total and active end points
|
||||||
|
@ -48,7 +51,7 @@ export const processSummary = (activeTargets: Target[], droppedTargets: DroppedT
|
||||||
return targets;
|
return targets;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const processTargets = (activeTargets: Target[], droppedTargets: DroppedTarget[]) => {
|
export const processTargets = (activeTargets: Target[], droppedTargets: DroppedTarget[]): Record<string, TargetLabels[]> => {
|
||||||
const labels: Record<string, TargetLabels[]> = {};
|
const labels: Record<string, TargetLabels[]> = {};
|
||||||
|
|
||||||
for (const target of activeTargets) {
|
for (const target of activeTargets) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface StatusPageProps {
|
||||||
|
|
||||||
export const statusConfig: Record<
|
export const statusConfig: Record<
|
||||||
string,
|
string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
{ title?: string; customizeValue?: (v: any, key: string) => any; customRow?: boolean; skip?: boolean }
|
{ title?: string; customizeValue?: (v: any, key: string) => any; customRow?: boolean; skip?: boolean }
|
||||||
> = {
|
> = {
|
||||||
startTime: { title: 'Start time', customizeValue: (v: string) => new Date(v).toUTCString() },
|
startTime: { title: 'Start time', customizeValue: (v: string) => new Date(v).toUTCString() },
|
||||||
|
@ -57,7 +58,7 @@ export const StatusContent: FC<StatusPageProps> = ({ data, title }) => {
|
||||||
<Table className="h-auto" size="sm" bordered striped>
|
<Table className="h-auto" size="sm" bordered striped>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.entries(data).map(([k, v]) => {
|
{Object.entries(data).map(([k, v]) => {
|
||||||
const { title = k, customizeValue = (val: any) => val, customRow, skip } = statusConfig[k] || {};
|
const { title = k, customizeValue = (val: string) => val, customRow, skip } = statusConfig[k] || {};
|
||||||
if (skip) {
|
if (skip) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,16 @@ describe('EndpointLink', () => {
|
||||||
expect(anchor.children().text()).toEqual('http://100.99.128.71:9115/probe');
|
expect(anchor.children().text()).toEqual('http://100.99.128.71:9115/probe');
|
||||||
expect(endpointLink.find('br')).toHaveLength(1);
|
expect(endpointLink.find('br')).toHaveLength(1);
|
||||||
expect(badges).toHaveLength(2);
|
expect(badges).toHaveLength(2);
|
||||||
const moduleLabel = badges.filterWhere(badge => badge.children().text() === 'module="http_2xx"');
|
const moduleLabel = badges.filterWhere((badge) => badge.children().text() === 'module="http_2xx"');
|
||||||
expect(moduleLabel.length).toEqual(1);
|
expect(moduleLabel.length).toEqual(1);
|
||||||
const targetLabel = badges.filterWhere(badge => badge.children().text() === 'target="http://some-service"');
|
const targetLabel = badges.filterWhere((badge) => badge.children().text() === 'target="http://some-service"');
|
||||||
expect(targetLabel.length).toEqual(1);
|
expect(targetLabel.length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an alert if url is invalid', () => {
|
it('renders an alert if url is invalid', () => {
|
||||||
const endpointLink = shallow(<EndpointLink endpoint={'afdsacas'} globalUrl={'afdsacas'} />);
|
const endpointLink = shallow(<EndpointLink endpoint={'afdsacas'} globalUrl={'afdsacas'} />);
|
||||||
const err = endpointLink.find(Alert);
|
const err = endpointLink.find(Alert);
|
||||||
expect(err.render().text()).toEqual('Error: Invalid URL');
|
expect(err.render().text()).toEqual('Error: Invalid URL: afdsacas');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles params with multiple values correctly', () => {
|
it('handles params with multiple values correctly', () => {
|
||||||
|
|
|
@ -15,7 +15,7 @@ describe('Filter', () => {
|
||||||
showUnhealthy: true,
|
showUnhealthy: true,
|
||||||
};
|
};
|
||||||
let setFilter: SinonSpy;
|
let setFilter: SinonSpy;
|
||||||
let filterWrapper: ShallowWrapper<FilterProps, Readonly<{}>, Component<{}, {}, Component>>;
|
let filterWrapper: ShallowWrapper<FilterProps, Readonly<unknown>, Component<unknown, unknown, Component>>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setFilter = sinon.spy();
|
setFilter = sinon.spy();
|
||||||
setExpaned = sinon.spy();
|
setExpaned = sinon.spy();
|
||||||
|
|
|
@ -50,7 +50,7 @@ describe('ScrapePoolList', () => {
|
||||||
expect(panels).toHaveLength(3);
|
expect(panels).toHaveLength(3);
|
||||||
const activeTargets: Target[] = sampleApiResponse.data.activeTargets as Target[];
|
const activeTargets: Target[] = sampleApiResponse.data.activeTargets as Target[];
|
||||||
activeTargets.forEach(({ scrapePool }: Target) => {
|
activeTargets.forEach(({ scrapePool }: Target) => {
|
||||||
const panel = scrapePoolList.find(ScrapePoolPanel).filterWhere(panel => panel.prop('scrapePool') === scrapePool);
|
const panel = scrapePoolList.find(ScrapePoolPanel).filterWhere((panel) => panel.prop('scrapePool') === scrapePool);
|
||||||
expect(panel).toHaveLength(1);
|
expect(panel).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,12 +34,12 @@ export const ScrapePoolContent: FC<ScrapePoolListProps> = ({ activeTargets }) =>
|
||||||
<>
|
<>
|
||||||
<Filter filter={filter} setFilter={setFilter} expanded={expanded} setExpanded={setExpanded} />
|
<Filter filter={filter} setFilter={setFilter} expanded={expanded} setExpanded={setExpanded} />
|
||||||
{Object.keys(targetGroups)
|
{Object.keys(targetGroups)
|
||||||
.filter(scrapePool => {
|
.filter((scrapePool) => {
|
||||||
const targetGroup = targetGroups[scrapePool];
|
const targetGroup = targetGroups[scrapePool];
|
||||||
const isHealthy = targetGroup.upCount === targetGroup.targets.length;
|
const isHealthy = targetGroup.upCount === targetGroup.targets.length;
|
||||||
return (isHealthy && showHealthy) || (!isHealthy && showUnhealthy);
|
return (isHealthy && showHealthy) || (!isHealthy && showUnhealthy);
|
||||||
})
|
})
|
||||||
.map<JSX.Element>(scrapePool => (
|
.map<JSX.Element>((scrapePool) => (
|
||||||
<ScrapePoolPanel
|
<ScrapePoolPanel
|
||||||
key={scrapePool}
|
key={scrapePool}
|
||||||
scrapePool={scrapePool}
|
scrapePool={scrapePool}
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe('ScrapePoolPanel', () => {
|
||||||
const scrapePoolPanel = shallow(<ScrapePoolPanel {...defaultProps} />);
|
const scrapePoolPanel = shallow(<ScrapePoolPanel {...defaultProps} />);
|
||||||
|
|
||||||
it('renders a container', () => {
|
it('renders a container', () => {
|
||||||
const div = scrapePoolPanel.find('div').filterWhere(elem => elem.hasClass('container'));
|
const div = scrapePoolPanel.find('div').filterWhere((elem) => elem.hasClass('container'));
|
||||||
expect(div).toHaveLength(1);
|
expect(div).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ describe('ScrapePoolPanel', () => {
|
||||||
const headers = table.find('th');
|
const headers = table.find('th');
|
||||||
expect(table).toHaveLength(1);
|
expect(table).toHaveLength(1);
|
||||||
expect(headers).toHaveLength(6);
|
expect(headers).toHaveLength(6);
|
||||||
columns.forEach(col => {
|
columns.forEach((col) => {
|
||||||
expect(headers.contains(col));
|
expect(headers.contains(col));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -97,7 +97,7 @@ describe('ScrapePoolPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a badge for health', () => {
|
it('renders a badge for health', () => {
|
||||||
const td = row.find('td').filterWhere(elem => Boolean(elem.hasClass('state')));
|
const td = row.find('td').filterWhere((elem) => Boolean(elem.hasClass('state')));
|
||||||
const badge = td.find(Badge);
|
const badge = td.find(Badge);
|
||||||
expect(badge).toHaveLength(1);
|
expect(badge).toHaveLength(1);
|
||||||
expect(badge.prop('color')).toEqual(getColor(health));
|
expect(badge.prop('color')).toEqual(getColor(health));
|
||||||
|
@ -112,17 +112,17 @@ describe('ScrapePoolPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders last scrape time', () => {
|
it('renders last scrape time', () => {
|
||||||
const lastScrapeCell = row.find('td').filterWhere(elem => Boolean(elem.hasClass('last-scrape')));
|
const lastScrapeCell = row.find('td').filterWhere((elem) => Boolean(elem.hasClass('last-scrape')));
|
||||||
expect(lastScrapeCell).toHaveLength(1);
|
expect(lastScrapeCell).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders last scrape duration', () => {
|
it('renders last scrape duration', () => {
|
||||||
const lastScrapeCell = row.find('td').filterWhere(elem => Boolean(elem.hasClass('scrape-duration')));
|
const lastScrapeCell = row.find('td').filterWhere((elem) => Boolean(elem.hasClass('scrape-duration')));
|
||||||
expect(lastScrapeCell).toHaveLength(1);
|
expect(lastScrapeCell).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a badge for Errors', () => {
|
it('renders a badge for Errors', () => {
|
||||||
const td = row.find('td').filterWhere(elem => Boolean(elem.hasClass('errors')));
|
const td = row.find('td').filterWhere((elem) => Boolean(elem.hasClass('errors')));
|
||||||
const badge = td.find(Badge);
|
const badge = td.find(Badge);
|
||||||
expect(badge).toHaveLength(lastError ? 1 : 0);
|
expect(badge).toHaveLength(lastError ? 1 : 0);
|
||||||
if (lastError) {
|
if (lastError) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ const ScrapePoolPanel: FC<PanelProps> = ({ scrapePool, targetGroup, expanded, to
|
||||||
<Table className={styles.table} size="sm" bordered hover striped>
|
<Table className={styles.table} size="sm" bordered hover striped>
|
||||||
<thead>
|
<thead>
|
||||||
<tr key="header">
|
<tr key="header">
|
||||||
{columns.map(column => (
|
{columns.map((column) => (
|
||||||
<th key={column}>{column}</th>
|
<th key={column}>{column}</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -23,7 +23,7 @@ describe('targetLabels', () => {
|
||||||
const targetLabels = shallow(<TargetLabels {...defaultProps} />);
|
const targetLabels = shallow(<TargetLabels {...defaultProps} />);
|
||||||
|
|
||||||
it('renders a div of series labels', () => {
|
it('renders a div of series labels', () => {
|
||||||
const div = targetLabels.find('div').filterWhere(elem => elem.hasClass('series-labels-container'));
|
const div = targetLabels.find('div').filterWhere((elem) => elem.hasClass('series-labels-container'));
|
||||||
expect(div).toHaveLength(1);
|
expect(div).toHaveLength(1);
|
||||||
expect(div.prop('id')).toEqual('series-labels-cortex/node-exporter_group/0-1');
|
expect(div.prop('id')).toEqual('series-labels-cortex/node-exporter_group/0-1');
|
||||||
});
|
});
|
||||||
|
@ -33,7 +33,7 @@ describe('targetLabels', () => {
|
||||||
Object.keys(l).forEach((labelName: string): void => {
|
Object.keys(l).forEach((labelName: string): void => {
|
||||||
const badge = targetLabels
|
const badge = targetLabels
|
||||||
.find(Badge)
|
.find(Badge)
|
||||||
.filterWhere(badge => badge.children().text() === `${labelName}="${l[labelName]}"`);
|
.filterWhere((badge) => badge.children().text() === `${labelName}="${l[labelName]}"`);
|
||||||
expect(badge).toHaveLength(1);
|
expect(badge).toHaveLength(1);
|
||||||
});
|
});
|
||||||
expect(targetLabels.find(Badge)).toHaveLength(3);
|
expect(targetLabels.find(Badge)).toHaveLength(3);
|
||||||
|
|
|
@ -14,7 +14,7 @@ export interface TargetLabelsProps {
|
||||||
scrapePool: string;
|
scrapePool: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatLabels = (labels: Labels): string[] => Object.keys(labels).map(key => `${key}="${labels[key]}"`);
|
const formatLabels = (labels: Labels): string[] => Object.keys(labels).map((key) => `${key}="${labels[key]}"`);
|
||||||
|
|
||||||
const TargetLabels: FC<TargetLabelsProps> = ({ discoveredLabels, labels, idx, scrapePool }) => {
|
const TargetLabels: FC<TargetLabelsProps> = ({ discoveredLabels, labels, idx, scrapePool }) => {
|
||||||
const [tooltipOpen, setTooltipOpen] = useState(false);
|
const [tooltipOpen, setTooltipOpen] = useState(false);
|
||||||
|
@ -25,7 +25,7 @@ const TargetLabels: FC<TargetLabelsProps> = ({ discoveredLabels, labels, idx, sc
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id={id} className="series-labels-container">
|
<div id={id} className="series-labels-container">
|
||||||
{Object.keys(labels).map(labelName => {
|
{Object.keys(labels).map((labelName) => {
|
||||||
return (
|
return (
|
||||||
<Badge color="primary" className="mr-1" key={labelName}>
|
<Badge color="primary" className="mr-1" key={labelName}>
|
||||||
{`${labelName}="${labels[labelName]}"`}
|
{`${labelName}="${labels[labelName]}"`}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* eslint @typescript-eslint/camelcase: 0 */
|
|
||||||
|
|
||||||
import { ScrapePools } from '../target';
|
import { ScrapePools } from '../target';
|
||||||
|
|
||||||
export const targetGroups: ScrapePools = Object.freeze({
|
export const targetGroups: ScrapePools = Object.freeze({
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* eslint @typescript-eslint/camelcase: 0 */
|
|
||||||
|
|
||||||
import { sampleApiResponse } from './__testdata__/testdata';
|
import { sampleApiResponse } from './__testdata__/testdata';
|
||||||
import { groupTargets, Target, ScrapePools, getColor } from './target';
|
import { groupTargets, Target, ScrapePools, getColor } from './target';
|
||||||
|
|
||||||
|
@ -8,7 +6,7 @@ describe('groupTargets', () => {
|
||||||
const targetGroups: ScrapePools = groupTargets(targets);
|
const targetGroups: ScrapePools = groupTargets(targets);
|
||||||
|
|
||||||
it('groups a list of targets by scrape job', () => {
|
it('groups a list of targets by scrape job', () => {
|
||||||
['blackbox', 'prometheus/test', 'node_exporter'].forEach(scrapePool => {
|
['blackbox', 'prometheus/test', 'node_exporter'].forEach((scrapePool) => {
|
||||||
expect(Object.keys(targetGroups)).toContain(scrapePool);
|
expect(Object.keys(targetGroups)).toContain(scrapePool);
|
||||||
});
|
});
|
||||||
Object.keys(targetGroups).forEach((scrapePool: string): void => {
|
Object.keys(targetGroups).forEach((scrapePool: string): void => {
|
||||||
|
|
|
@ -121,11 +121,7 @@ describe('TSDB Stats', () => {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
});
|
});
|
||||||
|
|
||||||
const headStats = page
|
const headStats = page.find(Table).at(0).find('tbody').find('td');
|
||||||
.find(Table)
|
|
||||||
.at(0)
|
|
||||||
.find('tbody')
|
|
||||||
.find('td');
|
|
||||||
['508', '937', '1234', '2020-06-07T08:00:00.000Z (1591516800000)', '2020-08-31T18:00:00.143Z (1598896800143)'].forEach(
|
['508', '937', '1234', '2020-06-07T08:00:00.000Z (1591516800000)', '2020-08-31T18:00:00.143Z (1598896800143)'].forEach(
|
||||||
(value, i) => {
|
(value, i) => {
|
||||||
expect(headStats.at(i).text()).toEqual(value);
|
expect(headStats.at(i).text()).toEqual(value);
|
||||||
|
@ -170,11 +166,7 @@ describe('TSDB Stats', () => {
|
||||||
|
|
||||||
expect(page.find('h2').text()).toEqual('TSDB Status');
|
expect(page.find('h2').text()).toEqual('TSDB Status');
|
||||||
|
|
||||||
const headStats = page
|
const headStats = page.find(Table).at(0).find('tbody').find('td');
|
||||||
.find(Table)
|
|
||||||
.at(0)
|
|
||||||
.find('tbody')
|
|
||||||
.find('td');
|
|
||||||
['0', '0', '0', 'No datapoints yet', 'No datapoints yet'].forEach((value, i) => {
|
['0', '0', '0', 'No datapoints yet', 'No datapoints yet'].forEach((value, i) => {
|
||||||
expect(headStats.at(i).text()).toEqual(value);
|
expect(headStats.at(i).text()).toEqual(value);
|
||||||
});
|
});
|
||||||
|
@ -199,11 +191,7 @@ describe('TSDB Stats', () => {
|
||||||
|
|
||||||
expect(page.find('h2').text()).toEqual('TSDB Status');
|
expect(page.find('h2').text()).toEqual('TSDB Status');
|
||||||
|
|
||||||
const headStats = page
|
const headStats = page.find(Table).at(0).find('tbody').find('td');
|
||||||
.find(Table)
|
|
||||||
.at(0)
|
|
||||||
.find('tbody')
|
|
||||||
.find('td');
|
|
||||||
['1', '0', '0', 'Error parsing time (9223372036854776000)', 'Error parsing time (-9223372036854776000)'].forEach(
|
['1', '0', '0', 'Error parsing time (9223372036854776000)', 'Error parsing time (-9223372036854776000)'].forEach(
|
||||||
(value, i) => {
|
(value, i) => {
|
||||||
expect(headStats.at(i).text()).toEqual(value);
|
expect(headStats.at(i).text()).toEqual(value);
|
||||||
|
|
2
web/ui/react-app/src/types/index.d.ts
vendored
2
web/ui/react-app/src/types/index.d.ts
vendored
|
@ -1,9 +1,7 @@
|
||||||
declare namespace jquery.flot {
|
declare namespace jquery.flot {
|
||||||
// eslint-disable-next-line @typescript-eslint/class-name-casing
|
|
||||||
interface plot extends jquery.flot.plot {
|
interface plot extends jquery.flot.plot {
|
||||||
destroy: () => void;
|
destroy: () => void;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/class-name-casing
|
|
||||||
interface plotOptions extends jquery.flot.plotOptions {
|
interface plotOptions extends jquery.flot.plotOptions {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
|
|
|
@ -3,13 +3,11 @@ import moment from 'moment-timezone';
|
||||||
import { PanelOptions, PanelType, PanelDefaultOptions } from '../pages/graph/Panel';
|
import { PanelOptions, PanelType, PanelDefaultOptions } from '../pages/graph/Panel';
|
||||||
import { PanelMeta } from '../pages/graph/PanelList';
|
import { PanelMeta } from '../pages/graph/PanelList';
|
||||||
|
|
||||||
export const generateID = () => {
|
export const generateID = (): string => {
|
||||||
return `_${Math.random()
|
return `_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
.toString(36)
|
|
||||||
.substr(2, 9)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const byEmptyString = (p: string) => p.length > 0;
|
export const byEmptyString = (p: string): boolean => p.length > 0;
|
||||||
|
|
||||||
export const isPresent = <T>(obj: T): obj is NonNullable<T> => obj !== null && obj !== undefined;
|
export const isPresent = <T>(obj: T): obj is NonNullable<T> => obj !== null && obj !== undefined;
|
||||||
|
|
||||||
|
@ -28,7 +26,7 @@ export const escapeHTML = (str: string): string => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const metricToSeriesName = (labels: { [key: string]: string }) => {
|
export const metricToSeriesName = (labels: { [key: string]: string }): string => {
|
||||||
if (labels === null) {
|
if (labels === null) {
|
||||||
return 'scalar';
|
return 'scalar';
|
||||||
}
|
}
|
||||||
|
@ -219,11 +217,13 @@ export const parseOption = (param: string): Partial<PanelOptions> => {
|
||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatParam = (key: string) => (paramName: string, value: number | string | boolean) => {
|
export const formatParam =
|
||||||
|
(key: string) =>
|
||||||
|
(paramName: string, value: number | string | boolean): string => {
|
||||||
return `g${key}.${paramName}=${encodeURIComponent(value)}`;
|
return `g${key}.${paramName}=${encodeURIComponent(value)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toQueryString = ({ key, options }: PanelMeta) => {
|
export const toQueryString = ({ key, options }: PanelMeta): string => {
|
||||||
const formatWithKey = formatParam(key);
|
const formatWithKey = formatParam(key);
|
||||||
const { expr, type, stacked, range, endTime, resolution, showExemplars } = options;
|
const { expr, type, stacked, range, endTime, resolution, showExemplars } = options;
|
||||||
const time = isPresent(endTime) ? formatTime(endTime) : false;
|
const time = isPresent(endTime) ? formatTime(endTime) : false;
|
||||||
|
@ -239,24 +239,30 @@ export const toQueryString = ({ key, options }: PanelMeta) => {
|
||||||
return urlParams.filter(byEmptyString).join('&');
|
return urlParams.filter(byEmptyString).join('&');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const encodePanelOptionsToQueryString = (panels: PanelMeta[]) => {
|
export const encodePanelOptionsToQueryString = (panels: PanelMeta[]): string => {
|
||||||
return `?${panels.map(toQueryString).join('&')}`;
|
return `?${panels.map(toQueryString).join('&')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createExpressionLink = (expr: string) => {
|
export const createExpressionLink = (expr: string): string => {
|
||||||
return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.show_exemplars=0.g0.range_input=1h.`;
|
return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.show_exemplars=0.g0.range_input=1h.`;
|
||||||
};
|
};
|
||||||
export const mapObjEntries = <T, key extends keyof T, Z>(
|
export const mapObjEntries = <T, key extends keyof T, Z>(
|
||||||
o: T,
|
o: T,
|
||||||
cb: ([k, v]: [string, T[key]], i: number, arr: [string, T[key]][]) => Z
|
cb: ([k, v]: [string, T[key]], i: number, arr: [string, T[key]][]) => Z
|
||||||
) => Object.entries(o).map(cb);
|
): Z[] => Object.entries(o).map(cb);
|
||||||
|
|
||||||
export const callAll = (...fns: Array<(...args: any) => void>) => (...args: any) => {
|
export const callAll =
|
||||||
|
(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
...fns: Array<(...args: any) => void>
|
||||||
|
) =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||||
|
(...args: any): void => {
|
||||||
// eslint-disable-next-line prefer-spread
|
// eslint-disable-next-line prefer-spread
|
||||||
fns.filter(Boolean).forEach(fn => fn.apply(null, args));
|
fns.filter(Boolean).forEach((fn) => fn.apply(null, args));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parsePrometheusFloat = (value: string) => {
|
export const parsePrometheusFloat = (value: string): string | number => {
|
||||||
if (isNaN(Number(value))) {
|
if (isNaN(Number(value))) {
|
||||||
return value;
|
return value;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -136,7 +136,7 @@ describe('Utils', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
tests.forEach(t => {
|
tests.forEach((t) => {
|
||||||
it(t.input, () => {
|
it(t.input, () => {
|
||||||
const d = parseDuration(t.input);
|
const d = parseDuration(t.input);
|
||||||
expect(d).toEqual(t.output);
|
expect(d).toEqual(t.output);
|
||||||
|
@ -148,7 +148,7 @@ describe('Utils', () => {
|
||||||
describe('should fail to parse invalid durations', () => {
|
describe('should fail to parse invalid durations', () => {
|
||||||
const tests = ['1', '1y1m1d', '-1w', '1.5d', 'd', ''];
|
const tests = ['1', '1y1m1d', '-1w', '1.5d', 'd', ''];
|
||||||
|
|
||||||
tests.forEach(t => {
|
tests.forEach((t) => {
|
||||||
it(t, () => {
|
it(t, () => {
|
||||||
expect(parseDuration(t)).toBe(null);
|
expect(parseDuration(t)).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,9 +17,12 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "preserve"
|
"jsx": "react",
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src", "test", "react-app-env.d.ts"
|
"src",
|
||||||
|
"test",
|
||||||
|
"react-app-env.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue