mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: eslint react hook fix for public folder (#31174)
* Fixes under public/app/plugins * Fixes under public/app/plugins/datasource * Fixes under public/app/features * Fixes under public/app/features * Fixes under public/app/features * Fixes under public/app/components * Fix PanelNotSupported test * Fix one more warning * Fix warning in usePanelSave * Fix traceview empty response * Azure monitor fixes * More fixes * Fix tests for azure monitor * Fixes after merging master * Add comment for disabled rules * Fixes after merging master * Fixes after merging master * Adress review comments * Fix azure tests * Address review feedbacks
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
{
|
||||
"extends": ["@grafana/eslint-config"],
|
||||
"root": true,
|
||||
"plugins": [
|
||||
"no-only-tests"
|
||||
],
|
||||
"plugins": ["no-only-tests"],
|
||||
"rules": {
|
||||
"no-only-tests/no-only-tests": "error",
|
||||
"react/prop-types": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"packages/grafana-ui/src/components/uPlot/**/*.{ts,tsx}",
|
||||
"public/app/**/*.{ts,tsx}"
|
||||
],
|
||||
"files": ["packages/grafana-ui/src/components/uPlot/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
|
||||
@@ -46,6 +46,7 @@ export const SigV4AuthSettings: React.FC<HttpSettingsProps> = (props) => {
|
||||
useEffect(() => {
|
||||
const sigV4AuthType = dataSourceConfig.jsonData.sigV4AuthType || 'default';
|
||||
onJsonDataChange('sigV4AuthType', sigV4AuthType);
|
||||
// We can't enforce the eslint rule here because we only want to run this once.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -21,19 +21,10 @@ export const FilterByValueFilterEditor: React.FC<Props> = (props) => {
|
||||
const { fieldsAsOptions, fieldByDisplayName } = fieldsInfo;
|
||||
const fieldName = getFieldName(filter, fieldsAsOptions) ?? '';
|
||||
const field = fieldByDisplayName[fieldName];
|
||||
|
||||
if (!field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matcherOptions = getMatcherOptions(field);
|
||||
const matcherId = getSelectedMatcherId(filter, matcherOptions);
|
||||
const editor = valueMatchersUI.getIfExists(matcherId);
|
||||
|
||||
if (!editor || !editor.component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onChangeField = useCallback(
|
||||
(selectable?: SelectableValue<string>) => {
|
||||
if (!selectable?.value) {
|
||||
@@ -77,6 +68,10 @@ export const FilterByValueFilterEditor: React.FC<Props> = (props) => {
|
||||
[onChange, filter]
|
||||
);
|
||||
|
||||
if (!field || !editor || !editor.component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form gf-form-spacing">
|
||||
|
||||
@@ -59,7 +59,7 @@ export const FilterByValueTransformerEditor: React.FC<TransformerUIProps<FilterB
|
||||
},
|
||||
});
|
||||
onChange({ ...options, filters });
|
||||
}, [onChange, options, valueMatchers, input]);
|
||||
}, [onChange, options, input]);
|
||||
|
||||
const onDeleteFilter = useCallback(
|
||||
(index: number) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { convertToType } from './utils';
|
||||
export function basicMatcherEditor<T = any>(
|
||||
config: ValueMatcherEditorConfig
|
||||
): React.FC<ValueMatcherUIProps<BasicValueMatcherOptions<T>>> {
|
||||
return function render({ options, onChange, field }) {
|
||||
return function Render({ options, onChange, field }) {
|
||||
const { validator, converter = convertToType } = config;
|
||||
const { value } = options;
|
||||
const [isInvalid, setInvalid] = useState(!validator(value));
|
||||
|
||||
@@ -40,7 +40,9 @@ export const GroupByTransformerEditor: React.FC<TransformerUIProps<GroupByTransf
|
||||
},
|
||||
});
|
||||
},
|
||||
[options]
|
||||
// Adding options to the dependency array causes infinite loop here.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,7 +34,7 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
|
||||
},
|
||||
});
|
||||
},
|
||||
[onChange, excludeByName, indexByName]
|
||||
[onChange, options, excludeByName]
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
@@ -55,7 +55,7 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
|
||||
indexByName: reorderToIndex(fieldNames, startIndex, endIndex),
|
||||
});
|
||||
},
|
||||
[onChange, indexByName, excludeByName, fieldNames]
|
||||
[onChange, options, fieldNames]
|
||||
);
|
||||
|
||||
const onRenameField = useCallback(
|
||||
@@ -68,7 +68,7 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
|
||||
},
|
||||
});
|
||||
},
|
||||
[onChange, fieldNames, renameByName]
|
||||
[onChange, options]
|
||||
);
|
||||
|
||||
// Show warning that we only apply the first frame
|
||||
|
||||
@@ -24,7 +24,7 @@ export const SortByTransformerEditor: React.FC<TransformerUIProps<SortByTransfor
|
||||
(idx: number, cfg: SortByField) => {
|
||||
onChange({ ...options, sort: [cfg] });
|
||||
},
|
||||
[options]
|
||||
[onChange, options]
|
||||
);
|
||||
|
||||
const sorts = options.sort?.length ? options.sort : [{} as SortByField];
|
||||
|
||||
@@ -27,7 +27,7 @@ export const connectWithCleanUp = <
|
||||
return function cleanUp() {
|
||||
dispatch(cleanUpAction({ stateSelector }));
|
||||
};
|
||||
}, []);
|
||||
}, [dispatch]);
|
||||
// @ts-ignore
|
||||
return <ConnectedComponent {...props} />;
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ export const AdminEditOrgPage: FC<Props> = ({ match }) => {
|
||||
useEffect(() => {
|
||||
fetchOrg();
|
||||
fetchOrgUsers().then((res) => setUsers(res));
|
||||
}, []);
|
||||
}, [fetchOrg, fetchOrgUsers]);
|
||||
|
||||
const updateOrgName = async (name: string) => {
|
||||
return await getBackendSrv().put('/api/orgs/' + orgId, { ...orgState.value, name });
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AdminListOrgsPages: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrgs();
|
||||
}, []);
|
||||
}, [fetchOrgs]);
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
|
||||
@@ -24,10 +24,13 @@ const createUser = async (user: UserDTO) => getBackendSrv().post('/api/admin/use
|
||||
const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
|
||||
const history = useHistory();
|
||||
|
||||
const onSubmit = useCallback(async (data: UserDTO) => {
|
||||
await createUser(data);
|
||||
history.push('/admin/users');
|
||||
}, []);
|
||||
const onSubmit = useCallback(
|
||||
async (data: UserDTO) => {
|
||||
await createUser(data);
|
||||
history.push('/admin/users');
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
|
||||
@@ -31,13 +31,14 @@ type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
const UserListAdminPageUnConnected: React.FC<Props> = (props) => {
|
||||
const styles = getStyles();
|
||||
const { fetchUsers, navModel, query, changeQuery, users, showPaging, totalPages, page, changePage } = props;
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchUsers();
|
||||
}, []);
|
||||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
|
||||
return (
|
||||
<Page navModel={props.navModel}>
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<>
|
||||
<div>
|
||||
@@ -48,9 +49,9 @@ const UserListAdminPageUnConnected: React.FC<Props> = (props) => {
|
||||
placeholder="Search user by login, email or name"
|
||||
tabIndex={1}
|
||||
autoFocus={true}
|
||||
value={props.query}
|
||||
value={query}
|
||||
spellCheck={false}
|
||||
onChange={(event) => props.changeQuery(event.currentTarget.value)}
|
||||
onChange={(event) => changeQuery(event.currentTarget.value)}
|
||||
prefix={<Icon name="search" />}
|
||||
/>
|
||||
<LinkButton href="admin/users/create" variant="primary">
|
||||
@@ -77,12 +78,10 @@ const UserListAdminPageUnConnected: React.FC<Props> = (props) => {
|
||||
<th style={{ width: '1%' }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{props.users.map(renderUser)}</tbody>
|
||||
<tbody>{users.map(renderUser)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{props.showPaging && (
|
||||
<Pagination numberOfPages={props.totalPages} currentPage={props.page} onNavigate={props.changePage} />
|
||||
)}
|
||||
{showPaging && <Pagination numberOfPages={totalPages} currentPage={page} onNavigate={changePage} />}
|
||||
</>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
|
||||
@@ -23,7 +23,7 @@ const NotificationsListPage: FC = () => {
|
||||
fetchNotifications().then((res) => {
|
||||
setNotifications(res);
|
||||
});
|
||||
}, []);
|
||||
}, [fetchNotifications]);
|
||||
|
||||
const deleteNotification = (id: number) => {
|
||||
appEvents.publish(
|
||||
|
||||
@@ -31,7 +31,7 @@ interface Props {
|
||||
export const AlertingQueryPreview: FC<Props> = ({ getInstances, onRunQueries, onTest, queries, queryRunner }) => {
|
||||
const [activeTab, setActiveTab] = useState<string>(Tabs.Query);
|
||||
const styles = getStyles(config.theme);
|
||||
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []);
|
||||
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), [queryRunner]);
|
||||
const data = useObservable<PanelData>(observable);
|
||||
const instances = getInstances();
|
||||
|
||||
|
||||
@@ -38,22 +38,21 @@ export const NotificationChannelForm: FC<Props> = ({
|
||||
}) => {
|
||||
const styles = getStyles(useTheme());
|
||||
|
||||
/*
|
||||
Finds fields that have dependencies on other fields and removes duplicates.
|
||||
Needs to be prefixed with settings.
|
||||
*/
|
||||
const fieldsToWatch =
|
||||
new Set(
|
||||
selectedChannel?.options
|
||||
.filter((o) => o.showWhen.field)
|
||||
.map((option) => {
|
||||
return `settings.${option.showWhen.field}`;
|
||||
})
|
||||
) || [];
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Finds fields that have dependencies on other fields and removes duplicates.
|
||||
Needs to be prefixed with settings.
|
||||
*/
|
||||
const fieldsToWatch =
|
||||
new Set(
|
||||
selectedChannel?.options
|
||||
.filter((o) => o.showWhen.field)
|
||||
.map((option) => {
|
||||
return `settings.${option.showWhen.field}`;
|
||||
})
|
||||
) || [];
|
||||
watch(['type', 'sendReminder', 'uploadImage', ...fieldsToWatch]);
|
||||
}, [fieldsToWatch]);
|
||||
}, [selectedChannel?.options, watch]);
|
||||
|
||||
const currentFormValues = getValues();
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export const AutoRefreshIntervals: FC<Props> = ({
|
||||
useEffect(() => {
|
||||
const intervals = getIntervalsFunc(refreshIntervals ?? defaultIntervals);
|
||||
setIntervals(intervals);
|
||||
}, [refreshIntervals]);
|
||||
}, [getIntervalsFunc, refreshIntervals]);
|
||||
|
||||
const intervalsString = useMemo(() => {
|
||||
if (!Array.isArray(intervals)) {
|
||||
@@ -52,7 +52,7 @@ export const AutoRefreshIntervals: FC<Props> = ({
|
||||
|
||||
setInvalidIntervalsMessage(invalidMessage);
|
||||
},
|
||||
[intervals, onRefreshIntervalChange, setInvalidIntervalsMessage]
|
||||
[getIntervalsFunc, intervals, onRefreshIntervalChange, validateIntervalsFunc]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,6 @@ import { connect, MapStateToProps } from 'react-redux';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
|
||||
import { StoreState } from 'app/types';
|
||||
import { GetDataOptions } from '../../../query/state/PanelQueryRunner';
|
||||
import { usePanelLatestData } from '../PanelEditor/usePanelLatestData';
|
||||
@@ -24,10 +23,6 @@ export interface ConnectedProps {
|
||||
export type Props = OwnProps & ConnectedProps;
|
||||
|
||||
const PanelInspectorUnconnected: React.FC<Props> = ({ panel, dashboard, plugin }) => {
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [dataOptions, setDataOptions] = useState<GetDataOptions>({
|
||||
withTransforms: false,
|
||||
withFieldConfig: true,
|
||||
@@ -36,7 +31,7 @@ const PanelInspectorUnconnected: React.FC<Props> = ({ panel, dashboard, plugin }
|
||||
const location = useLocation();
|
||||
const { data, isLoading, error } = usePanelLatestData(panel, dataOptions);
|
||||
const metaDs = useDatasourceMetadata(data);
|
||||
const tabs = useInspectTabs(plugin, dashboard, error, metaDs);
|
||||
const tabs = useInspectTabs(dashboard, plugin, error, metaDs);
|
||||
const defaultTab = new URLSearchParams(location.search).get('inspectTab') as InspectTab;
|
||||
|
||||
const onClose = () => {
|
||||
@@ -46,6 +41,10 @@ const PanelInspectorUnconnected: React.FC<Props> = ({ panel, dashboard, plugin }
|
||||
});
|
||||
};
|
||||
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InspectContent
|
||||
dashboard={dashboard}
|
||||
|
||||
@@ -34,8 +34,8 @@ export const useDatasourceMetadata = (data?: PanelData) => {
|
||||
* Configures tabs for PanelInspector
|
||||
*/
|
||||
export const useInspectTabs = (
|
||||
plugin: PanelPlugin,
|
||||
dashboard: DashboardModel,
|
||||
plugin: PanelPlugin | undefined | null,
|
||||
error?: DataQueryError,
|
||||
metaDs?: DataSourceApi
|
||||
) => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const OptionsPaneCategory: FC<OptionsPaneCategoryProps> = React.memo(
|
||||
if (!isExpanded && forceOpen && forceOpen > 0) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [forceOpen]);
|
||||
}, [forceOpen, isExpanded]);
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
setSavedState({ isExpanded: !isExpanded });
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import React from 'react';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import createMockStore from 'redux-mock-store';
|
||||
import { PanelNotSupported, Props } from './PanelNotSupported';
|
||||
import { PanelEditorTabId } from './types';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
|
||||
const setupTestContext = (options: Partial<Props>) => {
|
||||
const defaults: Props = {
|
||||
message: '',
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
const store = createMockStore();
|
||||
|
||||
const props = { ...defaults, ...options };
|
||||
render(<PanelNotSupported {...props} />);
|
||||
render(
|
||||
<Provider store={store()}>
|
||||
<PanelNotSupported {...props} />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
return { props };
|
||||
};
|
||||
|
||||
@@ -13,10 +13,11 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const PanelNotSupported: FC<Props> = ({ message, dispatch: propsDispatch }) => {
|
||||
const dispatch = propsDispatch ? propsDispatch : useDispatch();
|
||||
let dispatch = useDispatch();
|
||||
dispatch = propsDispatch ?? dispatch;
|
||||
const onBackToQueries = useCallback(() => {
|
||||
locationService.partial({ tab: PanelEditorTabId.Query });
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout justify="center" style={{ marginTop: '100px' }}>
|
||||
|
||||
@@ -25,13 +25,16 @@ export const VisualizationSelectPane: FC<Props> = ({ panel }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const searchRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const onPluginTypeChange = (meta: PanelPluginMeta) => {
|
||||
if (meta.id === plugin.meta.id) {
|
||||
dispatch(toggleVizPicker(false));
|
||||
} else {
|
||||
dispatch(changePanelPlugin(panel, meta.id));
|
||||
}
|
||||
};
|
||||
const onPluginTypeChange = useCallback(
|
||||
(meta: PanelPluginMeta) => {
|
||||
if (meta.id === plugin.meta.id) {
|
||||
dispatch(toggleVizPicker(false));
|
||||
} else {
|
||||
dispatch(changePanelPlugin(panel, meta.id));
|
||||
}
|
||||
},
|
||||
[dispatch, panel, plugin.meta.id]
|
||||
);
|
||||
|
||||
// Give Search input focus when using radio button switch list mode
|
||||
useEffect(() => {
|
||||
@@ -55,7 +58,7 @@ export const VisualizationSelectPane: FC<Props> = ({ panel }) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[onPluginTypeChange]
|
||||
[onPluginTypeChange, plugin.meta]
|
||||
);
|
||||
|
||||
const suffix =
|
||||
|
||||
@@ -35,6 +35,7 @@ export const usePanelLatestData = (panel: PanelModel, options: GetDataOptions):
|
||||
* Adding separate options to dependencies array to avoid additional hook for comparing previous options with current.
|
||||
* Otherwise, passing different references to the same object may cause troubles.
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [panel, options.withFieldConfig, options.withTransforms]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -27,7 +27,7 @@ export function calculatePanelSize(mode: DisplayMode, width: number, height: num
|
||||
};
|
||||
}
|
||||
|
||||
export function supportsDataQuery(plugin: PanelPlugin | undefined): boolean {
|
||||
export function supportsDataQuery(plugin: PanelPlugin | undefined | null): boolean {
|
||||
return plugin?.meta.skipDataQuery === false;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const RepeatRowSelect: FC<Props> = ({ repeat, onChange }) => {
|
||||
});
|
||||
|
||||
return options;
|
||||
}, variables);
|
||||
}, [variables]);
|
||||
|
||||
const onSelectChange = useCallback((option: SelectableValue<string | null>) => onChange(option.value), [onChange]);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export const SaveDashboardErrorProxy: React.FC<SaveDashboardErrorProxyProps> = (
|
||||
if (error.data && isHandledError(error.data.status)) {
|
||||
error.isHandled = true;
|
||||
}
|
||||
}, []);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
|
||||
type: 'application/json;charset=utf-8',
|
||||
});
|
||||
saveAs(blob, dashboard.title + '-' + new Date().getTime() + '.json');
|
||||
}, [dashboardJSON]);
|
||||
}, [dashboard.title, dashboardJSON]);
|
||||
|
||||
const onCopyToClipboardSuccess = useCallback(() => {
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Dashboard JSON copied to clipboard']);
|
||||
|
||||
@@ -39,7 +39,7 @@ export const useDashboardSave = (dashboard: DashboardModel) => {
|
||||
locationService.replace(newUrl);
|
||||
}
|
||||
}
|
||||
}, [state]);
|
||||
}, [dashboard, state]);
|
||||
|
||||
return { state, onDashboardSave };
|
||||
};
|
||||
|
||||
@@ -17,10 +17,6 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const DashboardLinks: FC<Props> = ({ dashboard, links }) => {
|
||||
if (!links.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Emulate forceUpdate (https://reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate)
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
@@ -39,6 +35,10 @@ export const DashboardLinks: FC<Props> = ({ dashboard, links }) => {
|
||||
};
|
||||
});
|
||||
|
||||
if (!links.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{links.map((link: DashboardLink, index: number) => {
|
||||
|
||||
@@ -61,10 +61,11 @@ export const TransformationEditor = ({
|
||||
[
|
||||
uiConfig.editor,
|
||||
uiConfig.transformation.defaultOptions,
|
||||
config.transformation.id,
|
||||
config.transformation.options,
|
||||
config.transformation.id,
|
||||
input,
|
||||
onChange,
|
||||
index,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ export const useDashboardRestore = (version: number) => {
|
||||
});
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Dashboard restored', 'Restored from version ' + version]);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
}, [state, version]);
|
||||
return { state, onRestoreDashboard };
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ export const VizTypePicker: React.FC<Props> = ({ searchQuery, onTypeChange, curr
|
||||
|
||||
const getFilteredPluginList = useCallback((): PanelPluginMeta[] => {
|
||||
return filterPluginList(pluginsList, searchQuery, current);
|
||||
}, [searchQuery]);
|
||||
}, [current, pluginsList, searchQuery]);
|
||||
|
||||
const renderVizPlugin = (plugin: PanelPluginMeta, index: number) => {
|
||||
const isCurrent = plugin.id === current.id;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { uniqBy, debounce } from 'lodash';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
// Types
|
||||
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
import { sortOrderOptions } from './RichHistory';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
export interface Props {
|
||||
queries: RichHistoryQuery[];
|
||||
@@ -139,6 +140,7 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
const [timeFilter, setTimeFilter] = useState<[number, number]>([0, retentionPeriod]);
|
||||
const [filteredQueries, setFilteredQueries] = useState<RichHistoryQuery[]>([]);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, height);
|
||||
@@ -146,19 +148,12 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
const datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map((d) => d.datasourceName);
|
||||
const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
|
||||
|
||||
const filterAndSortQueriesDebounced = useCallback(
|
||||
debounce((searchValue: string) => {
|
||||
setFilteredQueries(
|
||||
filterAndSortQueries(
|
||||
queries,
|
||||
sortOrder,
|
||||
datasourceFilters?.map((d) => d.value) as string[] | null,
|
||||
searchValue,
|
||||
timeFilter
|
||||
)
|
||||
);
|
||||
}, 300),
|
||||
[timeFilter, queries, sortOrder, datasourceFilters]
|
||||
useDebounce(
|
||||
() => {
|
||||
setDebouncedSearchInput(searchInput);
|
||||
},
|
||||
300,
|
||||
[searchInput]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -167,11 +162,11 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
queries,
|
||||
sortOrder,
|
||||
datasourceFilters?.map((d) => d.value) as string[] | null,
|
||||
searchInput,
|
||||
debouncedSearchInput,
|
||||
timeFilter
|
||||
)
|
||||
);
|
||||
}, [timeFilter, queries, sortOrder, datasourceFilters]);
|
||||
}, [timeFilter, queries, sortOrder, datasourceFilters, debouncedSearchInput]);
|
||||
|
||||
/* mappedQueriesToHeadings is an object where query headings (stringified dates/data sources)
|
||||
* are keys and arrays with queries that belong to that headings are values.
|
||||
@@ -219,7 +214,6 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
value={searchInput}
|
||||
onChange={(value: string) => {
|
||||
setSearchInput(value);
|
||||
filterAndSortQueriesDebounced(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { uniqBy, debounce } from 'lodash';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
// Types
|
||||
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
@@ -14,6 +14,7 @@ import { filterAndSortQueries, createDatasourcesList, SortOrder } from 'app/core
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
import { sortOrderOptions } from './RichHistory';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
export interface Props {
|
||||
queries: RichHistoryQuery[];
|
||||
@@ -82,38 +83,33 @@ export function RichHistoryStarredTab(props: Props) {
|
||||
|
||||
const [filteredQueries, setFilteredQueries] = useState<RichHistoryQuery[]>([]);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map((d) => d.datasourceName);
|
||||
const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
|
||||
const starredQueries = queries.filter((q) => q.starred === true);
|
||||
|
||||
const filterAndSortQueriesDebounced = useCallback(
|
||||
debounce((searchValue: string) => {
|
||||
setFilteredQueries(
|
||||
filterAndSortQueries(
|
||||
starredQueries,
|
||||
sortOrder,
|
||||
datasourceFilters?.map((d) => d.value) as string[] | null,
|
||||
searchValue
|
||||
)
|
||||
);
|
||||
}, 300),
|
||||
[queries, sortOrder, datasourceFilters]
|
||||
useDebounce(
|
||||
() => {
|
||||
setDebouncedSearchInput(searchInput);
|
||||
},
|
||||
300,
|
||||
[searchInput]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const starredQueries = queries.filter((q) => q.starred === true);
|
||||
setFilteredQueries(
|
||||
filterAndSortQueries(
|
||||
starredQueries,
|
||||
sortOrder,
|
||||
datasourceFilters?.map((d) => d.value) as string[] | null,
|
||||
searchInput
|
||||
debouncedSearchInput
|
||||
)
|
||||
);
|
||||
}, [queries, sortOrder, datasourceFilters]);
|
||||
}, [queries, sortOrder, datasourceFilters, debouncedSearchInput]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -136,7 +132,6 @@ export function RichHistoryStarredTab(props: Props) {
|
||||
value={searchInput}
|
||||
onChange={(value: string) => {
|
||||
setSearchInput(value);
|
||||
filterAndSortQueriesDebounced(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { DataFrame, DataFrameView, TraceSpanRow } from '@grafana/data';
|
||||
import { colors, useTheme } from '@grafana/ui';
|
||||
import {
|
||||
ThemeOptions,
|
||||
ThemeProvider,
|
||||
ThemeType,
|
||||
Trace,
|
||||
TraceKeyValuePair,
|
||||
TraceLink,
|
||||
TracePageHeader,
|
||||
TraceProcess,
|
||||
TraceResponse,
|
||||
TraceSpan,
|
||||
TraceTimelineViewer,
|
||||
transformTraceData,
|
||||
TTraceTimeline,
|
||||
UIElementsContext,
|
||||
} from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsData } from 'app/core/components/TraceToLogsSettings';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { StoreState } from 'app/types';
|
||||
import { ExploreId, SplitOpen } from 'app/types/explore';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
import { UIElements } from './uiElements';
|
||||
import { useViewRange } from './useViewRange';
|
||||
import { useSearch } from './useSearch';
|
||||
import { useChildrenState } from './useChildrenState';
|
||||
import { useDetailState } from './useDetailState';
|
||||
import { useHoverIndentGuide } from './useHoverIndentGuide';
|
||||
import { colors, useTheme } from '@grafana/ui';
|
||||
import { DataFrame, DataFrameView, TraceSpanRow } from '@grafana/data';
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StoreState } from 'app/types';
|
||||
import { ExploreId, SplitOpen } from 'app/types/explore';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { TraceToLogsData } from 'app/core/components/TraceToLogsSettings';
|
||||
import { useSearch } from './useSearch';
|
||||
import { useViewRange } from './useViewRange';
|
||||
|
||||
function noop(): {} {
|
||||
return {};
|
||||
}
|
||||
|
||||
type Props = {
|
||||
dataFrames: DataFrame[];
|
||||
@@ -37,9 +38,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export function TraceView(props: Props) {
|
||||
if (!props.dataFrames.length) {
|
||||
return null;
|
||||
}
|
||||
const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
|
||||
const {
|
||||
detailStates,
|
||||
@@ -102,8 +100,9 @@ export function TraceView(props: Props) {
|
||||
traceToLogsOptions,
|
||||
]);
|
||||
const scrollElement = document.getElementsByClassName('scrollbar-view')[0];
|
||||
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
|
||||
|
||||
if (!traceProp) {
|
||||
if (!props.dataFrames?.length || !traceProp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -112,14 +111,14 @@ export function TraceView(props: Props) {
|
||||
<UIElementsContext.Provider value={UIElements}>
|
||||
<TracePageHeader
|
||||
canCollapse={false}
|
||||
clearSearch={useCallback(() => {}, [])}
|
||||
focusUiFindMatches={useCallback(() => {}, [])}
|
||||
clearSearch={noop}
|
||||
focusUiFindMatches={noop}
|
||||
hideMap={false}
|
||||
hideSummary={false}
|
||||
nextResult={useCallback(() => {}, [])}
|
||||
onSlimViewClicked={useCallback(() => setSlim(!slim), [])}
|
||||
onTraceGraphViewClicked={useCallback(() => {}, [])}
|
||||
prevResult={useCallback(() => {}, [])}
|
||||
nextResult={noop}
|
||||
onSlimViewClicked={onSlimViewClicked}
|
||||
onTraceGraphViewClicked={noop}
|
||||
prevResult={noop}
|
||||
resultCount={0}
|
||||
slimView={slim}
|
||||
textFilter={null}
|
||||
@@ -133,23 +132,23 @@ export function TraceView(props: Props) {
|
||||
hideSearchButtons={true}
|
||||
/>
|
||||
<TraceTimelineViewer
|
||||
registerAccessors={useCallback(() => {}, [])}
|
||||
scrollToFirstVisibleSpan={useCallback(() => {}, [])}
|
||||
registerAccessors={noop}
|
||||
scrollToFirstVisibleSpan={noop}
|
||||
findMatchesIDs={spanFindMatches}
|
||||
trace={traceProp}
|
||||
traceTimeline={traceTimeline}
|
||||
updateNextViewRangeTime={updateNextViewRangeTime}
|
||||
updateViewRangeTime={updateViewRangeTime}
|
||||
viewRange={viewRange}
|
||||
focusSpan={useCallback(() => {}, [])}
|
||||
createLinkToExternalSpan={useCallback(() => '', [])}
|
||||
focusSpan={noop}
|
||||
createLinkToExternalSpan={noop as any}
|
||||
setSpanNameColumnWidth={setSpanNameColumnWidth}
|
||||
collapseAll={collapseAll}
|
||||
collapseOne={collapseOne}
|
||||
expandAll={expandAll}
|
||||
expandOne={expandOne}
|
||||
childrenToggle={childrenToggle}
|
||||
clearShouldScrollToFirstUiFindMatch={useCallback(() => {}, [])}
|
||||
clearShouldScrollToFirstUiFindMatch={noop}
|
||||
detailLogItemToggle={detailLogItemToggle}
|
||||
detailLogsToggle={detailLogsToggle}
|
||||
detailWarningsToggle={detailWarningsToggle}
|
||||
@@ -158,13 +157,10 @@ export function TraceView(props: Props) {
|
||||
detailProcessToggle={detailProcessToggle}
|
||||
detailTagsToggle={detailTagsToggle}
|
||||
detailToggle={toggleDetail}
|
||||
setTrace={useCallback((trace: Trace | null, uiFind: string | null) => {}, [])}
|
||||
setTrace={noop}
|
||||
addHoverIndentGuideId={addHoverIndentGuideId}
|
||||
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
||||
linksGetter={useCallback(
|
||||
(span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => [] as TraceLink[],
|
||||
[]
|
||||
)}
|
||||
linksGetter={noop as any}
|
||||
uiFind={search}
|
||||
createSpanLink={createSpanLink}
|
||||
scrollElement={scrollElement}
|
||||
@@ -177,6 +173,9 @@ export function TraceView(props: Props) {
|
||||
function transformDataFrames(frames: DataFrame[]): Trace | null {
|
||||
// At this point we only show single trace.
|
||||
const frame = frames[0];
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
let data: TraceResponse =
|
||||
frame.fields.length === 1
|
||||
? // For backward compatibility when we sent whole json response in a single field/value
|
||||
|
||||
@@ -40,20 +40,30 @@ export function useDetailState() {
|
||||
detailStates,
|
||||
toggleDetail,
|
||||
detailLogItemToggle,
|
||||
detailLogsToggle: useCallback(makeDetailSubsectionToggle('logs', detailStates, setDetailStates), [detailStates]),
|
||||
detailWarningsToggle: useCallback(makeDetailSubsectionToggle('warnings', detailStates, setDetailStates), [
|
||||
detailStates,
|
||||
]),
|
||||
detailStackTracesToggle: useCallback(makeDetailSubsectionToggle('stackTraces', detailStates, setDetailStates), [
|
||||
detailStates,
|
||||
]),
|
||||
detailReferencesToggle: useCallback(makeDetailSubsectionToggle('references', detailStates, setDetailStates), [
|
||||
detailStates,
|
||||
]),
|
||||
detailProcessToggle: useCallback(makeDetailSubsectionToggle('process', detailStates, setDetailStates), [
|
||||
detailStates,
|
||||
]),
|
||||
detailTagsToggle: useCallback(makeDetailSubsectionToggle('tags', detailStates, setDetailStates), [detailStates]),
|
||||
detailLogsToggle: useCallback(
|
||||
(spanID: string) => makeDetailSubsectionToggle('logs', detailStates, setDetailStates)(spanID),
|
||||
[detailStates]
|
||||
),
|
||||
detailWarningsToggle: useCallback(
|
||||
(spanID: string) => makeDetailSubsectionToggle('warnings', detailStates, setDetailStates)(spanID),
|
||||
[detailStates]
|
||||
),
|
||||
detailStackTracesToggle: useCallback(
|
||||
(spanID: string) => makeDetailSubsectionToggle('stackTraces', detailStates, setDetailStates)(spanID),
|
||||
[detailStates]
|
||||
),
|
||||
detailReferencesToggle: useCallback(
|
||||
(spanID: string) => makeDetailSubsectionToggle('references', detailStates, setDetailStates)(spanID),
|
||||
[detailStates]
|
||||
),
|
||||
detailProcessToggle: useCallback(
|
||||
(spanID: string) => makeDetailSubsectionToggle('process', detailStates, setDetailStates)(spanID),
|
||||
[detailStates]
|
||||
),
|
||||
detailTagsToggle: useCallback(
|
||||
(spanID: string) => makeDetailSubsectionToggle('tags', detailStates, setDetailStates)(spanID),
|
||||
[detailStates]
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const DeleteLibraryPanelModal: FC<Props> = ({ libraryPanel, onDismiss, on
|
||||
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
|
||||
useEffect(() => {
|
||||
asyncDispatch(getConnectedDashboards(libraryPanel));
|
||||
}, []);
|
||||
}, [asyncDispatch, libraryPanel]);
|
||||
const connected = Boolean(dashboardTitles.length);
|
||||
const done = loadingState === LoadingState.Done;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const SaveLibraryPanelModal: React.FC<Props> = ({
|
||||
const discardAndClose = useCallback(() => {
|
||||
onDiscard();
|
||||
onDismiss();
|
||||
}, []);
|
||||
}, [onDiscard, onDismiss]);
|
||||
|
||||
return (
|
||||
<Modal title="Update all panel instances" icon="save" onDismiss={onDismiss} isOpen={isOpen}>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const usePanelSave = () => {
|
||||
if (state.value) {
|
||||
dispatch(notifyApp(createPanelLibrarySuccessNotification('Library panel saved')));
|
||||
}
|
||||
}, [state]);
|
||||
}, [dispatch, state]);
|
||||
|
||||
return { state, saveLibraryPanel };
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
||||
if (isSubmitted && (errors.title || errors.uid)) {
|
||||
onSubmit(getValues({ nest: true }), {} as any);
|
||||
}
|
||||
}, [errors]);
|
||||
}, [errors, getValues, isSubmitted, onSubmit]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -111,5 +111,5 @@ const useFieldOverrides = (
|
||||
timeZone,
|
||||
}),
|
||||
};
|
||||
}, [fieldConfigRegistry, timeZone, fieldConfig, series]);
|
||||
}, [fieldConfigRegistry, fieldConfig, data, series, timeZone]);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export function usePlaylist(playlistId?: number) {
|
||||
setLoading(false);
|
||||
};
|
||||
initPlaylist();
|
||||
}, []);
|
||||
}, [playlistId]);
|
||||
|
||||
return { playlist, loading };
|
||||
}
|
||||
|
||||
@@ -61,14 +61,14 @@ export function usePlaylistItems(playlistItems?: PlaylistItem[]) {
|
||||
(item: PlaylistItem) => {
|
||||
movePlaylistItem(item, -1);
|
||||
},
|
||||
[items]
|
||||
[movePlaylistItem]
|
||||
);
|
||||
|
||||
const moveDown = useCallback(
|
||||
(item: PlaylistItem) => {
|
||||
movePlaylistItem(item, 1);
|
||||
},
|
||||
[items]
|
||||
[movePlaylistItem]
|
||||
);
|
||||
|
||||
const deleteItem = useCallback(
|
||||
|
||||
@@ -37,7 +37,7 @@ export const TestStuffPage: FC = () => {
|
||||
/**
|
||||
* Subscribe to data
|
||||
*/
|
||||
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []);
|
||||
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), [queryRunner]);
|
||||
const data = useObservable(observable);
|
||||
|
||||
return (
|
||||
|
||||
@@ -27,9 +27,12 @@ const getIconFromMeta = (meta = ''): IconName => {
|
||||
|
||||
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const tagSelected = useCallback((tag: string, event: React.MouseEvent<HTMLElement>) => {
|
||||
onTagSelected(tag);
|
||||
}, []);
|
||||
const tagSelected = useCallback(
|
||||
(tag: string, event: React.MouseEvent<HTMLElement>) => {
|
||||
onTagSelected(tag);
|
||||
},
|
||||
[onTagSelected]
|
||||
);
|
||||
|
||||
const toggleItem = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
@@ -38,7 +41,7 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
|
||||
onToggleChecked(item);
|
||||
}
|
||||
},
|
||||
[item]
|
||||
[item, onToggleChecked]
|
||||
);
|
||||
|
||||
const folderTitle = item.folderTitle || 'General';
|
||||
|
||||
@@ -37,7 +37,7 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
|
||||
onToggleChecked(section);
|
||||
}
|
||||
},
|
||||
[section]
|
||||
[onToggleChecked, section]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,7 @@ export const useSearch: UseSearch = (query, reducer, params = {}) => {
|
||||
// Set loading state before debounced search
|
||||
useEffect(() => {
|
||||
dispatch({ type: SEARCH_START });
|
||||
}, [query.tag, query.sort, query.starred, query.layout]);
|
||||
}, [query.tag, query.sort, query.starred, query.layout, dispatch]);
|
||||
|
||||
useDebounce(search, 300, [query, queryParsing]);
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export const AdHocFilterBuilder: FC<Props> = ({ datasource, appendBefore, onComp
|
||||
setKey(null);
|
||||
setOperator('=');
|
||||
},
|
||||
[onCompleted, key, setKey, setOperator]
|
||||
[onCompleted, operator, key]
|
||||
);
|
||||
|
||||
if (key === null) {
|
||||
|
||||
@@ -12,12 +12,9 @@ export const LEGACY_VARIABLE_QUERY_EDITOR_NAME = 'Grafana-LegacyVariableQueryEdi
|
||||
export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, query }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const [value, setValue] = useState(query);
|
||||
const onValueChange = useCallback(
|
||||
(event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
setValue(event.currentTarget.value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const onValueChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
setValue(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const onBlur = useCallback(
|
||||
(event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
@@ -14,33 +14,38 @@ export interface SelectionOptionsEditorProps<Model extends VariableWithMultiSupp
|
||||
onMultiChanged: (identifier: VariableIdentifier, value: boolean) => void;
|
||||
}
|
||||
|
||||
export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorProps> = (props) => {
|
||||
export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorProps> = ({
|
||||
onMultiChanged: onMultiChangedProps,
|
||||
onPropChange,
|
||||
variable,
|
||||
}) => {
|
||||
const onMultiChanged = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
props.onMultiChanged(toVariableIdentifier(props.variable), event.target.checked);
|
||||
onMultiChangedProps(toVariableIdentifier(variable), event.target.checked);
|
||||
},
|
||||
[props.onMultiChanged, props.variable]
|
||||
[onMultiChangedProps, variable]
|
||||
);
|
||||
|
||||
const onIncludeAllChanged = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
props.onPropChange({ propName: 'includeAll', propValue: event.target.checked });
|
||||
onPropChange({ propName: 'includeAll', propValue: event.target.checked });
|
||||
},
|
||||
[props.onPropChange]
|
||||
[onPropChange]
|
||||
);
|
||||
|
||||
const onAllValueChanged = useCallback(
|
||||
(event: FormEvent<HTMLInputElement>) => {
|
||||
props.onPropChange({ propName: 'allValue', propValue: event.currentTarget.value });
|
||||
onPropChange({ propName: 'allValue', propValue: event.currentTarget.value });
|
||||
},
|
||||
[props.onPropChange]
|
||||
[onPropChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="none">
|
||||
<VariableSectionHeader name="Selection Options" />
|
||||
<InlineFieldRow>
|
||||
<VariableSwitchField
|
||||
value={props.variable.multi}
|
||||
value={variable.multi}
|
||||
name="Multi-value"
|
||||
tooltip="Enables multiple values to be selected at the same time"
|
||||
onChange={onMultiChanged}
|
||||
@@ -49,17 +54,17 @@ export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorPro
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<VariableSwitchField
|
||||
value={props.variable.includeAll}
|
||||
value={variable.includeAll}
|
||||
name="Include All option"
|
||||
tooltip="Enables an option to include all variables"
|
||||
onChange={onIncludeAllChanged}
|
||||
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
{props.variable.includeAll && (
|
||||
{variable.includeAll && (
|
||||
<InlineFieldRow>
|
||||
<VariableTextField
|
||||
value={props.variable.allValue ?? ''}
|
||||
value={variable.allValue ?? ''}
|
||||
onChange={onAllValueChanged}
|
||||
name="Custom all value"
|
||||
placeholder="blank = auto"
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function VariableTypeSelect({ onChange, type }: PropsWithChildren<Props>) {
|
||||
const options = useMemo(() => getVariableTypes(), [getVariableTypes]);
|
||||
const options = useMemo(() => getVariableTypes(), []);
|
||||
const value = useMemo(() => options.find((o) => o.value === type) ?? options[0], [options, type]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,7 @@ interface DispatchProps {}
|
||||
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
export const NetworkGraph: FC<Props> = ({ nodes, edges, direction, width, height, onDoubleClick }) => {
|
||||
let network: any = null;
|
||||
const network = useRef<any>(null);
|
||||
const ref = useRef(null);
|
||||
|
||||
const onNodeDoubleClick = useCallback(
|
||||
@@ -55,16 +55,16 @@ export const NetworkGraph: FC<Props> = ({ nodes, edges, direction, width, height
|
||||
},
|
||||
};
|
||||
|
||||
network = new vis.Network(ref.current, data, options);
|
||||
network.on('doubleClick', onNodeDoubleClick);
|
||||
network.current = new vis.Network(ref.current, data, options);
|
||||
network.current?.on('doubleClick', onNodeDoubleClick);
|
||||
|
||||
return () => {
|
||||
// unsubscribe event handlers
|
||||
if (network) {
|
||||
network.off('doubleClick');
|
||||
if (network.current) {
|
||||
network.current.off('doubleClick');
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [direction, edges, nodes, onNodeDoubleClick]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export function TextBoxVariablePicker({ variable, onVariableChange }: Props): Re
|
||||
}
|
||||
|
||||
variableAdapters.get(variable.type).updateOptions(variable);
|
||||
}, [dispatch, variable, updatedValue]);
|
||||
}, [variable, updatedValue, dispatch, onVariableChange]);
|
||||
|
||||
const onChange = useCallback((event: ChangeEvent<HTMLInputElement>) => setUpdatedValue(event.target.value), [
|
||||
setUpdatedValue,
|
||||
|
||||
@@ -57,19 +57,19 @@ export const Aggregations: FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => {
|
||||
const valueType = metricDescriptor?.valueType;
|
||||
const metricKind = metricDescriptor?.metricKind;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!metricDescriptor) {
|
||||
if (!valueType || !metricKind) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getAggregationOptionsByMetric(
|
||||
metricDescriptor.valueType as ValueTypes,
|
||||
metricDescriptor.metricKind as MetricKind
|
||||
).map((a) => ({
|
||||
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({
|
||||
...a,
|
||||
label: a.text,
|
||||
}));
|
||||
}, [metricDescriptor?.metricKind, metricDescriptor?.valueType]);
|
||||
}, [valueType, metricKind]);
|
||||
};
|
||||
|
||||
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => {
|
||||
|
||||
@@ -51,14 +51,15 @@ function Editor({
|
||||
variableOptionGroup,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const [state, setState] = useState<State>(defaultState);
|
||||
const { projectName, metricType, groupBys, editorMode } = query;
|
||||
|
||||
useEffect(() => {
|
||||
if (query && query.projectName && query.metricType) {
|
||||
if (projectName && metricType) {
|
||||
datasource
|
||||
.getLabels(query.metricType, refId, query.projectName, query.groupBys)
|
||||
.then((labels) => setState({ ...state, labels }));
|
||||
.getLabels(metricType, refId, projectName, groupBys)
|
||||
.then((labels) => setState((prevState) => ({ ...prevState, labels })));
|
||||
}
|
||||
}, [query.projectName, query.groupBys, query.metricType]);
|
||||
}, [datasource, groupBys, metricType, projectName, refId]);
|
||||
|
||||
const onChange = (metricQuery: MetricQuery) => {
|
||||
onQueryChange({ ...query, ...metricQuery });
|
||||
@@ -81,14 +82,14 @@ function Editor({
|
||||
<>
|
||||
<Project
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
projectName={query.projectName}
|
||||
projectName={projectName}
|
||||
datasource={datasource}
|
||||
onChange={(projectName) => {
|
||||
onChange({ ...query, projectName });
|
||||
}}
|
||||
/>
|
||||
|
||||
{query.editorMode === EditorMode.Visual && (
|
||||
{editorMode === EditorMode.Visual && (
|
||||
<VisualMetricQueryEditor
|
||||
labels={state.labels}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
@@ -100,7 +101,7 @@ function Editor({
|
||||
/>
|
||||
)}
|
||||
|
||||
{query.editorMode === EditorMode.MQL && (
|
||||
{editorMode === EditorMode.MQL && (
|
||||
<MQLQueryEditor
|
||||
onChange={(q: string) => onQueryChange({ ...query, query: q })}
|
||||
onRunQuery={onRunQuery}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
@@ -37,48 +37,54 @@ export function Metrics(props: Props) {
|
||||
projectName: null,
|
||||
});
|
||||
|
||||
const { services, service, metrics } = state;
|
||||
const { metricType, templateVariableOptions, projectName } = props;
|
||||
const { services, service, metrics, metricDescriptors } = state;
|
||||
const { metricType, templateVariableOptions, projectName, templateSrv, datasource, onChange, children } = props;
|
||||
|
||||
const loadMetricDescriptors = async () => {
|
||||
if (projectName) {
|
||||
const metricDescriptors = await props.datasource.getMetricTypes(props.projectName);
|
||||
const services = getServicesList(metricDescriptors);
|
||||
const metrics = getMetricsList(metricDescriptors);
|
||||
const service = metrics.length > 0 ? metrics[0].service : '';
|
||||
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, props.metricType);
|
||||
setState({ ...state, metricDescriptors, services, metrics, service: service, metricDescriptor });
|
||||
}
|
||||
};
|
||||
const getSelectedMetricDescriptor = useCallback(
|
||||
(metricDescriptors: MetricDescriptor[], metricType: string) => {
|
||||
return metricDescriptors.find((md) => md.type === templateSrv.replace(metricType))!;
|
||||
},
|
||||
[templateSrv]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const getMetricsList = (metricDescriptors: MetricDescriptor[]) => {
|
||||
const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
|
||||
if (!selectedMetricDescriptor) {
|
||||
return [];
|
||||
}
|
||||
const metricsByService = metricDescriptors
|
||||
.filter((m) => m.service === selectedMetricDescriptor.service)
|
||||
.map((m) => ({
|
||||
service: m.service,
|
||||
value: m.type,
|
||||
label: m.displayName,
|
||||
description: m.description,
|
||||
}));
|
||||
return metricsByService;
|
||||
};
|
||||
|
||||
const loadMetricDescriptors = async () => {
|
||||
if (projectName) {
|
||||
const metricDescriptors = await datasource.getMetricTypes(projectName);
|
||||
const services = getServicesList(metricDescriptors);
|
||||
const metrics = getMetricsList(metricDescriptors);
|
||||
const service = metrics.length > 0 ? metrics[0].service : '';
|
||||
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
metricDescriptors,
|
||||
services,
|
||||
metrics,
|
||||
service: service,
|
||||
metricDescriptor,
|
||||
}));
|
||||
}
|
||||
};
|
||||
loadMetricDescriptors();
|
||||
}, [projectName]);
|
||||
|
||||
const getSelectedMetricDescriptor = (metricDescriptors: MetricDescriptor[], metricType: string) => {
|
||||
return metricDescriptors.find((md) => md.type === props.templateSrv.replace(metricType))!;
|
||||
};
|
||||
|
||||
const getMetricsList = (metricDescriptors: MetricDescriptor[]) => {
|
||||
const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, props.metricType);
|
||||
if (!selectedMetricDescriptor) {
|
||||
return [];
|
||||
}
|
||||
const metricsByService = metricDescriptors
|
||||
.filter((m) => m.service === selectedMetricDescriptor.service)
|
||||
.map((m) => ({
|
||||
service: m.service,
|
||||
value: m.type,
|
||||
label: m.displayName,
|
||||
description: m.description,
|
||||
}));
|
||||
return metricsByService;
|
||||
};
|
||||
}, [datasource, getSelectedMetricDescriptor, metricType, projectName]);
|
||||
|
||||
const onServiceChange = ({ value: service }: any) => {
|
||||
const { metricDescriptors } = state;
|
||||
const { metricType, templateSrv } = props;
|
||||
|
||||
const metrics = metricDescriptors
|
||||
.filter((m: MetricDescriptor) => m.service === templateSrv.replace(service))
|
||||
.map((m: MetricDescriptor) => ({
|
||||
@@ -98,7 +104,7 @@ export function Metrics(props: Props) {
|
||||
const onMetricTypeChange = ({ value }: SelectableValue<string>, extra: any = {}) => {
|
||||
const metricDescriptor = getSelectedMetricDescriptor(state.metricDescriptors, value!);
|
||||
setState({ ...state, metricDescriptor, ...extra });
|
||||
props.onChange({ ...metricDescriptor, type: value! });
|
||||
onChange({ ...metricDescriptor, type: value! });
|
||||
};
|
||||
|
||||
const getServicesList = (metricDescriptors: MetricDescriptor[]) => {
|
||||
@@ -150,7 +156,7 @@ export function Metrics(props: Props) {
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
{props.children(state.metricDescriptor)}
|
||||
{children(state.metricDescriptor)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export type Props = DataSourcePluginOptionsEditorProps<CloudWatchJsonData, Cloud
|
||||
|
||||
export const ConfigEditor: FC<Props> = (props: Props) => {
|
||||
const [datasource, setDatasource] = useState<CloudWatchDatasource>();
|
||||
const { options } = props;
|
||||
|
||||
const addWarning = (message: string) => {
|
||||
store.dispatch(notifyApp(createWarningNotification('CloudWatch Authentication', message)));
|
||||
@@ -22,23 +23,19 @@ export const ConfigEditor: FC<Props> = (props: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
getDatasourceSrv()
|
||||
.loadDatasource(props.options.name)
|
||||
.loadDatasource(options.name)
|
||||
.then((datasource: CloudWatchDatasource) => setDatasource(datasource));
|
||||
|
||||
if (props.options.jsonData.authType === 'arn') {
|
||||
if (options.jsonData.authType === 'arn') {
|
||||
addWarning('Since grafana 7.3 authentication type "arn" is deprecated, falling back to default SDK provider');
|
||||
} else if (
|
||||
props.options.jsonData.authType === 'credentials' &&
|
||||
!props.options.jsonData.profile &&
|
||||
!props.options.jsonData.database
|
||||
) {
|
||||
} else if (options.jsonData.authType === 'credentials' && !options.jsonData.profile && !options.jsonData.database) {
|
||||
addWarning(
|
||||
'As of grafana 7.3 authentication type "credentials" should be used only for shared file credentials. \
|
||||
If you don\'t have a credentials file, switch to the default SDK provider for extracting credentials \
|
||||
from environment variables or IAM roles'
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
}, [options.jsonData.authType, options.jsonData.database, options.jsonData.profile, options.name]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -53,7 +50,7 @@ export const ConfigEditor: FC<Props> = (props: Props) => {
|
||||
<Input
|
||||
width={60}
|
||||
placeholder="Namespace1,Namespace2"
|
||||
value={props.options.jsonData.customMetricsNamespaces || ''}
|
||||
value={options.jsonData.customMetricsNamespaces || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'customMetricsNamespaces')}
|
||||
/>
|
||||
</InlineField>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const Dimensions: FunctionComponent<Props> = ({ dimensions, loadValues, l
|
||||
if (!isEqual(completeDimensions, dimensions)) {
|
||||
onChange(completeDimensions);
|
||||
}
|
||||
}, [data]);
|
||||
}, [data, dimensions, onChange]);
|
||||
|
||||
const excludeUsedKeys = (options: SelectableStrings) => {
|
||||
return options.filter(({ value }) => !Object.keys(data).includes(value!));
|
||||
|
||||
@@ -44,15 +44,15 @@ export function MetricsQueryFieldsEditor({
|
||||
|
||||
Promise.all([datasource.metricFindQuery('regions()'), datasource.metricFindQuery('namespaces()')]).then(
|
||||
([regions, namespaces]) => {
|
||||
setState({
|
||||
...state,
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
regions: [...regions, variableOptionGroup],
|
||||
namespaces: [...namespaces, variableOptionGroup],
|
||||
variableOptionGroup,
|
||||
});
|
||||
}));
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
}, [datasource]);
|
||||
|
||||
const loadMetricNames = async () => {
|
||||
const { namespace, region } = query;
|
||||
|
||||
@@ -28,7 +28,7 @@ export const FiltersSettingsEditor: FunctionComponent<Props> = ({ value }) => {
|
||||
if (!value.settings?.filters?.length) {
|
||||
dispatch(addFilter());
|
||||
}
|
||||
}, []);
|
||||
}, [dispatch, value.settings?.filters?.length]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const BucketScriptSettingsEditor: FunctionComponent<Props> = ({ value, pr
|
||||
if (!value.pipelineVariables?.length) {
|
||||
dispatch(addPipelineVariable());
|
||||
}
|
||||
}, []);
|
||||
}, [dispatch, value.pipelineVariables?.length]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -26,6 +26,8 @@ export const ConfigEditor = (props: Props) => {
|
||||
logLevelField: options.jsonData.logLevelField || '',
|
||||
},
|
||||
});
|
||||
// We can't enforce the eslint rule here because we only want to run this once.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,7 +30,7 @@ const AggregationField: React.FC<AggregationFieldProps> = ({
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...aggregationOptions, variableOptionGroup], [
|
||||
|
||||
@@ -10,15 +10,18 @@ interface DimensionFieldsProps extends AzureQueryEditorFieldProps {
|
||||
}
|
||||
|
||||
const DimensionFields: React.FC<DimensionFieldsProps> = ({ query, dimensionOptions, onQueryChange }) => {
|
||||
const setDimensionFilters = (newFilters: AzureMetricDimension[]) => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
dimensionFilters: newFilters,
|
||||
},
|
||||
});
|
||||
};
|
||||
const setDimensionFilters = useCallback(
|
||||
(newFilters: AzureMetricDimension[]) => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
dimensionFilters: newFilters,
|
||||
},
|
||||
});
|
||||
},
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
const addFilter = useCallback(() => {
|
||||
setDimensionFilters([
|
||||
@@ -29,7 +32,7 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ query, dimensionOptio
|
||||
filter: '',
|
||||
},
|
||||
]);
|
||||
}, [query.azureMonitor.dimensionFilters]);
|
||||
}, [query.azureMonitor.dimensionFilters, setDimensionFilters]);
|
||||
|
||||
const removeFilter = (index: number) => {
|
||||
const newFilters = [...query.azureMonitor.dimensionFilters];
|
||||
|
||||
@@ -23,7 +23,7 @@ const LegendFormatField: React.FC<AzureQueryEditorFieldProps> = ({ onQueryChange
|
||||
alias: value,
|
||||
},
|
||||
});
|
||||
}, [query, value]);
|
||||
}, [onQueryChange, query, value]);
|
||||
|
||||
return (
|
||||
<Field label="Legend Format">
|
||||
|
||||
@@ -31,13 +31,7 @@ const MetricName: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
setMetricNames(results.map(toOption));
|
||||
})
|
||||
.catch((err) => setError(ERROR_SOURCE, err));
|
||||
}, [
|
||||
subscriptionId,
|
||||
query.azureMonitor.resourceGroup,
|
||||
query.azureMonitor.metricDefinition,
|
||||
query.azureMonitor.resourceName,
|
||||
query.azureMonitor.metricNamespace,
|
||||
]);
|
||||
}, [datasource, metricNames.length, query.azureMonitor, setError, subscriptionId]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
@@ -53,7 +47,7 @@ const MetricName: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...metricNames, variableOptionGroup], [metricNames, variableOptionGroup]);
|
||||
|
||||
@@ -40,12 +40,7 @@ const MetricNamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
setMetricNamespaces(results.map(toOption));
|
||||
})
|
||||
.catch((err) => setError(ERROR_SOURCE, err));
|
||||
}, [
|
||||
subscriptionId,
|
||||
query.azureMonitor.resourceGroup,
|
||||
query.azureMonitor.metricDefinition,
|
||||
query.azureMonitor.resourceName,
|
||||
]);
|
||||
}, [datasource, metricNamespaces.length, onQueryChange, query, setError, subscriptionId]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
@@ -64,7 +59,7 @@ const MetricNamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...metricNamespaces, variableOptionGroup], [metricNamespaces, variableOptionGroup]);
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('Azure Monitor QueryEditor', () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const onChange = jest.fn();
|
||||
const mockQuery = createMockQuery();
|
||||
mockDatasource.getMetricNames = jest.fn().mockResolvedValueOnce([
|
||||
mockDatasource.getMetricNames = jest.fn().mockResolvedValue([
|
||||
{
|
||||
value: 'metric-a',
|
||||
text: 'Metric A',
|
||||
|
||||
@@ -29,7 +29,7 @@ const NamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
.getMetricDefinitions(subscriptionId, resourceGroup)
|
||||
.then((results) => setNamespaces(results.map(toOption)))
|
||||
.catch((err) => setError(ERROR_SOURCE, err));
|
||||
}, [subscriptionId, query.azureMonitor.resourceGroup]);
|
||||
}, [datasource, namespaces.length, query.azureMonitor, setError, subscriptionId]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
@@ -51,7 +51,7 @@ const NamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...namespaces, variableOptionGroup], [namespaces, variableOptionGroup]);
|
||||
|
||||
@@ -30,7 +30,7 @@ const ResourceGroupsField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
setError(ERROR_SOURCE, undefined);
|
||||
})
|
||||
.catch((err) => setError(ERROR_SOURCE, err));
|
||||
}, [subscriptionId]);
|
||||
}, [datasource, resourceGroups.length, setError, subscriptionId]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
@@ -53,7 +53,7 @@ const ResourceGroupsField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...resourceGroups, variableOptionGroup], [resourceGroups, variableOptionGroup]);
|
||||
|
||||
@@ -29,7 +29,7 @@ const ResourceNameField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
.getResourceNames(subscriptionId, resourceGroup, metricDefinition)
|
||||
.then((results) => setResourceNames(results.map(toOption)))
|
||||
.catch((err) => setError(ERROR_SOURCE, err));
|
||||
}, [subscriptionId, query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition]);
|
||||
}, [datasource, query.azureMonitor, resourceNames.length, setError, subscriptionId]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
@@ -51,7 +51,7 @@ const ResourceNameField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...resourceNames, variableOptionGroup], [resourceNames, variableOptionGroup]);
|
||||
|
||||
@@ -31,7 +31,7 @@ const TimeGrainField: React.FC<TimeGrainFieldProps> = ({
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
const timeGrains = useMemo(() => {
|
||||
|
||||
@@ -23,7 +23,7 @@ const TopField: React.FC<AzureQueryEditorFieldProps> = ({ onQueryChange, query }
|
||||
top: value,
|
||||
},
|
||||
});
|
||||
}, [query, value]);
|
||||
}, [onQueryChange, query, value]);
|
||||
|
||||
return (
|
||||
<Field label="Top">
|
||||
|
||||
@@ -26,7 +26,7 @@ const QueryTypeField: React.FC<QueryTypeFieldProps> = ({ query, onQueryChange })
|
||||
queryType: change.value,
|
||||
});
|
||||
},
|
||||
[query]
|
||||
[onQueryChange, query]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -54,7 +54,14 @@ const SubscriptionField: React.FC<SubscriptionFieldProps> = ({
|
||||
});
|
||||
})
|
||||
.catch((err) => setError(ERROR_SOURCE, err));
|
||||
}, []);
|
||||
}, [
|
||||
datasource.azureLogAnalyticsDatasource?.logAnalyticsSubscriptionId,
|
||||
datasource.azureLogAnalyticsDatasource?.subscriptionId,
|
||||
datasource.azureMonitorDatasource,
|
||||
onQueryChange,
|
||||
query,
|
||||
setError,
|
||||
]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
|
||||
@@ -79,6 +79,9 @@ export function useMetricsMetadata(
|
||||
query.azureMonitor.resourceName,
|
||||
query.azureMonitor.metricNamespace,
|
||||
query.azureMonitor.metricName,
|
||||
query,
|
||||
datasource,
|
||||
onQueryChange,
|
||||
]);
|
||||
|
||||
return metricMetadata;
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useShadowedState<T>(outsideVal: T): [T, (newVal: T) => void] {
|
||||
if (isOutsideValChanged && currentVal !== outsideVal) {
|
||||
setCurrentVal(outsideVal);
|
||||
}
|
||||
}, [outsideVal, currentVal]);
|
||||
}, [outsideVal, currentVal, prevOutsideVal]);
|
||||
|
||||
return [currentVal, setCurrentVal];
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const PromExploreQueryEditor: FC<Props> = (props: Props) => {
|
||||
if (query.exemplar === undefined) {
|
||||
onChange({ ...query, exemplar: true });
|
||||
}
|
||||
}, [query]);
|
||||
}, [onChange, query]);
|
||||
|
||||
function onChangeQueryStep(value: string) {
|
||||
const { query, onChange } = props;
|
||||
|
||||
@@ -51,7 +51,7 @@ const PromLink: FC<Props> = ({ panelData, query, datasource }) => {
|
||||
|
||||
setHref(getExternalLink());
|
||||
}
|
||||
}, [panelData]);
|
||||
}, [datasource, panelData, query]);
|
||||
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
|
||||
@@ -19,7 +19,7 @@ export const AnnotationListItemTags: FC<Props> = ({ tags, remove, onClick }) =>
|
||||
e.stopPropagation();
|
||||
onClick(tag, remove);
|
||||
},
|
||||
[remove]
|
||||
[onClick, remove]
|
||||
);
|
||||
|
||||
if (!tags || !tags.length) {
|
||||
|
||||
@@ -82,17 +82,7 @@ export function DashList(props: PanelProps<DashListOptions>) {
|
||||
fetchDashboards(props.options, props.replaceVariables).then((dashes) => {
|
||||
setDashboards(dashes);
|
||||
});
|
||||
}, [
|
||||
props.options.showSearch,
|
||||
props.options.showStarred,
|
||||
props.options.showRecentlyViewed,
|
||||
props.options.maxItems,
|
||||
props.options.query,
|
||||
props.options.tags,
|
||||
props.options.folderId,
|
||||
props.replaceVariables,
|
||||
props.renderCounter,
|
||||
]);
|
||||
}, [props.options, props.replaceVariables, props.renderCounter]);
|
||||
|
||||
const toggleDashboardStar = async (e: React.SyntheticEvent, dash: Dashboard) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NodeGraph } from '@grafana/ui';
|
||||
import { useLinks } from '../../../features/explore/utils/links';
|
||||
|
||||
export const NodeGraphPanel: React.FunctionComponent<PanelProps<Options>> = ({ width, height, data }) => {
|
||||
const getLinks = useLinks(data.timeRange);
|
||||
if (!data || !data.series.length) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
@@ -13,8 +14,6 @@ export const NodeGraphPanel: React.FunctionComponent<PanelProps<Options>> = ({ w
|
||||
);
|
||||
}
|
||||
|
||||
const getLinks = useLinks(data.timeRange);
|
||||
|
||||
return (
|
||||
<div style={{ width, height }}>
|
||||
<NodeGraph dataFrames={data.series} getLinks={getLinks} />
|
||||
|
||||
@@ -33,7 +33,7 @@ export const FillBellowToEditor: React.FC<FieldOverrideEditorProps<string, any>>
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, names);
|
||||
}, [names, value]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -63,7 +63,7 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text,
|
||||
</div>
|
||||
</TooltipContainer>
|
||||
);
|
||||
}, [time, tags, text]);
|
||||
}, [onMouseEnter, onMouseLeave, styles, time, text, tags]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -16,7 +16,7 @@ interface AnnotationsDataFrameViewDTO {
|
||||
|
||||
export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotations, timeZone }) => {
|
||||
const pluginId = 'AnnotationsPlugin';
|
||||
const plotCtx = usePlotContext();
|
||||
const { isPlotReady, registerPlugin, getPlotInstance } = usePlotContext();
|
||||
|
||||
const theme = useTheme();
|
||||
const annotationsRef = useRef<Array<DataFrameView<AnnotationsDataFrameViewDTO>>>();
|
||||
@@ -32,7 +32,7 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (plotCtx.isPlotReady) {
|
||||
if (isPlotReady) {
|
||||
const views: Array<DataFrameView<AnnotationsDataFrameViewDTO>> = [];
|
||||
|
||||
for (const frame of annotations) {
|
||||
@@ -41,10 +41,10 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
|
||||
annotationsRef.current = views;
|
||||
}
|
||||
}, [plotCtx.isPlotReady, annotations]);
|
||||
}, [isPlotReady, annotations]);
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = plotCtx.registerPlugin({
|
||||
const unregister = registerPlugin({
|
||||
id: pluginId,
|
||||
hooks: {
|
||||
// Render annotation lines on the canvas
|
||||
@@ -89,13 +89,13 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
return () => {
|
||||
unregister();
|
||||
};
|
||||
}, []);
|
||||
}, [registerPlugin, theme.palette.red]);
|
||||
|
||||
const mapAnnotationToXYCoords = useCallback(
|
||||
(frame: DataFrame, index: number) => {
|
||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||
const annotation = view.get(index);
|
||||
const plotInstance = plotCtx.getPlotInstance();
|
||||
const plotInstance = getPlotInstance();
|
||||
if (!annotation.time || !plotInstance) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
y: plotInstance.bbox.height / window.devicePixelRatio + 4,
|
||||
};
|
||||
},
|
||||
[plotCtx.getPlotInstance]
|
||||
[getPlotInstance]
|
||||
);
|
||||
|
||||
const renderMarker = useCallback(
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setIsOpen(!isOpen);
|
||||
}, [setIsOpen]);
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<ClickPlugin id="ContextMenu" onClick={onClick}>
|
||||
|
||||
@@ -20,20 +20,6 @@ export const XYChartPanel: React.FC<XYChartPanelProps> = ({
|
||||
}) => {
|
||||
const dims = useMemo(() => getXYDimensions(options.dims, data.series), [options.dims, data.series]);
|
||||
|
||||
if (dims.error) {
|
||||
return (
|
||||
<div>
|
||||
<div>ERROR: {dims.error}</div>
|
||||
{dims.hasData && (
|
||||
<div>
|
||||
<Button onClick={() => alert('TODO, switch vis')}>Show as Table</Button>
|
||||
{dims.hasTime && <Button onClick={() => alert('TODO, switch vis')}>Show as Time series</Button>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const frames = useMemo(() => [dims.frame], [dims]);
|
||||
|
||||
const onLegendClick = useCallback(
|
||||
@@ -50,6 +36,20 @@ export const XYChartPanel: React.FC<XYChartPanelProps> = ({
|
||||
[fieldConfig, onFieldConfigChange]
|
||||
);
|
||||
|
||||
if (dims.error) {
|
||||
return (
|
||||
<div>
|
||||
<div>ERROR: {dims.error}</div>
|
||||
{dims.hasData && (
|
||||
<div>
|
||||
<Button onClick={() => alert('TODO, switch vis')}>Show as Table</Button>
|
||||
{dims.hasTime && <Button onClick={() => alert('TODO, switch vis')}>Show as Time series</Button>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GraphNG
|
||||
data={frames}
|
||||
|
||||
@@ -23,10 +23,6 @@ export const XYDimsEditor: FC<StandardEditorProps<XYDimensionConfig, any, Option
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
if (!context.data) {
|
||||
return <div>No data...</div>;
|
||||
}
|
||||
|
||||
const frameNames = useMemo(() => {
|
||||
if (context?.data?.length) {
|
||||
return context.data.map((f, idx) => ({
|
||||
@@ -35,7 +31,7 @@ export const XYDimsEditor: FC<StandardEditorProps<XYDimensionConfig, any, Option
|
||||
}));
|
||||
}
|
||||
return [{ value: 0, label: 'First result' }];
|
||||
}, [context.data, value?.frame]);
|
||||
}, [context.data]);
|
||||
|
||||
const dims = useMemo(() => getXYDimensions(value, context.data), [context.data, value]);
|
||||
|
||||
@@ -87,6 +83,10 @@ export const XYDimsEditor: FC<StandardEditorProps<XYDimensionConfig, any, Option
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
if (!context.data) {
|
||||
return <div>No data...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
|
||||
Reference in New Issue
Block a user