diff --git a/app/assets/javascripts/foreman_remote_execution/template_invocation.js b/app/assets/javascripts/foreman_remote_execution/template_invocation.js
index f7c6e4162..175bba9fc 100644
--- a/app/assets/javascripts/foreman_remote_execution/template_invocation.js
+++ b/app/assets/javascripts/foreman_remote_execution/template_invocation.js
@@ -47,7 +47,9 @@ function show_preview_hosts_modal() {
url: $('#previewHostsModal').attr('data-url')
}).then(function(result){
var modal_window = $('#previewHostsModal');
- modal_window.find('.modal-body').html(result);
+ // Sanitize the result to prevent XSS attacks
+ var sanitizedResult = $('
').text(result).html();
+ modal_window.find('.modal-body').html(sanitizedResult);
modal_window.modal({'show': true});
modal_window.find('a[rel="popover-modal"]').popover();
});
diff --git a/app/controllers/cockpit_controller.rb b/app/controllers/cockpit_controller.rb
index ccdefc1c7..0c8c4135d 100644
--- a/app/controllers/cockpit_controller.rb
+++ b/app/controllers/cockpit_controller.rb
@@ -8,15 +8,23 @@ def host_ssh_params
def redirect
return invalid_request unless params[:redirect_uri]
- redir_url = URI.parse(params[:redirect_uri])
+ begin
+ redir_url = URI.parse(params[:redirect_uri])
+ rescue URI::InvalidURIError
+ return invalid_request
+ end
+
+ # Validate URL scheme to prevent javascript: or data: schemes
+ return invalid_request unless %w[http https].include?(redir_url.scheme&.downcase)
cockpit_url = ScriptExecutionProvider.cockpit_url_for_host('')
- redir_url.query = if redir_url.hostname == URI.join(Setting[:foreman_url], cockpit_url).hostname
- "access_token=#{request.session_options[:id]}"
- else
- "error_description=Sorry"
- end
- redirect_to(redir_url.to_s)
+ expected_hostname = URI.join(Setting[:foreman_url], cockpit_url).hostname
+
+ # Only redirect to expected hostname to prevent open redirects
+ return invalid_request unless redir_url.hostname == expected_hostname
+
+ redir_url.query = "access_token=#{request.session_options[:id]}"
+ redirect_to(redir_url.to_s, allow_other_host: true)
end
private
diff --git a/test/functional/cockpit_controller_test.rb b/test/functional/cockpit_controller_test.rb
index f726ea8c2..216fc2793 100644
--- a/test/functional/cockpit_controller_test.rb
+++ b/test/functional/cockpit_controller_test.rb
@@ -13,4 +13,69 @@ def setup
response = ActiveSupport::JSON.decode(@response.body)
assert response.key?('ssh_user'), 'ssh_params response must include ssh_user'
end
+
+ describe 'redirect action security tests' do
+ setup do
+ Setting[:foreman_url] = 'https://foreman.example.com'
+ # Mock the cockpit URL
+ ScriptExecutionProvider.stubs(:cockpit_url_for_host).returns('/cockpit')
+ end
+
+ test "should reject redirect without redirect_uri parameter" do
+ get :redirect, session: set_session_user
+ assert_response :bad_request
+ end
+
+ test "should reject redirect with invalid URI" do
+ get :redirect, params: { redirect_uri: 'not-a-valid-uri::' }, session: set_session_user
+ assert_response :bad_request
+ end
+
+ test "should reject redirect with javascript scheme" do
+ get :redirect, params: { redirect_uri: 'javascript:alert("xss")' }, session: set_session_user
+ assert_response :bad_request
+ end
+
+ test "should reject redirect with data scheme" do
+ get :redirect, params: { redirect_uri: 'data:text/html,' }, session: set_session_user
+ assert_response :bad_request
+ end
+
+ test "should reject redirect with ftp scheme" do
+ get :redirect, params: { redirect_uri: 'ftp://evil.com/file' }, session: set_session_user
+ assert_response :bad_request
+ end
+
+ test "should reject redirect to wrong hostname" do
+ get :redirect, params: { redirect_uri: 'https://evil.com/cockpit' }, session: set_session_user
+ assert_response :bad_request
+ end
+
+ test "should reject redirect to subdomain attack" do
+ get :redirect, params: { redirect_uri: 'https://evil.foreman.example.com/cockpit' }, session: set_session_user
+ assert_response :bad_request
+ end
+
+ test "should allow redirect to valid cockpit URL" do
+ valid_uri = 'https://foreman.example.com/cockpit/path'
+ get :redirect, params: { redirect_uri: valid_uri }, session: set_session_user
+ assert_response :redirect
+
+ # Verify the redirect includes the access token
+ location = response.location
+ assert_includes location, 'access_token='
+ assert_includes location, 'https://foreman.example.com/cockpit/path'
+ end
+
+ test "should allow redirect with http scheme to same hostname" do
+ valid_uri = 'http://foreman.example.com/cockpit/path'
+ get :redirect, params: { redirect_uri: valid_uri }, session: set_session_user
+ assert_response :redirect
+ end
+
+ test "should handle case insensitive scheme validation" do
+ get :redirect, params: { redirect_uri: 'HTTPS://foreman.example.com/cockpit' }, session: set_session_user
+ assert_response :redirect
+ end
+ end
end
diff --git a/webpack/JobInvocationDetail/JobInvocationConstants.js b/webpack/JobInvocationDetail/JobInvocationConstants.js
index d851b4052..282e43291 100644
--- a/webpack/JobInvocationDetail/JobInvocationConstants.js
+++ b/webpack/JobInvocationDetail/JobInvocationConstants.js
@@ -74,7 +74,9 @@ const Columns = () => {
return { title: __('Failed'), status: 1 };
case 'planned':
return { title: __('Scheduled'), status: 2 };
- case 'running' || 'pending':
+ case 'running':
+ return { title: __('Pending'), status: 3 };
+ case 'pending':
return { title: __('Pending'), status: 3 };
case 'cancelled':
return { title: __('Cancelled'), status: 4 };
diff --git a/webpack/JobInvocationDetail/JobInvocationHostTable.js b/webpack/JobInvocationDetail/JobInvocationHostTable.js
index 9192720d1..4252b29ec 100644
--- a/webpack/JobInvocationDetail/JobInvocationHostTable.js
+++ b/webpack/JobInvocationDetail/JobInvocationHostTable.js
@@ -258,7 +258,7 @@ const JobInvocationHostTable = ({
setAPIOptions: filterApiCall,
};
- const results = apiResponse.results ?? [];
+ const results = apiResponse?.results ?? [];
const selectionToolbar = (
diff --git a/webpack/JobInvocationDetail/TemplateInvocation.js b/webpack/JobInvocationDetail/TemplateInvocation.js
index 175b41061..e1e0c2182 100644
--- a/webpack/JobInvocationDetail/TemplateInvocation.js
+++ b/webpack/JobInvocationDetail/TemplateInvocation.js
@@ -212,10 +212,10 @@ export const TemplateInvocation = ({
) : (
- {preview.plain}
+ {preview?.plain}
)}
>
)}
diff --git a/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js b/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js
index 1634c8ea3..db8603baf 100644
--- a/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js
+++ b/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js
@@ -144,4 +144,76 @@ describe('TemplateInvocation', () => {
expect(document.querySelectorAll('.pf-v5-c-skeleton')).toHaveLength(1);
});
+
+ test('handles null preview object gracefully', async () => {
+ const mockResponseWithNullPreview = {
+ ...mockTemplateInvocationResponse,
+ preview: null,
+ };
+
+ selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
+ 'RESOLVED'
+ );
+ selectors.selectTemplateInvocation.mockImplementation(() => () =>
+ mockResponseWithNullPreview
+ );
+
+ render(
+
+
+
+ );
+
+ // Enable showCommand to trigger preview rendering
+ act(() => {
+ fireEvent.click(screen.getByText('Command'));
+ });
+
+ // Should not crash and should not display any preview content
+ expect(screen.queryByText('template-invocation-preview')).not.toBeInTheDocument();
+ });
+
+ test('handles undefined preview.plain gracefully', async () => {
+ const mockResponseWithUndefinedPreview = {
+ ...mockTemplateInvocationResponse,
+ preview: {}, // preview object exists but plain is undefined
+ };
+
+ selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
+ 'RESOLVED'
+ );
+ selectors.selectTemplateInvocation.mockImplementation(() => () =>
+ mockResponseWithUndefinedPreview
+ );
+
+ render(
+
+
+
+ );
+
+ // Enable showCommand to trigger preview rendering
+ act(() => {
+ fireEvent.click(screen.getByText('Command'));
+ });
+
+ // Should not crash - the optional chaining should handle undefined preview.plain
+ expect(() => {
+ screen.getByText('template-invocation-preview');
+ }).not.toThrow();
+ });
});
diff --git a/webpack/JobWizard/JobWizard.js b/webpack/JobWizard/JobWizard.js
index 7a22b8389..386b9a8fd 100644
--- a/webpack/JobWizard/JobWizard.js
+++ b/webpack/JobWizard/JobWizard.js
@@ -99,8 +99,10 @@ export const JobWizard = ({ rerunData }) => {
if (inputs) {
setTemplateValues(prev => {
inputs.forEach(input => {
- defaultTemplateValues[input.name] =
- prev[input.name] || input?.default || '';
+ if (input) {
+ defaultTemplateValues[input.name] =
+ prev[input.name] || input.default || '';
+ }
});
return defaultTemplateValues;
});
@@ -108,8 +110,10 @@ export const JobWizard = ({ rerunData }) => {
setAdvancedValues(currentAdvancedValues => {
if (advancedInputs) {
advancedInputs.forEach(input => {
- advancedTemplateValues[input.name] =
- currentAdvancedValues[input.name] || input?.default || '';
+ if (input) {
+ advancedTemplateValues[input.name] =
+ currentAdvancedValues[input.name] || input.default || '';
+ }
});
}
return {
@@ -206,7 +210,7 @@ export const JobWizard = ({ rerunData }) => {
const [isStartsAtError, setIsStartsAtError] = useState(false);
useEffect(() => {
const updateStartsError = () => {
- if (scheduleValue.scheduleType === SCHEDULE_TYPES.FUTURE) {
+ if (scheduleValue?.scheduleType === SCHEDULE_TYPES.FUTURE) {
setIsStartsAtError(
!!scheduleValue?.startsAt?.length &&
new Date().getTime() >= new Date(scheduleValue.startsAt).getTime()
@@ -216,7 +220,7 @@ export const JobWizard = ({ rerunData }) => {
new Date().getTime() >=
new Date(scheduleValue.startsBefore).getTime()
);
- } else if (scheduleValue.scheduleType === SCHEDULE_TYPES.RECURRING) {
+ } else if (scheduleValue?.scheduleType === SCHEDULE_TYPES.RECURRING) {
setIsStartsAtError(
!!scheduleValue?.startsAt?.length &&
new Date().getTime() >= new Date(scheduleValue.startsAt).getTime()
diff --git a/webpack/JobWizard/JobWizardPageRerun.js b/webpack/JobWizard/JobWizardPageRerun.js
index f25938a99..82c75cfcc 100644
--- a/webpack/JobWizard/JobWizardPageRerun.js
+++ b/webpack/JobWizard/JobWizardPageRerun.js
@@ -173,10 +173,12 @@ const JobWizardPageRerun = ({
diff --git a/webpack/JobWizard/__tests__/JobWizard.null-inputs.test.js b/webpack/JobWizard/__tests__/JobWizard.null-inputs.test.js
new file mode 100644
index 000000000..c92048e1b
--- /dev/null
+++ b/webpack/JobWizard/__tests__/JobWizard.null-inputs.test.js
@@ -0,0 +1,171 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { render } from '@testing-library/react';
+import { MockedProvider } from '@apollo/client/testing';
+import * as api from 'foremanReact/redux/API';
+import { JobWizard } from '../JobWizard';
+import * as selectors from '../JobWizardSelectors';
+import { testSetup, gqlMock } from './fixtures';
+
+const store = testSetup(selectors, api);
+
+describe('JobWizard null input handling', () => {
+ beforeEach(() => {
+ jest.spyOn(selectors, 'selectRouterSearch');
+ selectors.selectRouterSearch.mockImplementation(() => ({}));
+
+ jest.spyOn(selectors, 'selectJobCategoriesStatus');
+ selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
+
+ jest.spyOn(selectors, 'selectJobTemplate');
+ jest.spyOn(selectors, 'selectJobCategoriesResponse');
+ jest.spyOn(selectors, 'selectTemplateError');
+ jest.spyOn(selectors, 'selectIsSubmitting');
+ jest.spyOn(selectors, 'selectIsLoading');
+
+ selectors.selectTemplateError.mockImplementation(() => false);
+ selectors.selectIsSubmitting.mockImplementation(() => false);
+ selectors.selectIsLoading.mockImplementation(() => false);
+ selectors.selectJobCategoriesResponse.mockImplementation(() => ({
+ default_template: 1,
+ default_category: 'Test Category'
+ }));
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ test('handles null inputs in template_inputs array gracefully', () => {
+ const templateResponseWithNullInputs = {
+ job_template: {
+ name: 'Test Template',
+ execution_timeout_interval: 60,
+ time_to_pickup: 30,
+ description_format: 'Test %{input1}',
+ job_category: 'Test Category',
+ },
+ template_inputs: [
+ { name: 'valid_input', default: 'test' },
+ null, // This should be handled gracefully
+ { name: 'another_valid_input', default: 'test2' },
+ undefined, // This should also be handled gracefully
+ ],
+ advanced_template_inputs: [],
+ effective_user: { value: 'root' },
+ randomized_ordering: false,
+ ssh_user: 'test',
+ concurrency_control: { level: 1 },
+ };
+
+ selectors.selectJobTemplate.mockImplementation(() => templateResponseWithNullInputs);
+
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
+ if (action.key === 'JOB_TEMPLATE') {
+ handleSuccess && handleSuccess({ data: templateResponseWithNullInputs });
+ }
+ return { type: 'get', ...action };
+ });
+
+ // This should not throw an error despite null inputs
+ expect(() => {
+ render(
+
+
+
+
+
+ );
+ }).not.toThrow();
+ });
+
+ test('handles null inputs in advanced_template_inputs array gracefully', () => {
+ const templateResponseWithNullAdvancedInputs = {
+ job_template: {
+ name: 'Test Template',
+ execution_timeout_interval: 60,
+ time_to_pickup: 30,
+ description_format: 'Test %{input1}',
+ job_category: 'Test Category',
+ },
+ template_inputs: [
+ { name: 'valid_input', default: 'test' },
+ ],
+ advanced_template_inputs: [
+ { name: 'valid_advanced_input', default: 'test' },
+ null, // This should be handled gracefully
+ { name: 'another_valid_advanced_input', default: 'test2' },
+ undefined, // This should also be handled gracefully
+ ],
+ effective_user: { value: 'root' },
+ randomized_ordering: false,
+ ssh_user: 'test',
+ concurrency_control: { level: 1 },
+ };
+
+ selectors.selectJobTemplate.mockImplementation(() => templateResponseWithNullAdvancedInputs);
+
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
+ if (action.key === 'JOB_TEMPLATE') {
+ handleSuccess && handleSuccess({ data: templateResponseWithNullAdvancedInputs });
+ }
+ return { type: 'get', ...action };
+ });
+
+ // This should not throw an error despite null advanced inputs
+ expect(() => {
+ render(
+
+
+
+
+
+ );
+ }).not.toThrow();
+ });
+
+ test('handles null schedule value gracefully', () => {
+ const templateResponse = {
+ job_template: {
+ name: 'Test Template',
+ execution_timeout_interval: 60,
+ time_to_pickup: 30,
+ description_format: 'Test template',
+ job_category: 'Test Category',
+ },
+ template_inputs: [],
+ advanced_template_inputs: [],
+ effective_user: { value: 'root' },
+ randomized_ordering: false,
+ ssh_user: 'test',
+ concurrency_control: { level: 1 },
+ };
+
+ selectors.selectJobTemplate.mockImplementation(() => templateResponse);
+
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
+ if (action.key === 'JOB_TEMPLATE') {
+ handleSuccess && handleSuccess({ data: templateResponse });
+ }
+ return { type: 'get', ...action };
+ });
+
+ // Test with rerunData that might have null scheduleValue
+ const rerunDataWithNullSchedule = {
+ job_category: 'Test Category',
+ template_invocations: [{ template_id: 1, effective_user: 'root' }],
+ targeting: { search_query: 'name ~ test' },
+ schedule: null, // This should be handled gracefully
+ };
+
+ expect(() => {
+ render(
+
+
+
+
+
+ );
+ }).not.toThrow();
+ });
+});
\ No newline at end of file
diff --git a/webpack/JobWizard/steps/form/Formatter.js b/webpack/JobWizard/steps/form/Formatter.js
index 33defd9b0..3aeaf26b2 100644
--- a/webpack/JobWizard/steps/form/Formatter.js
+++ b/webpack/JobWizard/steps/form/Formatter.js
@@ -51,7 +51,9 @@ const TemplateSearchField = ({
};
export const formatter = (input, values, setValue) => {
- const isSelectType = !!input?.options;
+ if (!input) return null;
+
+ const isSelectType = !!input.options;
const inputType = input.value_type;
const isTextType = inputType === 'plain' || !inputType; // null defaults to plain
diff --git a/webpack/JobWizard/steps/form/ResourceSelect.js b/webpack/JobWizard/steps/form/ResourceSelect.js
index 16389bea6..af839b5ca 100644
--- a/webpack/JobWizard/steps/form/ResourceSelect.js
+++ b/webpack/JobWizard/steps/form/ResourceSelect.js
@@ -48,7 +48,7 @@ export const ResourceSelect = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let selectOptions = [];
- if (response.subtotal > maxResults) {
+ if (response?.subtotal > maxResults) {
selectOptions = [
maxResults) {
+ if (response?.subtotal > maxResults) {
selectOptions = [
{
if (variant === SelectVariant.typeahead) {
- setSelected(response.results.find(r => r.id === selection)); // saving the name and id so we will have access to the name between steps
+ setSelected(response?.results?.find(r => r.id === selection)); // saving the name and id so we will have access to the name between steps
} else if (variant === SelectVariant.typeaheadMulti) {
if (selected.map(({ id }) => id).includes(selection)) {
setSelected(currentSelected =>
@@ -69,7 +69,7 @@ export const SearchSelect = ({
} else {
setSelected(currentSelected => [
...currentSelected,
- response.results.find(r => r.id === selection),
+ response?.results?.find(r => r.id === selection),
]);
}
}
diff --git a/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js b/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js
index 42d096b9e..ca2e09e62 100644
--- a/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js
+++ b/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js
@@ -2,7 +2,7 @@ import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { SearchSelect } from '../SearchSelect';
-const apiKey = 'HOSTS_KEY';
+const TEST_API_KEY = 'HOSTS_KEY';
describe('SearchSelect', () => {
it('too many', () => {
@@ -12,7 +12,7 @@ describe('SearchSelect', () => {
[
diff --git a/webpack/JobWizard/submit.js b/webpack/JobWizard/submit.js
index f741fc5cf..aa2982d86 100644
--- a/webpack/JobWizard/submit.js
+++ b/webpack/JobWizard/submit.js
@@ -24,7 +24,7 @@ export const submit = ({
startsBefore,
ends,
purpose,
- } = scheduleValue;
+ } = scheduleValue || {};
const {
sshUser,
effectiveUserValue,
diff --git a/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js b/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js
index 25cb6238a..d4ef98604 100644
--- a/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js
+++ b/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js
@@ -26,6 +26,30 @@ export const HostStatusFixtures = {
renders: {
status: 'success',
},
+ 'renders running status': {
+ status: 'running',
+ },
+ 'renders pending status': {
+ status: 'pending',
+ },
+ 'renders cancelled status': {
+ status: 'cancelled',
+ },
+ 'renders N/A status': {
+ status: 'N/A',
+ },
+ 'renders planned status': {
+ status: 'planned',
+ },
+ 'renders warning status': {
+ status: 'warning',
+ },
+ 'renders error status': {
+ status: 'error',
+ },
+ 'renders unknown status': {
+ status: 'unknown_status',
+ },
};
export const TargetingHostsFixtures = {
diff --git a/webpack/react_app/components/TargetingHosts/components/HostStatus.js b/webpack/react_app/components/TargetingHosts/components/HostStatus.js
index e6500f831..9e34aee8b 100644
--- a/webpack/react_app/components/TargetingHosts/components/HostStatus.js
+++ b/webpack/react_app/components/TargetingHosts/components/HostStatus.js
@@ -17,7 +17,13 @@ const HostStatus = ({ status }) => {
{__('Awaiting start')}
);
- case 'running' || 'pending':
+ case 'running':
+ return (
+
+ {__('Pending')}
+
+ );
+ case 'pending':
return (
{__('Pending')}
diff --git a/webpack/react_app/components/TargetingHosts/index.js b/webpack/react_app/components/TargetingHosts/index.js
index 159efe051..132a7a267 100644
--- a/webpack/react_app/components/TargetingHosts/index.js
+++ b/webpack/react_app/components/TargetingHosts/index.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { get } from 'foremanReact/redux/API';
@@ -27,8 +27,8 @@ const buildSearchQuery = (query, stateFilter) => {
);
return [query, filters]
.flat()
- .filter(x => x)
- .map(x => `(${x})`)
+ .filter((x) => x)
+ .map((x) => `(${x})`)
.join(' AND ');
};
@@ -50,38 +50,47 @@ const WrappedTargetingHosts = () => {
const [apiUrl, setApiUrl] = useState(getApiUrl(searchQuery, pagination));
const intervalExists = useSelector(selectIntervalExists);
- const handleSearch = (query, status) => {
- const defaultPagination = { page: 1, per_page: pagination.per_page };
- stopApiInterval();
+ const handleSearch = useCallback(
+ (query, status) => {
+ const defaultPagination = { page: 1, per_page: pagination.per_page };
+ stopApiInterval();
- setApiUrl(getApiUrl(buildSearchQuery(query, status), defaultPagination));
- setSearchQuery(query);
- setPagination(defaultPagination);
- };
+ setApiUrl(getApiUrl(buildSearchQuery(query, status), defaultPagination));
+ setSearchQuery(query);
+ setPagination(defaultPagination);
+ },
+ [pagination.per_page, stopApiInterval]
+ );
- const handlePagination = args => {
- stopApiInterval();
- setPagination(args);
- setApiUrl(getApiUrl(buildSearchQuery(searchQuery, statusFilter), args));
- };
+ const handlePagination = useCallback(
+ (args) => {
+ stopApiInterval();
+ setPagination(args);
+ setApiUrl(getApiUrl(buildSearchQuery(searchQuery, statusFilter), args));
+ },
+ [stopApiInterval, searchQuery, statusFilter]
+ );
- const stopApiInterval = () => {
+ const stopApiInterval = useCallback(() => {
if (intervalExists) {
dispatch(stopInterval(TARGETING_HOSTS));
}
- };
-
- const getData = url =>
- withInterval(
- get({
- key: TARGETING_HOSTS,
- url,
- handleError: () => {
- dispatch(stopInterval(TARGETING_HOSTS));
- },
- }),
- 1000
- );
+ }, [intervalExists, dispatch]);
+
+ const getData = useCallback(
+ (url) =>
+ withInterval(
+ get({
+ key: TARGETING_HOSTS,
+ url,
+ handleError: () => {
+ dispatch(stopInterval(TARGETING_HOSTS));
+ },
+ }),
+ 1000
+ ),
+ [dispatch]
+ );
useEffect(() => {
dispatch(getData(apiUrl));
@@ -93,18 +102,18 @@ const WrappedTargetingHosts = () => {
return () => {
dispatch(stopInterval(TARGETING_HOSTS));
};
- }, [dispatch, apiUrl, autoRefresh]);
+ }, [dispatch, apiUrl, autoRefresh, getData]);
useEffect(() => {
handleSearch(searchQuery, statusFilter);
- }, [statusFilter, searchQuery]);
+ }, [statusFilter, searchQuery, handleSearch]);
return (
chartFilter(null)(dispatch, null)}
+ statusFilterReset={(_x) => chartFilter(null)(dispatch, null)}
apiStatus={apiStatus}
items={items}
totalHosts={totalHosts}