foreman_statistics 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_statistics/api/v2/statistics_controller.rb +9 -1
  3. data/app/controllers/foreman_statistics/api/v2/trends_controller.rb +8 -0
  4. data/db/migrate/20200605153005_migrate_core_types.rb +6 -2
  5. data/lib/foreman_statistics/engine.rb +1 -1
  6. data/lib/foreman_statistics/version.rb +1 -1
  7. data/webpack/__mocks__/foremanReact/API.js +7 -0
  8. data/webpack/__mocks__/foremanReact/common/HOC.js +24 -0
  9. data/webpack/__mocks__/foremanReact/common/I18n.js +3 -0
  10. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  11. data/webpack/__mocks__/foremanReact/common/urlHelpers.js +1 -0
  12. data/webpack/__mocks__/foremanReact/components/ChartBox/index.js +2 -0
  13. data/webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalActions.js +2 -0
  14. data/webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalHooks.js +10 -0
  15. data/webpack/__mocks__/foremanReact/components/ForemanModal/index.js +4 -0
  16. data/webpack/__mocks__/foremanReact/components/Layout/LayoutActions.js +2 -0
  17. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
  18. data/webpack/__mocks__/foremanReact/components/common/EmptyState.js +5 -0
  19. data/webpack/__mocks__/foremanReact/components/common/MessageBox.js +4 -0
  20. data/webpack/__mocks__/foremanReact/components/common/dates/LongDateTime.js +5 -0
  21. data/webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js +3 -0
  22. data/webpack/__mocks__/foremanReact/components/common/table.js +5 -0
  23. data/webpack/__mocks__/foremanReact/components/common/table/actionsHelpers/actionTypeCreator.js +7 -0
  24. data/webpack/__mocks__/foremanReact/constants.js +24 -0
  25. data/webpack/__mocks__/foremanReact/readme.md +11 -0
  26. data/webpack/__mocks__/foremanReact/redux/actions/toasts.js +8 -0
  27. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  28. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/components/ExportButton/ExportButton.js +5 -0
  29. data/webpack/__mocks__/foremanReact/routes/common/reducerHOC/withDataReducer.js +35 -0
  30. data/webpack/fills_index.js +15 -0
  31. data/webpack/index.js +21 -0
  32. data/webpack/src/Components/StatisticsChartsList/StatisticsChartsList.fixtures.js +19 -0
  33. data/webpack/src/Components/StatisticsChartsList/StatisticsChartsList.test.js +18 -0
  34. data/webpack/src/Components/StatisticsChartsList/StatisticsChartsListStyles.scss +28 -0
  35. data/webpack/src/Components/StatisticsChartsList/__snapshots__/StatisticsChartsList.test.js.snap +42 -0
  36. data/webpack/src/Components/StatisticsChartsList/index.js +32 -0
  37. data/webpack/src/ForemanStatistics.js +11 -0
  38. data/webpack/src/Router/StatisticsPage/Statistics/Statistics.js +27 -0
  39. data/webpack/src/Router/StatisticsPage/StatisticsPage.fixtures.js +11 -0
  40. data/webpack/src/Router/StatisticsPage/StatisticsPage.js +21 -0
  41. data/webpack/src/Router/StatisticsPage/StatisticsPageActions.js +43 -0
  42. data/webpack/src/Router/StatisticsPage/StatisticsPageSelectors.js +12 -0
  43. data/webpack/src/Router/StatisticsPage/__tests__/StatisticsPage.test.js +12 -0
  44. data/webpack/src/Router/StatisticsPage/__tests__/StatisticsPageActions.test.js +27 -0
  45. data/webpack/src/Router/StatisticsPage/__tests__/StatisticsPageSelectors.test.js +32 -0
  46. data/webpack/src/Router/StatisticsPage/__tests__/__snapshots__/StatisticsPage.test.js.snap +32 -0
  47. data/webpack/src/Router/StatisticsPage/__tests__/__snapshots__/StatisticsPageActions.test.js.snap +54 -0
  48. data/webpack/src/Router/StatisticsPage/__tests__/__snapshots__/StatisticsPageSelectors.test.js.snap +35 -0
  49. data/webpack/src/Router/StatisticsPage/constants.js +4 -0
  50. data/webpack/src/Router/StatisticsPage/index.js +36 -0
  51. data/webpack/src/Router/__snapshots__/routes.test.js.snap +47 -0
  52. data/webpack/src/Router/index.js +14 -0
  53. data/webpack/src/Router/routes.js +11 -0
  54. data/webpack/src/Router/routes.test.js +27 -0
  55. data/webpack/src/index.js +1 -0
  56. data/webpack/src/reducers.js +7 -0
  57. data/webpack/src/trends.js +7 -0
  58. data/webpack/src/trends.test.js +44 -0
  59. metadata +58 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba423682698624b7d224e67b892a00982dfadb9ba225c3431212fda28a8665bd
4
- data.tar.gz: abfbe9e5304f233e205438219fffc9d6ad7b7e8e5bcc9a041c54cb2a344848a2
3
+ metadata.gz: c2535d7ffe5c9d9e7d594d61a1ed6e5fbed330c3b5739a3792ce78910c365ef2
4
+ data.tar.gz: 8be508f7afacb9d168013e7d5a101e57dd775107f4d3aa12211b252d5a3bfe94
5
5
  SHA512:
6
- metadata.gz: 8bbe0bbeaedc311bca4f88dfbd91d7475432c4ed31c4805e34db9c8340c23a537bcbe67c6eba24cda22cf1a84505cee3cfc2509cc2aec6eefab45f701ddec04f
7
- data.tar.gz: 22444861f80d94fd85281af4a03420defef52336e006841f6e647a17008dacbdf09d9b24675e3399803c73163348c8f8f8564e496458bd99748da2db521395d0
6
+ metadata.gz: 1849bc62e6be28e6572727e87afaeba9c9e803643d0de003a0d903a81ba3b5a6a964755a1ebc734054ab1e09cd5116135b34d5392a2eb41e653d956dac1b9973
7
+ data.tar.gz: 0fa5e7507dcc8d5e75755cdbd8fd7e70195e1bc1fa37c037c0d00740da2771446fec24b49dbbed50dfe4b7d6e7d45a7c6386e5261cd5329e49bffac0808f8946
@@ -2,13 +2,14 @@ module ForemanStatistics
2
2
  module Api
3
3
  module V2
4
4
  class StatisticsController < ::Api::V2::BaseController
5
+ before_action :show_deprecation_for_core_routes
6
+
5
7
  resource_description do
6
8
  api_version 'v2'
7
9
  api_base_url '/foreman_statistics/api'
8
10
  end
9
11
 
10
12
  api :GET, '/statistics/', N_('Get statistics')
11
-
12
13
  def index
13
14
  @os_count = Host.authorized(:view_hosts).count_distribution :operatingsystem
14
15
  @arch_count = Host.authorized(:view_hosts).count_distribution :architecture
@@ -27,6 +28,13 @@ module ForemanStatistics
27
28
  :model_count => @model_count, :mem_size => @mem_size, :mem_free => @mem_free,
28
29
  :swap_free => @swap_free, :mem_totsize => @mem_totsize, :mem_totfree => @mem_totfree }
29
30
  end
31
+
32
+ private
33
+
34
+ def show_deprecation_for_core_routes
35
+ return if request.path.starts_with?('/foreman_statistics')
36
+ Foreman::Deprecation.api_deprecation_warning('/api/v2/statistics API endpoint is deprecated, please use /foreman_statistics/api/v2/statistics instead')
37
+ end
30
38
  end
31
39
  end
32
40
  end
@@ -4,6 +4,7 @@ module ForemanStatistics
4
4
  class TrendsController < ::Api::V2::BaseController
5
5
  include ForemanStatistics::Parameters::Trend
6
6
 
7
+ before_action :show_deprecation_for_core_routes
7
8
  before_action :find_resource, :only => %i[show destroy]
8
9
 
9
10
  TRENDABLE_TYPES = %w[
@@ -52,6 +53,13 @@ module ForemanStatistics
52
53
  def resource_scope(options = {})
53
54
  @resource_scope ||= scope_for(ForemanStatistics::Trend.types, options)
54
55
  end
56
+
57
+ private
58
+
59
+ def show_deprecation_for_core_routes
60
+ return if request.path.starts_with?('/foreman_statistics')
61
+ Foreman::Deprecation.api_deprecation_warning('/api/v2/trends API endpoints are deprecated, please use /foreman_statistics/api/v2/trends instead')
62
+ end
55
63
  end
56
64
  end
57
65
  end
@@ -1,15 +1,19 @@
1
1
  class MigrateCoreTypes < ActiveRecord::Migration[6.0]
2
+ class FakeTrend < ApplicationRecord
3
+ self.table_name = 'trends'
4
+ end
5
+
2
6
  def up
3
7
  Permission.where(:resource_type => 'Trend').update_all(:resource_type => 'ForemanStatistics::Trend')
4
8
  %w[ForemanTrend FactTrend Trend].each do |t|
5
- Trend.where(:type => t).update_all(:type => "ForemanStatistics::#{t}")
9
+ FakeTrend.where(:type => t).update_all(:type => "ForemanStatistics::#{t}")
6
10
  end
7
11
  end
8
12
 
9
13
  def down
10
14
  Permission.where(:resource_type => 'ForemanStatistics::Trend').update_all(:resource_type => 'Trend')
11
15
  %w[ForemanTrend FactTrend Trend].each do |t|
12
- Trend.where(:type => "ForemanStatistics::#{t}").update_all(:type => t)
16
+ FakeTrend.where(:type => "ForemanStatistics::#{t}").update_all(:type => t)
13
17
  end
14
18
  end
15
19
  end
@@ -8,7 +8,7 @@ module ForemanStatistics
8
8
  config.autoload_paths += Dir["#{config.root}/app/models/concerns"]
9
9
  config.autoload_paths += Dir["#{config.root}/app/overrides"]
10
10
 
11
- config.paths['db/migrate'] << 'db/migrate_foreman' if Gem::Dependency.new('', '>= 2.2').match?('', SETTINGS[:version])
11
+ config.paths['db/migrate'] << 'db/migrate_foreman' unless Gem::Version.new(SETTINGS[:version]).release < Gem::Version.new('2.2')
12
12
 
13
13
  # Add any db migrations
14
14
  initializer 'foreman_statistics.load_app_instance_data' do |app|
@@ -1,3 +1,3 @@
1
1
  module ForemanStatistics
2
- VERSION = '0.1.1'.freeze
2
+ VERSION = '0.1.2'.freeze
3
3
  end
@@ -0,0 +1,7 @@
1
+ export default {
2
+ get: jest.fn(),
3
+ put: jest.fn(),
4
+ post: jest.fn(),
5
+ delete: jest.fn(),
6
+ patch: jest.fn(),
7
+ };
@@ -0,0 +1,24 @@
1
+ import React, { useEffect } from 'react';
2
+
3
+ export const callOnMount = callback => WrappedComponent => componentProps => {
4
+ // fires callback onMount, [] means don't listen to any props change
5
+ useEffect(() => {
6
+ callback(componentProps);
7
+ }, [componentProps]);
8
+
9
+ return <WrappedComponent {...componentProps} />;
10
+ };
11
+
12
+ export const withRenderHandler = ({
13
+ Component,
14
+ LoadingComponent = () => jest.fn(),
15
+ ErrorComponent = () => jest.fn(),
16
+ EmptyComponent = () => jest.fn(),
17
+ }) => componentProps => {
18
+ const { isLoading, hasData, hasError } = componentProps;
19
+
20
+ if (isLoading && !hasData) return <LoadingComponent {...componentProps} />;
21
+ if (hasError) return <ErrorComponent {...componentProps} />;
22
+ if (hasData) return <Component {...componentProps} />;
23
+ return <EmptyComponent {...componentProps} />;
24
+ };
@@ -0,0 +1,3 @@
1
+ export const translate = s => s;
2
+
3
+ export const documentLocale = () => 'en';
@@ -0,0 +1 @@
1
+ export const noop = Function.prototype;
@@ -0,0 +1 @@
1
+ export const getURIsearch = () => 'a=b';
@@ -0,0 +1,2 @@
1
+ const ConnectedChartBox = () => jest.fn();
2
+ export default ConnectedChartBox;
@@ -0,0 +1,2 @@
1
+ const ForemanModalActions = () => jest.fn();
2
+ export default ForemanModalActions;
@@ -0,0 +1,10 @@
1
+ const modalOpen = true;
2
+ const setModalOpen = jest.fn();
3
+ const setModalClosed = jest.fn();
4
+
5
+ export const useForemanModal = () => ({
6
+ modalOpen,
7
+ setModalOpen,
8
+ setModalClosed,
9
+ });
10
+ export default useForemanModal;
@@ -0,0 +1,4 @@
1
+ const ForemanModal = () => jest.fn();
2
+ ForemanModal.Header = () => jest.fn();
3
+ ForemanModal.Footer = () => jest.fn();
4
+ export default ForemanModal;
@@ -0,0 +1,2 @@
1
+ export const showLoading = () => null;
2
+ export const hideLoading = () => null;
@@ -0,0 +1,2 @@
1
+ const PaginationWrapper = () => jest.fn();
2
+ export default PaginationWrapper;
@@ -0,0 +1,5 @@
1
+ const EmptyState = () => jest.fn();
2
+
3
+ export const EmptyStatePattern = () => jest.fn();
4
+
5
+ export default EmptyState;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+
3
+ export const MessageBox = () => <div className="message-box-root" />;
4
+ export default MessageBox;
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ export const LongDateTime = value => <p>{value}</p>;
4
+
5
+ export default LongDateTime;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+
3
+ export default date => <p>{`${date} time ago`}</p>;
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ export const Table = () => <div className="table" />;
4
+ export const createTableReducer = jest.fn(controller => controller);
5
+ export const cellFormatter = cell => cell;
@@ -0,0 +1,7 @@
1
+ const createTableActionTypes = tableID => ({
2
+ REQUEST: `${tableID.toUpperCase()}_REQUEST`,
3
+ SUCCESS: `${tableID.toUpperCase()}_SUCCESS`,
4
+ FAILURE: `${tableID.toUpperCase()}_FAILURE`,
5
+ });
6
+
7
+ export default createTableActionTypes;
@@ -0,0 +1,24 @@
1
+ export const STATUS = {
2
+ PENDING: 'PENDING',
3
+ RESOLVED: 'RESOLVED',
4
+ ERROR: 'ERROR',
5
+ };
6
+
7
+ export const getControllerSearchProps = (
8
+ controller,
9
+ id = 'searchBar',
10
+ canCreateBookmarks = true
11
+ ) => ({
12
+ controller,
13
+ autocomplete: {
14
+ id,
15
+ searchQuery: '',
16
+ url: `${controller}/auto_complete_search`,
17
+ useKeyShortcuts: true,
18
+ },
19
+ bookmarks: {
20
+ url: '/api/bookmarks',
21
+ canCreateBookmarks,
22
+ documentationUrl: `4.1.5Searching`,
23
+ },
24
+ });
@@ -0,0 +1,11 @@
1
+ For testing components which have imported foreman-core components,
2
+ a mock file is required in this folder.
3
+
4
+ ### Example: Mocking ForemanModal component
5
+ ```js
6
+ // __mocks__/foremanReact/components/ForemanModal/index.js
7
+ const ForemanModal = () => jest.fn();
8
+ ForemanModal.Header = () => jest.fn();
9
+ ForemanModal.Footer = () => jest.fn();
10
+ export default ForemanModal;
11
+ ```
@@ -0,0 +1,8 @@
1
+ export const addToast = toast => ({
2
+ type: 'TOASTS_ADD',
3
+ payload: {
4
+ message: toast,
5
+ },
6
+ });
7
+
8
+ export default addToast;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const PageLayout = ({ children }) => <div>{children}</div>;
5
+
6
+ PageLayout.propTypes = {
7
+ children: PropTypes.node.isRequired,
8
+ };
9
+
10
+ export default PageLayout;
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ const ExportButton = () => <button>export</button>;
4
+
5
+ export default ExportButton;
@@ -0,0 +1,35 @@
1
+ import Immutable from 'seamless-immutable';
2
+
3
+ const initialState = Immutable({
4
+ isLoading: true,
5
+ hasError: false,
6
+ hasData: false,
7
+ message: { type: 'empty', text: '' },
8
+ });
9
+
10
+ const withDataReducer = controller => (
11
+ state = initialState,
12
+ { type, payload }
13
+ ) => {
14
+ switch (type) {
15
+ case `${controller}_DATA_RESOLVED`:
16
+ return state.merge({ ...payload, isLoading: false });
17
+
18
+ case `${controller}_DATA_FAILED`:
19
+ return state.merge({ ...payload, isLoading: false, hasError: true });
20
+
21
+ case `${controller}_CLEAR_ERROR`:
22
+ return state.set('hasError', false);
23
+
24
+ case `${controller}_SHOW_LOADING`:
25
+ return state.set('isLoading', true);
26
+
27
+ case `${controller}_HIDE_LOADING`:
28
+ return state.set('isLoading', false);
29
+
30
+ default:
31
+ return state;
32
+ }
33
+ };
34
+
35
+ export default withDataReducer;
@@ -0,0 +1,15 @@
1
+ // This example for extanding foreman-core's component via slot&fill
2
+
3
+ /*
4
+ import React from 'react';
5
+ import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
6
+
7
+ addGlobalFill('slotId', 'fillId', <SomeComponent key="some-key" />, 300);
8
+
9
+ addGlobalFill(
10
+ 'slotId',
11
+ 'fillId',
12
+ { someProp: 'this is an override prop' },
13
+ 300
14
+ );
15
+ */
@@ -0,0 +1,21 @@
1
+ /* eslint import/no-unresolved: [2, { ignore: [foremanReact/*] }] */
2
+ /* eslint-disable import/no-extraneous-dependencies */
3
+ /* eslint-disable import/extensions */
4
+ import componentRegistry from 'foremanReact/components/componentRegistry';
5
+ import { registerReducer } from 'foremanReact/common/MountingService';
6
+ import reducers from './src/reducers';
7
+ import ForemanStatistics from './src/ForemanStatistics';
8
+ import * as trends from './src/trends';
9
+
10
+ Object.assign(window.tfm, { trends });
11
+
12
+ // register reducers
13
+ Object.entries(reducers).forEach(([key, reducer]) =>
14
+ registerReducer(key, reducer)
15
+ );
16
+
17
+ // register components for erb mounting
18
+ componentRegistry.register({
19
+ name: 'ForemanStatistics',
20
+ type: ForemanStatistics,
21
+ });
@@ -0,0 +1,19 @@
1
+ export const statisticsData = {
2
+ operatingsystem: {
3
+ id: 'operatingsystem',
4
+ title: 'OS Distribution',
5
+ url: 'statistics/operatingsystem',
6
+ search: '/hosts?search=os_title=~VAL~',
7
+ },
8
+ architecture: {
9
+ id: 'architecture',
10
+ title: 'Architecture Distribution',
11
+ url: 'statistics/architecture',
12
+ search: '/hosts?search=facts.architecture=~VAL~',
13
+ },
14
+ };
15
+
16
+ export const statisticsMeta = [
17
+ statisticsData.operatingsystem,
18
+ statisticsData.architecture,
19
+ ];
@@ -0,0 +1,18 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+
3
+ import StatisticsChartsList from './';
4
+ import { statisticsData } from './StatisticsChartsList.fixtures';
5
+
6
+ const fixtures = {
7
+ 'should render no panels for empty data': {
8
+ data: {},
9
+ },
10
+ 'should render two panels for fixtures data': {
11
+ data: statisticsData,
12
+ },
13
+ };
14
+
15
+ describe('StatisticsChartsList', () => {
16
+ describe('rendering', () =>
17
+ testComponentSnapshotsWithFixtures(StatisticsChartsList, fixtures));
18
+ });
@@ -0,0 +1,28 @@
1
+ .statistics-charts-list-root {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ justify-content: space-around;
5
+ padding-top: 20px;
6
+ .chart-box {
7
+ height: 312px;
8
+ width: 280px;
9
+ display: -ms-flexbox;
10
+ display: flex;
11
+ -ms-flex-direction: column;
12
+ flex-direction: column;
13
+ min-height: 312px;
14
+
15
+ .card-pf-heading {
16
+ -ms-flex: 0 0 30px;
17
+ flex: 0 0 30px;
18
+ }
19
+
20
+ .card-pf-body {
21
+ display: -ms-flexbox;
22
+ display: flex;
23
+ -ms-flex: 1;
24
+ flex: 1;
25
+ margin: 0;
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,42 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`StatisticsChartsList rendering should render no panels for empty data 1`] = `
4
+ <div
5
+ className="statistics-charts-list-root cards-pf"
6
+ />
7
+ `;
8
+
9
+ exports[`StatisticsChartsList rendering should render two panels for fixtures data 1`] = `
10
+ <div
11
+ className="statistics-charts-list-root cards-pf"
12
+ >
13
+ <ConnectedChartBox
14
+ chart={
15
+ Object {
16
+ "id": "operatingsystem",
17
+ "search": "/hosts?search=os_title=~VAL~",
18
+ "title": "OS Distribution",
19
+ "url": "statistics/operatingsystem",
20
+ }
21
+ }
22
+ key="operatingsystem"
23
+ noDataMsg="No data available"
24
+ tip="Expand the chart"
25
+ type="donut"
26
+ />
27
+ <ConnectedChartBox
28
+ chart={
29
+ Object {
30
+ "id": "architecture",
31
+ "search": "/hosts?search=facts.architecture=~VAL~",
32
+ "title": "Architecture Distribution",
33
+ "url": "statistics/architecture",
34
+ }
35
+ }
36
+ key="architecture"
37
+ noDataMsg="No data available"
38
+ tip="Expand the chart"
39
+ type="donut"
40
+ />
41
+ </div>
42
+ `;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import ConnectedChartBox from 'foremanReact/components/ChartBox';
6
+ import './StatisticsChartsListStyles.scss';
7
+
8
+ const StatisticsChartsList = ({ data }) => {
9
+ const chartBoxes = Object.values(data).map(chart => (
10
+ <ConnectedChartBox
11
+ key={chart.id}
12
+ type="donut"
13
+ chart={chart}
14
+ noDataMsg={__('No data available')}
15
+ tip={__('Expand the chart')}
16
+ />
17
+ ));
18
+
19
+ return (
20
+ <div className="statistics-charts-list-root cards-pf">{chartBoxes}</div>
21
+ );
22
+ };
23
+
24
+ StatisticsChartsList.propTypes = {
25
+ data: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
26
+ };
27
+
28
+ StatisticsChartsList.defaultProps = {
29
+ data: [],
30
+ };
31
+
32
+ export default StatisticsChartsList;
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { BrowserRouter } from 'react-router-dom';
3
+ import ForemanStatisticsRoute from './Router';
4
+
5
+ const ForemanStatistics = () => (
6
+ <BrowserRouter>
7
+ <ForemanStatisticsRoute />
8
+ </BrowserRouter>
9
+ );
10
+
11
+ export default ForemanStatistics;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { withRenderHandler } from 'foremanReact/common/HOC';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { EmptyStatePattern } from 'foremanReact/components/common/EmptyState';
6
+ import StatisticsChartsList from '../../../Components/StatisticsChartsList';
7
+
8
+ const Statistics = ({ statisticsMeta }) => {
9
+ if (!statisticsMeta) {
10
+ return (
11
+ <EmptyStatePattern
12
+ icon="info"
13
+ header={__('No Charts To Load')}
14
+ description=""
15
+ />
16
+ );
17
+ }
18
+ return <StatisticsChartsList data={statisticsMeta} />;
19
+ };
20
+
21
+ Statistics.propTypes = {
22
+ statisticsMeta: PropTypes.array.isRequired,
23
+ };
24
+
25
+ export default withRenderHandler({
26
+ Component: Statistics,
27
+ });
@@ -0,0 +1,11 @@
1
+ import { noop } from 'foremanReact/common/helpers';
2
+ import { statisticsMeta } from '../../Components/StatisticsChartsList/StatisticsChartsList.fixtures';
3
+
4
+ export const statisticsProps = {
5
+ statisticsMeta,
6
+ isLoading: false,
7
+ hasData: true,
8
+ hasError: false,
9
+ message: {},
10
+ getStatisticsMeta: noop,
11
+ };
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
5
+ import Statistics from './Statistics/Statistics';
6
+
7
+ const StatisticsPage = ({ statisticsMeta, ...props }) => (
8
+ <PageLayout header={__('Statistics')} searchable={false}>
9
+ <Statistics statisticsMeta={statisticsMeta} {...props} />
10
+ </PageLayout>
11
+ );
12
+
13
+ StatisticsPage.propTypes = {
14
+ statisticsMeta: PropTypes.array,
15
+ };
16
+
17
+ StatisticsPage.defaultProps = {
18
+ statisticsMeta: [],
19
+ };
20
+
21
+ export default StatisticsPage;
@@ -0,0 +1,43 @@
1
+ import API from 'foremanReact/API';
2
+
3
+ import {
4
+ STATISTICS_PAGE_DATA_RESOLVED,
5
+ STATISTICS_PAGE_DATA_FAILED,
6
+ STATISTICS_PAGE_HIDE_LOADING,
7
+ STATISTICS_PAGE_URL,
8
+ } from './constants';
9
+
10
+ export const getStatisticsMeta = (
11
+ url = STATISTICS_PAGE_URL
12
+ ) => async dispatch => {
13
+ const onFetchSuccess = ({ data }) => {
14
+ dispatch(hideLoading());
15
+ dispatch({
16
+ type: STATISTICS_PAGE_DATA_RESOLVED,
17
+ payload: { metadata: data.charts, hasData: data.charts.length > 0 },
18
+ });
19
+ };
20
+
21
+ const onFetchError = ({ message }) => {
22
+ dispatch(hideLoading());
23
+ dispatch({
24
+ type: STATISTICS_PAGE_DATA_FAILED,
25
+ payload: {
26
+ message: {
27
+ type: 'error',
28
+ text: message,
29
+ },
30
+ },
31
+ });
32
+ };
33
+ try {
34
+ const response = await API.get(url);
35
+ return onFetchSuccess(response);
36
+ } catch (error) {
37
+ return onFetchError(error);
38
+ }
39
+ };
40
+
41
+ const hideLoading = () => ({
42
+ type: STATISTICS_PAGE_HIDE_LOADING,
43
+ });
@@ -0,0 +1,12 @@
1
+ export const selectStatisticsPage = state => state.statisticsPage;
2
+
3
+ export const selectStatisticsMetadata = state =>
4
+ selectStatisticsPage(state).metadata;
5
+ export const selectStatisticsIsLoading = state =>
6
+ selectStatisticsPage(state).isLoading;
7
+ export const selectStatisticsMessage = state =>
8
+ selectStatisticsPage(state).message;
9
+ export const selectStatisticsHasError = state =>
10
+ selectStatisticsPage(state).hasError;
11
+ export const selectStatisticsHasMetadata = state =>
12
+ selectStatisticsPage(state).hasData;
@@ -0,0 +1,12 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+ import { statisticsProps } from '../StatisticsPage.fixtures';
3
+ import StatisticsPage from '../StatisticsPage';
4
+
5
+ const fixtures = {
6
+ 'render with props': statisticsProps,
7
+ };
8
+
9
+ describe('StatisticsPage', () => {
10
+ describe('rendering', () =>
11
+ testComponentSnapshotsWithFixtures(StatisticsPage, fixtures));
12
+ });
@@ -0,0 +1,27 @@
1
+ import API from 'foremanReact/API';
2
+
3
+ import { testActionSnapshotWithFixtures } from '@theforeman/test';
4
+ import { getStatisticsMeta } from '../StatisticsPageActions';
5
+ import { statisticsProps } from '../StatisticsPage.fixtures';
6
+
7
+ jest.mock('foremanReact/API');
8
+
9
+ const runStatisticsAction = (callback, props, serverMock) => {
10
+ API.get.mockImplementation(serverMock);
11
+
12
+ return callback(props);
13
+ };
14
+
15
+ const fixtures = {
16
+ 'should fetch statisticsMeta': () =>
17
+ runStatisticsAction(getStatisticsMeta, {}, async () => ({
18
+ data: { charts: statisticsProps.statisticsMeta },
19
+ })),
20
+ 'should fetch statisticsMeta and fail': () =>
21
+ runStatisticsAction(getStatisticsMeta, {}, async () => {
22
+ throw new Error('some-error');
23
+ }),
24
+ };
25
+
26
+ describe('StatisticsPage actions', () =>
27
+ testActionSnapshotWithFixtures(fixtures));
@@ -0,0 +1,32 @@
1
+ import { testSelectorsSnapshotWithFixtures } from '@theforeman/test';
2
+ import {
3
+ selectStatisticsPage,
4
+ selectStatisticsMetadata,
5
+ selectStatisticsHasMetadata,
6
+ selectStatisticsIsLoading,
7
+ selectStatisticsMessage,
8
+ selectStatisticsHasError,
9
+ } from '../StatisticsPageSelectors';
10
+ import { statisticsProps } from '../StatisticsPage.fixtures';
11
+
12
+ const state = {
13
+ statisticsPage: {
14
+ ...statisticsProps,
15
+ },
16
+ };
17
+
18
+ const fixtures = {
19
+ 'should return StatisticsPage': () => selectStatisticsPage(state),
20
+ 'should return StatisticsHasMetadata': () =>
21
+ selectStatisticsHasMetadata(state),
22
+ 'should return StatisticsPage statisticsMeta': () =>
23
+ selectStatisticsMetadata(state),
24
+ 'should return StatisticsPage isLoading': () =>
25
+ selectStatisticsIsLoading(state),
26
+ 'should return StatisticsPage Message': () => selectStatisticsMessage(state),
27
+ 'should return StatisticsPage hasError': () =>
28
+ selectStatisticsHasError(state),
29
+ };
30
+
31
+ describe('StatisticsPage selectors', () =>
32
+ testSelectorsSnapshotWithFixtures(fixtures));
@@ -0,0 +1,32 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`StatisticsPage rendering render with props 1`] = `
4
+ <PageLayout
5
+ header="Statistics"
6
+ searchable={false}
7
+ >
8
+ <Component
9
+ getStatisticsMeta={[Function]}
10
+ hasData={true}
11
+ hasError={false}
12
+ isLoading={false}
13
+ message={Object {}}
14
+ statisticsMeta={
15
+ Array [
16
+ Object {
17
+ "id": "operatingsystem",
18
+ "search": "/hosts?search=os_title=~VAL~",
19
+ "title": "OS Distribution",
20
+ "url": "statistics/operatingsystem",
21
+ },
22
+ Object {
23
+ "id": "architecture",
24
+ "search": "/hosts?search=facts.architecture=~VAL~",
25
+ "title": "Architecture Distribution",
26
+ "url": "statistics/architecture",
27
+ },
28
+ ]
29
+ }
30
+ />
31
+ </PageLayout>
32
+ `;
@@ -0,0 +1,54 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`StatisticsPage actions should fetch statisticsMeta 1`] = `
4
+ Array [
5
+ Array [
6
+ Object {
7
+ "type": "STATISTICS_PAGE_HIDE_LOADING",
8
+ },
9
+ ],
10
+ Array [
11
+ Object {
12
+ "payload": Object {
13
+ "hasData": true,
14
+ "metadata": Array [
15
+ Object {
16
+ "id": "operatingsystem",
17
+ "search": "/hosts?search=os_title=~VAL~",
18
+ "title": "OS Distribution",
19
+ "url": "statistics/operatingsystem",
20
+ },
21
+ Object {
22
+ "id": "architecture",
23
+ "search": "/hosts?search=facts.architecture=~VAL~",
24
+ "title": "Architecture Distribution",
25
+ "url": "statistics/architecture",
26
+ },
27
+ ],
28
+ },
29
+ "type": "STATISTICS_PAGE_DATA_RESOLVED",
30
+ },
31
+ ],
32
+ ]
33
+ `;
34
+
35
+ exports[`StatisticsPage actions should fetch statisticsMeta and fail 1`] = `
36
+ Array [
37
+ Array [
38
+ Object {
39
+ "type": "STATISTICS_PAGE_HIDE_LOADING",
40
+ },
41
+ ],
42
+ Array [
43
+ Object {
44
+ "payload": Object {
45
+ "message": Object {
46
+ "text": "some-error",
47
+ "type": "error",
48
+ },
49
+ },
50
+ "type": "STATISTICS_PAGE_FETCH_FAILED",
51
+ },
52
+ ],
53
+ ]
54
+ `;
@@ -0,0 +1,35 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`StatisticsPage selectors should return StatisticsHasMetadata 1`] = `true`;
4
+
5
+ exports[`StatisticsPage selectors should return StatisticsPage 1`] = `
6
+ Object {
7
+ "getStatisticsMeta": [Function],
8
+ "hasData": true,
9
+ "hasError": false,
10
+ "isLoading": false,
11
+ "message": Object {},
12
+ "statisticsMeta": Array [
13
+ Object {
14
+ "id": "operatingsystem",
15
+ "search": "/hosts?search=os_title=~VAL~",
16
+ "title": "OS Distribution",
17
+ "url": "statistics/operatingsystem",
18
+ },
19
+ Object {
20
+ "id": "architecture",
21
+ "search": "/hosts?search=facts.architecture=~VAL~",
22
+ "title": "Architecture Distribution",
23
+ "url": "statistics/architecture",
24
+ },
25
+ ],
26
+ }
27
+ `;
28
+
29
+ exports[`StatisticsPage selectors should return StatisticsPage Message 1`] = `Object {}`;
30
+
31
+ exports[`StatisticsPage selectors should return StatisticsPage hasError 1`] = `false`;
32
+
33
+ exports[`StatisticsPage selectors should return StatisticsPage isLoading 1`] = `false`;
34
+
35
+ exports[`StatisticsPage selectors should return StatisticsPage statisticsMeta 1`] = `undefined`;
@@ -0,0 +1,4 @@
1
+ export const STATISTICS_PAGE_DATA_RESOLVED = 'STATISTICS_PAGE_DATA_RESOLVED';
2
+ export const STATISTICS_PAGE_DATA_FAILED = 'STATISTICS_PAGE_FETCH_FAILED';
3
+ export const STATISTICS_PAGE_HIDE_LOADING = 'STATISTICS_PAGE_HIDE_LOADING';
4
+ export const STATISTICS_PAGE_URL = '/foreman_statistics/statistics';
@@ -0,0 +1,36 @@
1
+ import { compose, bindActionCreators } from 'redux';
2
+ import { connect } from 'react-redux';
3
+ import { callOnMount } from 'foremanReact/common/HOC';
4
+ import withDataReducer from 'foremanReact/routes/common/reducerHOC/withDataReducer';
5
+
6
+ import * as actions from './StatisticsPageActions';
7
+ import {
8
+ selectStatisticsMetadata,
9
+ selectStatisticsMessage,
10
+ selectStatisticsIsLoading,
11
+ selectStatisticsHasMetadata,
12
+ selectStatisticsHasError,
13
+ } from './StatisticsPageSelectors';
14
+
15
+ import StatisticsPage from './StatisticsPage';
16
+
17
+ // map state to props
18
+ const mapStateToProps = state => ({
19
+ statisticsMeta: selectStatisticsMetadata(state),
20
+ isLoading: selectStatisticsIsLoading(state),
21
+ message: selectStatisticsMessage(state),
22
+ hasData: selectStatisticsHasMetadata(state),
23
+ hasError: selectStatisticsHasError(state),
24
+ });
25
+
26
+ // map action dispatchers to props
27
+ const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch);
28
+
29
+ // export reducers
30
+ export const reducers = { statisticsPage: withDataReducer('STATISTICS_PAGE') };
31
+
32
+ // export connected component
33
+ export default compose(
34
+ connect(mapStateToProps, mapDispatchToProps),
35
+ callOnMount(({ getStatisticsMeta }) => getStatisticsMeta())
36
+ )(StatisticsPage);
@@ -0,0 +1,47 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ForemanPluginTemplateRoutes should create routes 1`] = `
4
+ Object {
5
+ "statistics": Object {
6
+ "component": Object {
7
+ "$$typeof": Symbol(react.memo),
8
+ "WrappedComponent": [Function],
9
+ "compare": null,
10
+ "displayName": "Connect(Component)",
11
+ "type": [Function],
12
+ },
13
+ "exact": true,
14
+ "path": "/foreman_statistics/statistics",
15
+ "renderResult": <ContextProvider
16
+ value={
17
+ Object {
18
+ "store": Object {
19
+ "dispatch": [MockFunction],
20
+ "getState": [Function],
21
+ "subscribe": [MockFunction],
22
+ },
23
+ "subscription": Subscription {
24
+ "handleChangeWrapper": [Function],
25
+ "listeners": Object {
26
+ "notify": [Function],
27
+ },
28
+ "onStateChange": [Function],
29
+ "parentSub": undefined,
30
+ "store": Object {
31
+ "dispatch": [MockFunction],
32
+ "getState": [Function],
33
+ "subscribe": [MockFunction],
34
+ },
35
+ "unsubscribe": null,
36
+ },
37
+ }
38
+ }
39
+ >
40
+ <Connect(Component)
41
+ history={Object {}}
42
+ some="props"
43
+ />
44
+ </ContextProvider>,
45
+ },
46
+ }
47
+ `;
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { Switch, Route } from 'react-router-dom';
3
+
4
+ import routes from './routes';
5
+
6
+ const Router = () => (
7
+ <Switch>
8
+ {Object.entries(routes).map(([key, props]) => (
9
+ <Route key={key} {...props} />
10
+ ))}
11
+ </Switch>
12
+ );
13
+
14
+ export default Router;
@@ -0,0 +1,11 @@
1
+ import StatisticsPage from './StatisticsPage';
2
+
3
+ const routes = {
4
+ statistics: {
5
+ path: '/foreman_statistics/statistics',
6
+ exact: true,
7
+ component: StatisticsPage,
8
+ },
9
+ };
10
+
11
+ export default routes;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { shallow } from '@theforeman/test';
4
+
5
+ import { statisticsProps } from './StatisticsPage/StatisticsPage.fixtures';
6
+ import Routes from './routes';
7
+
8
+ describe('ForemanPluginTemplateRoutes', () => {
9
+ it('should create routes', () => {
10
+ Object.entries(Routes).forEach(([key, Route]) => {
11
+ const store = {
12
+ subscribe: jest.fn(),
13
+ dispatch: jest.fn(),
14
+ getState: () => statisticsProps,
15
+ };
16
+ const RouteComponent = Route.component;
17
+ const component = shallow(
18
+ <Provider store={store}>
19
+ <RouteComponent history={{}} some="props" />
20
+ </Provider>
21
+ );
22
+ Route.renderResult = component;
23
+ });
24
+
25
+ expect(Routes).toMatchSnapshot();
26
+ });
27
+ });
@@ -0,0 +1 @@
1
+ export { default } from './ForemanStatistics';
@@ -0,0 +1,7 @@
1
+ // import { combineReducers } from 'redux';
2
+
3
+ const reducers = {
4
+ // foremanStatistics: combineReducers({}),
5
+ };
6
+
7
+ export default reducers;
@@ -0,0 +1,7 @@
1
+ export function trendTypeSelected({ value }) {
2
+ ['trend_trendable_id', 'trend_name'].forEach(id => {
3
+ const element = document.getElementById(id);
4
+ element.disabled = value !== 'FactName';
5
+ element.value = '';
6
+ });
7
+ }
@@ -0,0 +1,44 @@
1
+ /* eslint-disable jquery/no-val */
2
+ /* eslint-disable jquery/no-is */
3
+
4
+ import $ from 'jquery';
5
+
6
+ jest.unmock('jquery');
7
+ jest.unmock('./trends');
8
+ window.trends = require('./trends');
9
+
10
+ describe('selecting trend type', () => {
11
+ it('should disable fields on non-fact trend', () => {
12
+ document.body.innerHTML = `<select id="trendable_type" onchange="trends.trendTypeSelected(this)">
13
+ <option value="FactName">Facts</option>
14
+ <option value="Hostgroup">Host group</option>
15
+ </select>
16
+ <select id="trend_trendable_id">
17
+ <option value=""></option>
18
+ <option value="27">architecture</option>
19
+ </select>
20
+ <input id="trend_name">`;
21
+ $('#trendable_type')
22
+ .val('Hostgroup')
23
+ .change();
24
+ expect($('#trend_trendable_id').is(':disabled')).toBeTruthy();
25
+ expect($('#trend_name').is(':disabled')).toBeTruthy();
26
+ });
27
+
28
+ it('should enable fields on non-fact trend', () => {
29
+ document.body.innerHTML = `<select id="trendable_type" onchange="trends.trendTypeSelected(this)">
30
+ <option value="Evironment">Environment</option>
31
+ <option value="FactName">Facts</option>
32
+ </select>
33
+ <select id="trend_trendable_id" disabled>
34
+ <option value=""></option>
35
+ <option value="27">architecture</option>
36
+ </select>
37
+ <input id="trend_name" disabled>`;
38
+ $('#trendable_type')
39
+ .val('FactName')
40
+ .change();
41
+ expect($('#trend_trendable_id').is(':disabled')).toBeFalsy();
42
+ expect($('#trend_name').is(':disabled')).toBeFalsy();
43
+ });
44
+ });
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_statistics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ondrej Ezr
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-18 00:00:00.000000000 Z
11
+ date: 2020-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rdoc
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: rubocop
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '0.83'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '0.83'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rubocop-minitest
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -159,6 +159,58 @@ files:
159
159
  - test/unit/foreman_statistics/statistics_test.rb
160
160
  - test/unit/foreman_statistics_test.rb
161
161
  - test/unit/tasks/foreman_statistics_tasks_test.rb
162
+ - webpack/__mocks__/foremanReact/API.js
163
+ - webpack/__mocks__/foremanReact/common/HOC.js
164
+ - webpack/__mocks__/foremanReact/common/I18n.js
165
+ - webpack/__mocks__/foremanReact/common/helpers.js
166
+ - webpack/__mocks__/foremanReact/common/urlHelpers.js
167
+ - webpack/__mocks__/foremanReact/components/ChartBox/index.js
168
+ - webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalActions.js
169
+ - webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalHooks.js
170
+ - webpack/__mocks__/foremanReact/components/ForemanModal/index.js
171
+ - webpack/__mocks__/foremanReact/components/Layout/LayoutActions.js
172
+ - webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js
173
+ - webpack/__mocks__/foremanReact/components/common/EmptyState.js
174
+ - webpack/__mocks__/foremanReact/components/common/MessageBox.js
175
+ - webpack/__mocks__/foremanReact/components/common/dates/LongDateTime.js
176
+ - webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js
177
+ - webpack/__mocks__/foremanReact/components/common/table.js
178
+ - webpack/__mocks__/foremanReact/components/common/table/actionsHelpers/actionTypeCreator.js
179
+ - webpack/__mocks__/foremanReact/constants.js
180
+ - webpack/__mocks__/foremanReact/readme.md
181
+ - webpack/__mocks__/foremanReact/redux/actions/toasts.js
182
+ - webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js
183
+ - webpack/__mocks__/foremanReact/routes/common/PageLayout/components/ExportButton/ExportButton.js
184
+ - webpack/__mocks__/foremanReact/routes/common/reducerHOC/withDataReducer.js
185
+ - webpack/fills_index.js
186
+ - webpack/index.js
187
+ - webpack/src/Components/StatisticsChartsList/StatisticsChartsList.fixtures.js
188
+ - webpack/src/Components/StatisticsChartsList/StatisticsChartsList.test.js
189
+ - webpack/src/Components/StatisticsChartsList/StatisticsChartsListStyles.scss
190
+ - webpack/src/Components/StatisticsChartsList/__snapshots__/StatisticsChartsList.test.js.snap
191
+ - webpack/src/Components/StatisticsChartsList/index.js
192
+ - webpack/src/ForemanStatistics.js
193
+ - webpack/src/Router/StatisticsPage/Statistics/Statistics.js
194
+ - webpack/src/Router/StatisticsPage/StatisticsPage.fixtures.js
195
+ - webpack/src/Router/StatisticsPage/StatisticsPage.js
196
+ - webpack/src/Router/StatisticsPage/StatisticsPageActions.js
197
+ - webpack/src/Router/StatisticsPage/StatisticsPageSelectors.js
198
+ - webpack/src/Router/StatisticsPage/__tests__/StatisticsPage.test.js
199
+ - webpack/src/Router/StatisticsPage/__tests__/StatisticsPageActions.test.js
200
+ - webpack/src/Router/StatisticsPage/__tests__/StatisticsPageSelectors.test.js
201
+ - webpack/src/Router/StatisticsPage/__tests__/__snapshots__/StatisticsPage.test.js.snap
202
+ - webpack/src/Router/StatisticsPage/__tests__/__snapshots__/StatisticsPageActions.test.js.snap
203
+ - webpack/src/Router/StatisticsPage/__tests__/__snapshots__/StatisticsPageSelectors.test.js.snap
204
+ - webpack/src/Router/StatisticsPage/constants.js
205
+ - webpack/src/Router/StatisticsPage/index.js
206
+ - webpack/src/Router/__snapshots__/routes.test.js.snap
207
+ - webpack/src/Router/index.js
208
+ - webpack/src/Router/routes.js
209
+ - webpack/src/Router/routes.test.js
210
+ - webpack/src/index.js
211
+ - webpack/src/reducers.js
212
+ - webpack/src/trends.js
213
+ - webpack/src/trends.test.js
162
214
  homepage: https://theforeman.org
163
215
  licenses:
164
216
  - GPL-3.0