foreman_statistics 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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