This is an automated email from the ASF dual-hosted git repository.

fjy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 01f3da6  Web Console: add Group by interval to segments view (#7986)
01f3da6 is described below

commit 01f3da6fda150280c074337a38880d2113786df0
Author: mcbrewster <[email protected]>
AuthorDate: Fri Jul 19 11:25:25 2019 -0700

    Web Console: add Group by interval to segments view (#7986)
    
    * add group by
    
    * update snapshot
    
    * fix spacing
    
    * change design pattern
    
    * rename interface
    
    * add static function
    
    * save
    
    * add whereClause"
    
    * add default page size
    
    * add where to inner query
    
    * snapshots
---
 web-console/script/mkcomp                          |  26 ++-
 web-console/src/utils/query-manager.tsx            |  16 +-
 .../__snapshots__/segments-view.spec.tsx.snap      |  28 ++-
 .../src/views/segments-view/segments-view.tsx      | 232 ++++++++++++++++-----
 web-console/tsconfig.json                          |   8 +-
 web-console/unified-console.html                   |  24 +--
 web-console/webpack.config.js                      |  54 ++---
 7 files changed, 276 insertions(+), 112 deletions(-)

diff --git a/web-console/script/mkcomp b/web-console/script/mkcomp
index 5634959..80bf859 100755
--- a/web-console/script/mkcomp
+++ b/web-console/script/mkcomp
@@ -44,14 +44,14 @@ fs.ensureDirSync(path);
 console.log('Making path:', path);
 
 const spaceName = name.replace(/-/g, ' ');
-const camelName = name.replace(/(^|-)[a-z]/g, (s) => s.replace('-', 
'').toUpperCase());
+const camelName = name.replace(/(^|-)[a-z]/g, s => s.replace('-', 
'').toUpperCase());
 const snakeName = camelName[0].toLowerCase() + camelName.substr(1);
 
 function writeFile(path, data) {
   try {
     return fs.writeFileSync(path, data, {
       flag: 'wx', // x = fail if file exists
-      encoding: 'utf8'
+      encoding: 'utf8',
     });
   } catch (error) {
     return console.log(`Skipping ${path}`);
@@ -59,8 +59,9 @@ function writeFile(path, data) {
 }
 
 // Make the TypeScript file
-writeFile(path + name + '.tsx',
-`/*
+writeFile(
+  path + name + '.tsx',
+  `/*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -104,11 +105,13 @@ export class ${camelName} extends 
React.PureComponent<${camelName}Props, ${camel
     </div>;
   }
 }
-`);
+`,
+);
 
 // Make the SASS file
-writeFile(path + name + '.scss',
-`/*
+writeFile(
+  path + name + '.scss',
+  `/*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -129,10 +132,12 @@ writeFile(path + name + '.scss',
 .${name} {
 
 }
-`);
+`,
+);
 
 // Make the spec test file
-writeFile(path + name + '.spec.tsx',
+writeFile(
+  path + name + '.spec.tsx',
   `/*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -165,4 +170,5 @@ describe('${spaceName}', () => {
     expect(container.firstChild).toMatchSnapshot();
   });
 });
-`);
+`,
+);
diff --git a/web-console/src/utils/query-manager.tsx 
b/web-console/src/utils/query-manager.tsx
index 1317c7b..ecf78f1 100644
--- a/web-console/src/utils/query-manager.tsx
+++ b/web-console/src/utils/query-manager.tsx
@@ -25,19 +25,23 @@ export interface QueryStateInt<R> {
 }
 
 export interface QueryManagerOptions<Q, R> {
-  processQuery: (query: Q) => Promise<R>;
+  processQuery: (query: Q, setIntermediateQuery: (intermediateQuery: any) => 
void) => Promise<R>;
   onStateChange?: (queryResolve: QueryStateInt<R>) => void;
   debounceIdle?: number;
   debounceLoading?: number;
 }
 
 export class QueryManager<Q, R> {
-  private processQuery: (query: Q) => Promise<R>;
+  private processQuery: (
+    query: Q,
+    setIntermediateQuery: (intermediateQuery: any) => void,
+  ) => Promise<R>;
   private onStateChange?: (queryResolve: QueryStateInt<R>) => void;
 
   private terminated = false;
   private nextQuery: Q | undefined;
   private lastQuery: Q | undefined;
+  private lastIntermediateQuery: any;
   private actuallyLoading = false;
   private state: QueryStateInt<R> = {
     result: null,
@@ -78,7 +82,9 @@ export class QueryManager<Q, R> {
     const myQueryId = this.currentQueryId;
 
     this.actuallyLoading = true;
-    this.processQuery(this.lastQuery).then(
+    this.processQuery(this.lastQuery, (intermediateQuery: any) => {
+      this.lastIntermediateQuery = intermediateQuery;
+    }).then(
       result => {
         if (this.currentQueryId !== myQueryId) return;
         this.actuallyLoading = false;
@@ -136,6 +142,10 @@ export class QueryManager<Q, R> {
     return this.lastQuery;
   }
 
+  public getLastIntermediateQuery(): any {
+    return this.lastIntermediateQuery;
+  }
+
   public getState(): QueryStateInt<R> {
     return this.state;
   }
diff --git 
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap 
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index 5179ad4..70e4003 100755
--- 
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++ 
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -18,6 +18,23 @@ exports[`segments-view matches snapshot 1`] = `
         onClick={[Function]}
         text="Go to SQL"
       />
+      <Component>
+        Group by
+      </Component>
+      <Blueprint3.ButtonGroup>
+        <Blueprint3.Button
+          active={true}
+          onClick={[Function]}
+        >
+          None
+        </Blueprint3.Button>
+        <Blueprint3.Button
+          active={false}
+          onClick={[Function]}
+        >
+          Interval
+        </Blueprint3.Button>
+      </Blueprint3.ButtonGroup>
       <TableColumnSelector
         columns={
           Array [
@@ -112,6 +129,14 @@ exports[`segments-view matches snapshot 1`] = `
           },
           Object {
             "Cell": [Function],
+            "Header": "Interval",
+            "accessor": "interval",
+            "defaultSortDesc": true,
+            "show": false,
+            "width": 120,
+          },
+          Object {
+            "Cell": [Function],
             "Header": "Start",
             "accessor": "start",
             "defaultSortDesc": true,
@@ -209,7 +234,7 @@ exports[`segments-view matches snapshot 1`] = `
       defaultFilterMethod={[Function]}
       defaultFiltered={Array []}
       defaultPage={0}
-      defaultPageSize={50}
+      defaultPageSize={25}
       defaultResized={Array []}
       defaultSortDesc={false}
       defaultSortMethod={[Function]}
@@ -287,6 +312,7 @@ exports[`segments-view matches snapshot 1`] = `
       }
       pageText="Page"
       pages={10000000}
+      pivotBy={Array []}
       pivotDefaults={Object {}}
       pivotIDKey="_pivotID"
       pivotValKey="_pivotVal"
diff --git a/web-console/src/views/segments-view/segments-view.tsx 
b/web-console/src/views/segments-view/segments-view.tsx
index 2a65636..33b5c2b 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Button, Intent } from '@blueprintjs/core';
+import { Button, ButtonGroup, Intent, Label } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import React from 'react';
@@ -86,11 +86,27 @@ export interface SegmentsViewState {
   terminateSegmentId: string | null;
   terminateDatasourceId: string | null;
   hiddenColumns: LocalStorageBackedArray<string>;
+  loaded: boolean;
+  groupByInterval: boolean;
+
+  // table state
+  page: number | null;
+  pageSize: number | null;
+  filtered: number[] | null;
+  sorted: Sorted | null;
+}
+
+interface Sorted {
+  id: number;
+  desc: boolean;
 }
 
-interface QueryAndSkip {
-  query: string;
-  skip: number;
+interface SegmentsQuery {
+  page: number;
+  pageSize: number;
+  filtered: Filter[];
+  sorted: Sorted[];
+  groupByInterval: boolean;
 }
 
 interface SegmentQueryResultRow {
@@ -111,7 +127,9 @@ interface SegmentQueryResultRow {
 }
 
 export class SegmentsView extends React.PureComponent<SegmentsViewProps, 
SegmentsViewState> {
-  private segmentsSqlQueryManager: QueryManager<QueryAndSkip, 
SegmentQueryResultRow[]>;
+  static PAGE_SIZE = 25;
+
+  private segmentsSqlQueryManager: QueryManager<SegmentsQuery, 
SegmentQueryResultRow[]>;
   private segmentsNoSqlQueryManager: QueryManager<null, 
SegmentQueryResultRow[]>;
 
   constructor(props: SegmentsViewProps, context: any) {
@@ -134,11 +152,87 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
       hiddenColumns: new LocalStorageBackedArray<string>(
         LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION,
       ),
+      loaded: false,
+      groupByInterval: false,
+
+      // Table state
+      page: null,
+      pageSize: null,
+      sorted: null,
+      filtered: null,
     };
 
     this.segmentsSqlQueryManager = new QueryManager({
-      processQuery: async (query: QueryAndSkip) => {
-        const results: any[] = (await queryDruidSql({ query: query.query 
})).slice(query.skip);
+      processQuery: async (query: SegmentsQuery, setIntermediateQuery) => {
+        const totalQuerySize = (query.page + 1) * query.pageSize;
+
+        const whereParts = query.filtered
+          .map((f: Filter) => {
+            if (f.id.startsWith('is_')) {
+              if (f.value === 'all') return null;
+              return `${JSON.stringify(f.id)} = ${f.value === 'true' ? 1 : 0}`;
+            } else {
+              return sqlQueryCustomTableFilter(f);
+            }
+          })
+          .filter(Boolean);
+
+        let queryParts: string[];
+        if (query.groupByInterval) {
+          queryParts = [
+            `SELECT`,
+            `  ("start" || '/' || "end") AS "interval",`,
+            `  "segment_id", "datasource", "start", "end", "size", "version", 
"partition_num", "num_replicas", "num_rows", "is_published", "is_available", 
"is_realtime", "is_overshadowed", "payload"`,
+            `FROM sys.segments`,
+            `WHERE`,
+          ];
+          if (whereParts.length) {
+            queryParts.push(whereParts.join(' AND ') + 'AND');
+          }
+          queryParts.push(
+            ` ("start" || '/' || "end") IN (SELECT "start" || '/' || "end" 
FROM sys.segments GROUP BY 1 LIMIT ${totalQuerySize})`,
+          );
+
+          if (whereParts.length) {
+            queryParts.push('AND ' + whereParts.join(' AND '));
+          }
+
+          if (query.sorted.length) {
+            queryParts.push(
+              'ORDER BY ' +
+                query.sorted
+                  .map((sort: any) => `${JSON.stringify(sort.id)} ${sort.desc 
? 'DESC' : 'ASC'}`)
+                  .join(', '),
+            );
+          }
+
+          queryParts.push(`LIMIT ${totalQuerySize * 1000}`);
+        } else {
+          queryParts = [
+            `SELECT "segment_id", "datasource", "start", "end", "size", 
"version", "partition_num", "num_replicas", "num_rows", "is_published", 
"is_available", "is_realtime", "is_overshadowed", "payload"`,
+            `FROM sys.segments`,
+          ];
+
+          if (whereParts.length) {
+            queryParts.push('WHERE ' + whereParts.join(' AND '));
+          }
+
+          if (query.sorted.length) {
+            queryParts.push(
+              'ORDER BY ' +
+                query.sorted
+                  .map((sort: any) => `${JSON.stringify(sort.id)} ${sort.desc 
? 'DESC' : 'ASC'}`)
+                  .join(', '),
+            );
+          }
+
+          queryParts.push(`LIMIT ${totalQuerySize}`);
+        }
+        const sqlQuery = queryParts.join('\n');
+        setIntermediateQuery(sqlQuery);
+        const results: any[] = (await queryDruidSql({ query: sqlQuery 
})).slice(
+          query.page * query.pageSize,
+        );
         results.forEach(result => {
           try {
             result.payload = JSON.parse(result.payload);
@@ -194,7 +288,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
       onStateChange: ({ result, loading, error }) => {
         this.setState({
           allSegments: result,
-          segments: result ? result.slice(0, 50) : null,
+          segments: result ? result.slice(0, SegmentsView.PAGE_SIZE) : null,
           segmentsLoading: loading,
           segmentsError: error,
         });
@@ -213,50 +307,19 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
     this.segmentsNoSqlQueryManager.terminate();
   }
 
-  private fetchData = (state: any) => {
-    const { page, pageSize, filtered, sorted } = state;
-    const totalQuerySize = (page + 1) * pageSize;
-
-    const queryParts = [
-      `SELECT "segment_id", "datasource", "start", "end", "size", "version", 
"partition_num", "num_replicas", "num_rows", "is_published", "is_available", 
"is_realtime", "is_overshadowed", "payload"`,
-      `FROM sys.segments`,
-    ];
-
-    const whereParts = filtered
-      .map((f: Filter) => {
-        if (f.id.startsWith('is_')) {
-          if (f.value === 'all') return null;
-          return `${JSON.stringify(f.id)} = ${f.value === 'true' ? 1 : 0}`;
-        } else {
-          return sqlQueryCustomTableFilter(f);
-        }
-      })
-      .filter(Boolean);
-
-    if (whereParts.length) {
-      queryParts.push('WHERE ' + whereParts.join(' AND '));
-    }
-
-    if (sorted.length) {
-      queryParts.push(
-        'ORDER BY ' +
-          sorted
-            .map((sort: any) => `${JSON.stringify(sort.id)} ${sort.desc ? 
'DESC' : 'ASC'}`)
-            .join(', '),
-      );
-    }
-
-    queryParts.push(`LIMIT ${totalQuerySize}`);
-
-    const query = queryParts.join('\n');
+  private fetchData = (groupByInterval: boolean, state?: any) => {
+    const { page, pageSize, filtered, sorted } = state ? state : this.state;
     this.segmentsSqlQueryManager.runQuery({
-      query,
-      skip: totalQuerySize - pageSize,
+      page,
+      pageSize,
+      filtered,
+      sorted,
+      groupByInterval: groupByInterval,
     });
   };
 
-  private fetchClientSideData = (state: any) => {
-    const { page, pageSize, filtered, sorted } = state;
+  private fetchClientSideData = (state?: any) => {
+    const { page, pageSize, filtered, sorted } = state ? state : state;
     const { allSegments } = this.state;
     if (allSegments == null) return;
     const startPage = page * pageSize;
@@ -296,7 +359,14 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
   }
 
   renderSegmentsTable() {
-    const { segments, segmentsLoading, segmentsError, segmentFilter, 
hiddenColumns } = this.state;
+    const {
+      segments,
+      segmentsLoading,
+      segmentsError,
+      segmentFilter,
+      hiddenColumns,
+      groupByInterval,
+    } = this.state;
     const { noSqlMode } = this.props;
 
     return (
@@ -314,9 +384,24 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
         onFilteredChange={filtered => {
           this.setState({ segmentFilter: filtered });
         }}
-        onFetchData={noSqlMode ? this.fetchClientSideData : this.fetchData}
+        onFetchData={
+          noSqlMode
+            ? this.fetchClientSideData
+            : state => {
+                this.setState({
+                  page: state.page,
+                  pageSize: state.pageSize,
+                  filtered: state.filtered,
+                  sorted: state.sorted,
+                });
+                if (this.segmentsSqlQueryManager.getLastQuery) {
+                  this.fetchData(groupByInterval, state);
+                }
+              }
+        }
         showPageJump={false}
         ofText=""
+        pivotBy={groupByInterval ? ['interval'] : []}
         columns={[
           {
             Header: 'Segment ID',
@@ -342,6 +427,25 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
             show: hiddenColumns.exists('Datasource'),
           },
           {
+            Header: 'Interval',
+            accessor: 'interval',
+            width: 120,
+            defaultSortDesc: true,
+            Cell: row => {
+              const value = row.value;
+              return (
+                <a
+                  onClick={() => {
+                    this.setState({ segmentFilter: addFilter(segmentFilter, 
'interval', value) });
+                  }}
+                >
+                  {value}
+                </a>
+              );
+            },
+            show: hiddenColumns.exists('interval') && groupByInterval,
+          },
+          {
             Header: 'Start',
             accessor: 'start',
             width: 120,
@@ -472,7 +576,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
             show: hiddenColumns.exists(ActionCell.COLUMN_LABEL),
           },
         ]}
-        defaultPageSize={50}
+        defaultPageSize={SegmentsView.PAGE_SIZE}
       />
     );
   }
@@ -516,7 +620,8 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
       hiddenColumns,
     } = this.state;
     const { goToQuery, noSqlMode } = this.props;
-    const lastSegmentsQuery = this.segmentsSqlQueryManager.getLastQuery();
+    const { groupByInterval } = this.state;
+    const lastSegmentsQuery = 
this.segmentsSqlQueryManager.getLastIntermediateQuery();
 
     return (
       <>
@@ -537,10 +642,31 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
                 disabled={!lastSegmentsQuery}
                 onClick={() => {
                   if (!lastSegmentsQuery) return;
-                  goToQuery(lastSegmentsQuery.query);
+                  goToQuery(lastSegmentsQuery);
                 }}
               />
             )}
+            <Label>Group by</Label>
+            <ButtonGroup>
+              <Button
+                active={!groupByInterval}
+                onClick={() => {
+                  this.setState({ groupByInterval: false });
+                  noSqlMode ? this.fetchClientSideData() : 
this.fetchData(false);
+                }}
+              >
+                None
+              </Button>
+              <Button
+                active={groupByInterval}
+                onClick={() => {
+                  this.setState({ groupByInterval: true });
+                  this.fetchData(true);
+                }}
+              >
+                Interval
+              </Button>
+            </ButtonGroup>
             <TableColumnSelector
               columns={noSqlMode ? tableColumnsNoSql : tableColumns}
               onChange={column => this.setState({ hiddenColumns: 
hiddenColumns.toggle(column) })}
diff --git a/web-console/tsconfig.json b/web-console/tsconfig.json
index 255116f..697bd76 100644
--- a/web-console/tsconfig.json
+++ b/web-console/tsconfig.json
@@ -18,13 +18,9 @@
     "moduleResolution": "node",
     "lib": ["dom", "es2016"],
     "jsx": "react",
-    "rootDirs": ["lib","src"],
+    "rootDirs": ["lib", "src"],
 
     "outDir": "build"
   },
-  "include": [
-    "src/**/*.ts",
-    "src/**/*.tsx",
-    "lib/sql-function-doc.ts"
-  ]
+  "include": ["src/**/*.ts", "src/**/*.tsx", "lib/sql-function-doc.ts"]
 }
diff --git a/web-console/unified-console.html b/web-console/unified-console.html
index 9f722af..81073f1 100644
--- a/web-console/unified-console.html
+++ b/web-console/unified-console.html
@@ -1,4 +1,4 @@
-<!doctype html>
+<!DOCTYPE html>
 <!--
   ~ Licensed to the Apache Software Foundation (ASF) under one
   ~ or more contributor license agreements.  See the NOTICE file
@@ -18,15 +18,15 @@
   ~ under the License.
   -->
 <html lang="en">
-<head>
-  <meta charset="utf-8">
-  <title>Apache Druid</title>
-  <meta name="description" content="Apache Druid console">
-  <link rel="shortcut icon" href="favicon.png">
-</head>
-<body class="bp3-dark mouse-mode">
-  <div class="app-container"></div>
-  <script src="console-config.js"></script>
-  <script src="public/web-console-0.16.0.js"></script>
-</body>
+  <head>
+    <meta charset="utf-8" />
+    <title>Apache Druid</title>
+    <meta name="description" content="Apache Druid console" />
+    <link rel="shortcut icon" href="favicon.png" />
+  </head>
+  <body class="bp3-dark mouse-mode">
+    <div class="app-container"></div>
+    <script src="console-config.js"></script>
+    <script src="public/web-console-0.16.0.js"></script>
+  </body>
 </html>
diff --git a/web-console/webpack.config.js b/web-console/webpack.config.js
index d155384..cbb75a6 100644
--- a/web-console/webpack.config.js
+++ b/web-console/webpack.config.js
@@ -29,14 +29,14 @@ function friendlyErrorFormatter(e) {
   return `${e.severity}: ${e.content} [TS${e.code}]\n    at 
(${e.file}:${e.line}:${e.character})`;
 }
 
-module.exports = (env) => {
-  let druidUrl = ((env || {}).druid_host || process.env.druid_host || 
'localhost');
+module.exports = env => {
+  let druidUrl = (env || {}).druid_host || process.env.druid_host || 
'localhost';
   if (!druidUrl.startsWith('http')) druidUrl = 'http://' + druidUrl;
   if (!/:\d+$/.test(druidUrl)) druidUrl += ':8888';
 
   const proxyTarget = {
     target: druidUrl,
-    secure: false
+    secure: false,
   };
 
   const mode = process.env.NODE_ENV === 'production' ? 'production' : 
'development';
@@ -45,17 +45,17 @@ module.exports = (env) => {
     mode: mode,
     devtool: 'hidden-source-map',
     entry: {
-      'web-console': './src/entry.ts'
+      'web-console': './src/entry.ts',
     },
     output: {
       path: path.resolve(__dirname, './public'),
       filename: `[name]-${version}.js`,
       chunkFilename: `[name]-${version}.js`,
-      publicPath: '/public'
+      publicPath: '/public',
     },
     target: 'web',
     resolve: {
-      extensions: ['.tsx', '.ts', '.html', '.js', '.json', '.scss', '.css']
+      extensions: ['.tsx', '.ts', '.html', '.js', '.json', '.scss', '.css'],
     },
     devServer: {
       publicPath: '/public',
@@ -65,8 +65,8 @@ module.exports = (env) => {
       proxy: {
         '/status': proxyTarget,
         '/druid': proxyTarget,
-        '/proxy': proxyTarget
-      }
+        '/proxy': proxyTarget,
+      },
     },
     module: {
       rules: [
@@ -79,10 +79,10 @@ module.exports = (env) => {
               options: {
                 configFile: 'tslint.json',
                 emitErrors: true,
-                fix: false // Set this to true to auto fix errors
-              }
-            }
-          ]
+                fix: false, // Set this to true to auto fix errors
+              },
+            },
+          ],
         },
         {
           test: /\.tsx?$/,
@@ -91,10 +91,10 @@ module.exports = (env) => {
             {
               loader: 'ts-loader',
               options: {
-                errorFormatter: friendlyErrorFormatter
-              }
-            }
-          ]
+                errorFormatter: friendlyErrorFormatter,
+              },
+            },
+          ],
         },
         {
           test: (ALWAYS_BABEL || mode === 'production') ? /\.m?js$/ : /^xxx$/,
@@ -105,29 +105,29 @@ module.exports = (env) => {
         {
           test: /\.s?css$/,
           use: [
-            {loader: 'style-loader'}, // creates style nodes from JS strings
-            {loader: 'css-loader'}, // translates CSS into CommonJS
+            { loader: 'style-loader' }, // creates style nodes from JS strings
+            { loader: 'css-loader' }, // translates CSS into CommonJS
             {
               loader: 'postcss-loader',
               options: {
                 ident: 'postcss',
                 plugins: () => [
                   postcssPresetEnv({
-                    browsers: ['> 1%', 'last 3 versions', 'Firefox ESR', 
'Opera 12.1']
-                  })
-                ]
-              }
+                    browsers: ['> 1%', 'last 3 versions', 'Firefox ESR', 
'Opera 12.1'],
+                  }),
+                ],
+              },
             },
-            {loader: 'sass-loader'} // compiles Sass to CSS, using Node Sass 
by default
-          ]
-        }
-      ]
+            { loader: 'sass-loader' }, // compiles Sass to CSS, using Node 
Sass by default
+          ],
+        },
+      ],
     },
     performance: {
       hints: false
     },
     plugins: [
       // new BundleAnalyzerPlugin()
-    ]
+    ],
   };
 };


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to