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
pull/32321/head
Zoltán Bedi 5 years ago committed by GitHub
parent c9eff1892e
commit 8232b6ebbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .eslintrc
  2. 1
      packages/grafana-ui/src/components/DataSourceSettings/SigV4AuthSettings.tsx
  3. 13
      public/app/core/components/TransformersUI/FilterByValueTransformer/FilterByValueFilterEditor.tsx
  4. 2
      public/app/core/components/TransformersUI/FilterByValueTransformer/FilterByValueTransformerEditor.tsx
  5. 2
      public/app/core/components/TransformersUI/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx
  6. 4
      public/app/core/components/TransformersUI/GroupByTransformerEditor.tsx
  7. 6
      public/app/core/components/TransformersUI/OrganizeFieldsTransformerEditor.tsx
  8. 2
      public/app/core/components/TransformersUI/SortByTransformerEditor.tsx
  9. 2
      public/app/core/components/connectWithCleanUp.tsx
  10. 2
      public/app/features/admin/AdminEditOrgPage.tsx
  11. 2
      public/app/features/admin/AdminListOrgsPage.tsx
  12. 11
      public/app/features/admin/UserCreatePage.tsx
  13. 17
      public/app/features/admin/UserListAdminPage.tsx
  14. 2
      public/app/features/alerting/NotificationsListPage.tsx
  15. 2
      public/app/features/alerting/components/AlertingQueryPreview.tsx
  16. 27
      public/app/features/alerting/components/NotificationChannelForm.tsx
  17. 4
      public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx
  18. 11
      public/app/features/dashboard/components/Inspector/PanelInspector.tsx
  19. 2
      public/app/features/dashboard/components/Inspector/hooks.ts
  20. 2
      public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx
  21. 13
      public/app/features/dashboard/components/PanelEditor/PanelNotSupported.test.tsx
  22. 5
      public/app/features/dashboard/components/PanelEditor/PanelNotSupported.tsx
  23. 19
      public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx
  24. 1
      public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts
  25. 2
      public/app/features/dashboard/components/PanelEditor/utils.ts
  26. 2
      public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx
  27. 2
      public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx
  28. 2
      public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx
  29. 2
      public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx
  30. 8
      public/app/features/dashboard/components/SubMenu/DashboardLinks.tsx
  31. 3
      public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx
  32. 3
      public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx
  33. 2
      public/app/features/dashboard/components/VizTypePicker/VizTypePicker.tsx
  34. 30
      public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx
  35. 31
      public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx
  36. 67
      public/app/features/explore/TraceView/TraceView.tsx
  37. 38
      public/app/features/explore/TraceView/useDetailState.ts
  38. 2
      public/app/features/library-panels/components/DeleteLibraryPanelModal/DeleteLibraryPanelModal.tsx
  39. 2
      public/app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal.tsx
  40. 2
      public/app/features/library-panels/utils/usePanelSave.ts
  41. 2
      public/app/features/manage-dashboards/components/ImportDashboardForm.tsx
  42. 2
      public/app/features/panel/PanelRenderer.tsx
  43. 2
      public/app/features/playlist/usePlaylist.tsx
  44. 4
      public/app/features/playlist/usePlaylistItems.tsx
  45. 2
      public/app/features/sandbox/TestStuffPage.tsx
  46. 11
      public/app/features/search/components/SearchItem.tsx
  47. 2
      public/app/features/search/components/SectionHeader.tsx
  48. 2
      public/app/features/search/hooks/useSearch.ts
  49. 2
      public/app/features/variables/adhoc/picker/AdHocFilterBuilder.tsx
  50. 9
      public/app/features/variables/editor/LegacyVariableQueryEditor.tsx
  51. 27
      public/app/features/variables/editor/SelectionOptionsEditor.tsx
  52. 2
      public/app/features/variables/editor/VariableTypeSelect.tsx
  53. 12
      public/app/features/variables/inspect/NetworkGraph.tsx
  54. 2
      public/app/features/variables/textbox/TextBoxVariablePicker.tsx
  55. 12
      public/app/plugins/datasource/cloud-monitoring/components/Aggregations.tsx
  56. 15
      public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx
  57. 84
      public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx
  58. 15
      public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx
  59. 2
      public/app/plugins/datasource/cloudwatch/components/Dimensions.tsx
  60. 8
      public/app/plugins/datasource/cloudwatch/components/MetricsQueryFieldsEditor.tsx
  61. 2
      public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx
  62. 2
      public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx
  63. 2
      public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx
  64. 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AggregationField.tsx
  65. 23
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx
  66. 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/LegendFormatField.tsx
  67. 10
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNameField.tsx
  68. 9
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNamespaceField.tsx
  69. 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx
  70. 4
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/NamespaceField.tsx
  71. 4
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceGroupsField.tsx
  72. 4
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceNameField.tsx
  73. 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TimeGrainField.tsx
  74. 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TopField.tsx
  75. 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryTypeField.tsx
  76. 9
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx
  77. 3
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/metrics.ts
  78. 2
      public/app/plugins/datasource/influxdb/components/useShadowedState.ts
  79. 2
      public/app/plugins/datasource/prometheus/components/PromExploreQueryEditor.tsx
  80. 2
      public/app/plugins/datasource/prometheus/components/PromLink.tsx
  81. 2
      public/app/plugins/panel/annolist/AnnotationListItemTags.tsx
  82. 12
      public/app/plugins/panel/dashlist/DashList.tsx
  83. 3
      public/app/plugins/panel/nodeGraph/NodeGraphPanel.tsx
  84. 2
      public/app/plugins/panel/timeseries/FillBelowToEditor.tsx
  85. 2
      public/app/plugins/panel/timeseries/plugins/AnnotationMarker.tsx
  86. 14
      public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx
  87. 2
      public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx
  88. 28
      public/app/plugins/panel/xychart/XYChartPanel.tsx
  89. 10
      public/app/plugins/panel/xychart/XYDimsEditor.tsx

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

Loading…
Cancel
Save