ryan 6 anni fa
parent
commit
2c8e1cbdb1
63 ha cambiato i file con 1037 aggiunte e 457 eliminazioni
  1. 41 38
      package.json
  2. 1 1
      packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss
  3. 16 3
      packages/grafana-ui/src/components/FormField/FormField.test.tsx
  4. 8 3
      packages/grafana-ui/src/components/FormField/FormField.tsx
  5. 19 1
      packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
  6. 38 0
      packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx
  7. 71 0
      packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx
  8. 0 2
      packages/grafana-ui/src/components/Table/Table.story.tsx
  9. 13 3
      packages/grafana-ui/src/components/Table/Table.tsx
  10. 1 1
      packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap
  11. 1 0
      packages/grafana-ui/src/components/index.ts
  12. 3 17
      packages/grafana-ui/src/types/data.ts
  13. 2 7
      packages/grafana-ui/src/types/panel.ts
  14. 0 4
      packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap
  15. 1 1
      packages/grafana-ui/src/utils/index.ts
  16. 39 1
      packages/grafana-ui/src/utils/processTableData.test.ts
  17. 40 7
      packages/grafana-ui/src/utils/processTableData.ts
  18. 31 12
      packages/grafana-ui/src/utils/processTimeSeries.ts
  19. 0 33
      packages/grafana-ui/src/utils/singlestat.ts
  20. 1 1
      packages/grafana-ui/src/utils/storybook/UseState.tsx
  21. 1 1
      pkg/infra/remotecache/database_storage.go
  22. 17 1
      pkg/infra/remotecache/database_storage_test.go
  23. 4 0
      pkg/models/org_user.go
  24. 8 1
      public/app/core/angular_wrappers.ts
  25. 3 3
      public/app/core/components/Animations/FadeIn.tsx
  26. 12 4
      public/app/core/components/Animations/SlideDown.tsx
  27. 2 2
      public/app/core/components/CopyToClipboard/CopyToClipboard.tsx
  28. 2 2
      public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx
  29. 0 1
      public/app/core/components/JSONFormatter/JSONFormatter.tsx
  30. 21 14
      public/app/core/components/form_dropdown/form_dropdown.ts
  31. 1 1
      public/app/core/components/json_explorer/json_explorer.ts
  32. 4 4
      public/app/core/components/layout_selector/layout_selector.ts
  33. 15 6
      public/app/core/services/ng_react.ts
  34. 8 7
      public/app/core/table_model.ts
  35. 1 1
      public/app/features/api-keys/ApiKeysPage.tsx
  36. 1 1
      public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
  37. 6 22
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  38. 7 7
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  39. 6 6
      public/app/features/dashboard/state/PanelModel.ts
  40. 1 18
      public/app/features/dashboard/utils/panel.ts
  41. 1 1
      public/app/features/folders/FolderPermissions.tsx
  42. 3 1
      public/app/features/playlist/playlist_srv.ts
  43. 1 1
      public/app/features/teams/TeamGroupSync.tsx
  44. 1 1
      public/app/features/teams/TeamMembers.tsx
  45. 14 0
      public/app/plugins/datasource/mssql/config_ctrl.ts
  46. 9 9
      public/app/plugins/datasource/mssql/partials/config.html
  47. 13 0
      public/app/plugins/datasource/postgres/config_ctrl.ts
  48. 9 8
      public/app/plugins/datasource/postgres/partials/config.html
  49. 1 1
      public/app/plugins/datasource/prometheus/specs/datasource.test.ts
  50. 2 2
      public/app/plugins/panel/bargauge/BarGaugePanel.tsx
  51. 2 2
      public/app/plugins/panel/gauge/GaugePanel.tsx
  52. 3 3
      public/app/plugins/panel/graph2/GraphPanel.tsx
  53. 2 2
      public/app/plugins/panel/singlestat/module.ts
  54. 16 11
      public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
  55. 2 1
      public/app/plugins/panel/table/module.ts
  56. 3 3
      public/app/plugins/panel/table2/TablePanel.tsx
  57. 0 0
      public/app/types/jquery/jquery.d.ts
  58. 1 2
      scripts/webpack/sass.rule.js
  59. 2 9
      scripts/webpack/webpack.dev.js
  60. 2 2
      scripts/webpack/webpack.hot.js
  61. 4 4
      scripts/webpack/webpack.prod.js
  62. 1 1
      tsconfig.json
  63. 499 156
      yarn.lock

+ 41 - 38
package.json

@@ -11,11 +11,11 @@
     "url": "http://github.com/grafana/grafana.git"
   },
   "devDependencies": {
-    "@babel/core": "^7.1.2",
+    "@babel/core": "^7.3.4",
     "@babel/plugin-syntax-dynamic-import": "^7.0.0",
-    "@babel/preset-env": "^7.1.0",
+    "@babel/preset-env": "^7.3.4",
     "@babel/preset-react": "^7.0.0",
-    "@babel/preset-typescript": "^7.1.0",
+    "@babel/preset-typescript": "^7.3.3",
     "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
     "@types/angular": "^1.6.6",
     "@types/chalk": "^2.2.0",
@@ -24,26 +24,28 @@
     "@types/d3": "^4.10.1",
     "@types/enzyme": "^3.1.13",
     "@types/inquirer": "^0.0.43",
-    "@types/jest": "^23.3.2",
+    "@types/jest": "^24.0.11",
     "@types/jquery": "^1.10.35",
     "@types/node": "^8.0.31",
+    "@types/papaparse": "^4.5.9",
     "@types/react": "^16.8.8",
     "@types/react-dom": "^16.8.2",
     "@types/react-grid-layout": "^0.16.6",
     "@types/react-select": "^2.0.4",
     "@types/react-transition-group": "^2.0.15",
     "@types/react-virtualized": "^9.18.12",
+    "@types/clipboard": "^2.0.1",
     "angular-mocks": "1.6.6",
-    "autoprefixer": "^6.4.0",
-    "axios": "^0.17.1",
+    "autoprefixer": "^9.4.10",
+    "axios": "^0.18.0",
     "babel-core": "^7.0.0-bridge",
-    "babel-jest": "^23.6.0",
+    "babel-jest": "^24.5.0",
     "babel-loader": "^8.0.4",
-    "babel-plugin-angularjs-annotate": "^0.9.0",
+    "babel-plugin-angularjs-annotate": "^0.10.0",
     "chalk": "^2.4.2",
-    "clean-webpack-plugin": "^0.1.19",
+    "clean-webpack-plugin": "^2.0.0",
     "concurrently": "^4.1.0",
-    "css-loader": "^0.28.7",
+    "css-loader": "^2.1.1",
     "enzyme": "^3.6.0",
     "enzyme-adapter-react-16": "^1.5.0",
     "enzyme-to-json": "^3.3.4",
@@ -51,11 +53,11 @@
     "es6-shim": "^0.35.3",
     "execa": "^1.0.0",
     "expect.js": "~0.2.0",
-    "expose-loader": "^0.7.3",
-    "file-loader": "^1.1.11",
-    "fork-ts-checker-webpack-plugin": "^0.4.9",
+    "expose-loader": "0.7.5",
+    "file-loader": "^3.0.1",
+    "fork-ts-checker-webpack-plugin": "^1.0.0",
     "gaze": "^1.1.2",
-    "glob": "~7.0.0",
+    "glob": "~7.1.3",
     "grunt": "1.0.1",
     "grunt-angular-templates": "^1.1.0",
     "grunt-cli": "~1.2.0",
@@ -69,29 +71,29 @@
     "grunt-sass-lint": "^0.2.4",
     "grunt-usemin": "3.1.1",
     "grunt-webpack": "^3.0.2",
-    "html-loader": "^0.5.1",
-    "html-webpack-harddisk-plugin": "^0.2.0",
+    "html-loader": "0.5.5",
+    "html-webpack-harddisk-plugin": "^1.0.1",
     "html-webpack-plugin": "^3.2.0",
     "husky": "^1.3.1",
     "inquirer": "^6.2.2",
-    "jest": "^23.6.0",
+    "jest": "^24.5.0",
     "jest-date-mock": "^1.0.6",
     "lint-staged": "^8.1.3",
     "load-grunt-tasks": "3.5.2",
-    "mini-css-extract-plugin": "^0.4.0",
+    "mini-css-extract-plugin": "^0.5.0",
     "mocha": "^4.0.1",
     "monaco-editor": "^0.15.6",
     "ng-annotate-loader": "^0.6.1",
     "ng-annotate-webpack-plugin": "^0.3.0",
     "ngtemplate-loader": "^2.0.1",
     "node-sass": "^4.11.0",
-    "npm": "^5.4.2",
-    "optimize-css-assets-webpack-plugin": "^4.0.2",
-    "ora": "^3.1.0",
+    "npm": "^6.9.0",
+    "optimize-css-assets-webpack-plugin": "^5.0.1",
+    "ora": "^3.2.0",
     "phantomjs-prebuilt": "^2.1.15",
     "postcss-browser-reporter": "^0.5.0",
-    "postcss-loader": "^2.0.6",
-    "postcss-reporter": "^5.0.0",
+    "postcss-loader": "^3.0.0",
+    "postcss-reporter": "^6.0.1",
     "prettier": "1.16.4",
     "react-hot-loader": "^4.3.6",
     "react-test-renderer": "^16.5.0",
@@ -99,27 +101,27 @@
     "regexp-replace-loader": "^1.0.1",
     "rimraf": "^2.6.3",
     "sass-lint": "^1.10.2",
-    "sass-loader": "^7.0.1",
+    "sass-loader": "7.1.0",
     "semver": "^5.6.0",
     "sinon": "1.17.6",
-    "style-loader": "^0.21.0",
+    "style-loader": "0.23.1",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
-    "ts-jest": "^23.10.4",
-    "ts-loader": "^5.1.0",
-    "ts-node": "^8.0.2",
-    "tslib": "^1.9.3",
-    "tslint": "^5.8.0",
+    "terser-webpack-plugin": "^1.2.3",
+    "ts-jest": "^24.0.0",
+    "ts-loader": "5.3.3",
+    "ts-node": "8.0.2",
+    "tslib": "1.9.3",
+    "tslint": "5.14.0",
     "tslint-loader": "^3.5.3",
     "tslint-react": "^3.6.0",
-    "typescript": "^3.0.3",
-    "uglifyjs-webpack-plugin": "^1.2.7",
-    "webpack": "4.19.1",
-    "webpack-bundle-analyzer": "^2.9.0",
+    "typescript": "3.3.3333",
+    "webpack": "4.29.6",
+    "webpack-bundle-analyzer": "3.1.0",
     "webpack-cleanup-plugin": "^0.5.1",
-    "webpack-cli": "^2.1.4",
-    "webpack-dev-server": "^3.1.0",
-    "webpack-merge": "^4.1.0",
+    "webpack-cli": "3.2.3",
+    "webpack-dev-server": "3.2.1",
+    "webpack-merge": "4.2.1",
     "zone.js": "^0.7.2"
   },
   "scripts": {
@@ -176,7 +178,7 @@
     "baron": "^3.0.3",
     "brace": "^0.10.0",
     "classnames": "^2.2.6",
-    "clipboard": "^1.7.1",
+    "clipboard": "^2.0.4",
     "d3": "^4.11.0",
     "d3-scale-chromatic": "^1.3.0",
     "eventemitter3": "^2.0.3",
@@ -188,6 +190,7 @@
     "mousetrap": "^1.6.0",
     "mousetrap-global-bind": "^1.1.0",
     "nodemon": "^1.18.10",
+    "papaparse": "^4.6.3",
     "prismjs": "^1.6.0",
     "prop-types": "^15.6.2",
     "rc-cascader": "^0.14.0",

+ 1 - 1
packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss

@@ -45,7 +45,7 @@ $arrowSize: 15px;
     border-right-color: transparent;
     border-top-color: transparent;
     top: 0;
-    left: calc(100% -$arrowSize);
+    left: calc(100%-#{$arrowSize});
   }
 
   &[data-placement^='right'] {

+ 16 - 3
packages/grafana-ui/src/components/FormField/FormField.test.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import { FormField, Props } from './FormField';
 
-const setup = (propOverrides?: object) => {
+const setup = (propOverrides?: Partial<Props>) => {
   const props: Props = {
     label: 'Test',
     labelWidth: 11,
@@ -15,10 +15,23 @@ const setup = (propOverrides?: object) => {
   return shallow(<FormField {...props} />);
 };
 
-describe('Render', () => {
-  it('should render component', () => {
+describe('FormField', () => {
+  it('should render component with default inputEl', () => {
     const wrapper = setup();
 
     expect(wrapper).toMatchSnapshot();
   });
+
+  it('should render component with custom inputEl', () => {
+    const wrapper = setup({
+      inputEl: (
+        <>
+          <span>Input</span>
+          <button>Ok</button>
+        </>
+      ),
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
 });

+ 8 - 3
packages/grafana-ui/src/components/FormField/FormField.tsx

@@ -5,6 +5,7 @@ export interface Props extends InputHTMLAttributes<HTMLInputElement> {
   label: string;
   labelWidth?: number;
   inputWidth?: number;
+  inputEl?: React.ReactNode;
 }
 
 const defaultProps = {
@@ -12,14 +13,18 @@ const defaultProps = {
   inputWidth: 12,
 };
 
-const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => {
+/**
+ * Default form field including label used in Grafana UI. Default input element is simple <input />. You can also pass
+ * custom inputEl if required in which case inputWidth and inputProps are ignored.
+ */
+export const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, inputEl, ...inputProps }) => {
   return (
     <div className="form-field">
       <FormLabel width={labelWidth}>{label}</FormLabel>
-      <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />
+      {inputEl || <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />}
     </div>
   );
 };
 
+FormField.displayName = 'FormField';
 FormField.defaultProps = defaultProps;
-export { FormField };

+ 19 - 1
packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap

@@ -1,6 +1,24 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Render should render component 1`] = `
+exports[`FormField should render component with custom inputEl 1`] = `
+<div
+  className="form-field"
+>
+  <Component
+    width={11}
+  >
+    Test
+  </Component>
+  <span>
+    Input
+  </span>
+  <button>
+    Ok
+  </button>
+</div>
+`;
+
+exports[`FormField should render component with default inputEl 1`] = `
 <div
   className="form-field"
 >

+ 38 - 0
packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx

@@ -0,0 +1,38 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+import { boolean } from '@storybook/addon-knobs';
+
+import { SecretFormField } from './SecretFormField';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { UseState } from '../../utils/storybook/UseState';
+
+const SecretFormFieldStories = storiesOf('UI/SecretFormField/SecretFormField', module);
+
+SecretFormFieldStories.addDecorator(withCenteredStory);
+const getSecretFormFieldKnobs = () => {
+  return {
+    isConfigured: boolean('Set configured state', false),
+  };
+};
+
+SecretFormFieldStories.add('default', () => {
+  const knobs = getSecretFormFieldKnobs();
+  return (
+    <UseState initialState="Input value">
+      {(value, setValue) => (
+        <SecretFormField
+          label={'Secret field'}
+          labelWidth={10}
+          value={value}
+          isConfigured={knobs.isConfigured}
+          onChange={e => setValue(e.currentTarget.value)}
+          onReset={() => {
+            action('Value was reset')('');
+            setValue('');
+          }}
+        />
+      )}
+    </UseState>
+  );
+});

+ 71 - 0
packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx

@@ -0,0 +1,71 @@
+import { omit } from 'lodash';
+import React, { InputHTMLAttributes, FunctionComponent } from 'react';
+import { FormField } from '..';
+
+interface Props extends InputHTMLAttributes<HTMLInputElement> {
+  // Function to use when reset is clicked. Means you have to reset the input value yourself as this is  uncontrolled
+  // component (or do something else if required).
+  onReset: () => void;
+  isConfigured: boolean;
+
+  label?: string;
+  labelWidth?: number;
+  inputWidth?: number;
+  // Placeholder of the input field when in non configured state.
+  placeholder?: string;
+}
+
+const defaultProps = {
+  inputWidth: 12,
+  placeholder: 'Password',
+  label: 'Password',
+};
+
+/**
+ * Form field that has 2 states configured and not configured. If configured it will not show its contents and adds
+ * a reset button that will clear the input and makes it accessible. In non configured state it behaves like normal
+ * form field. This is used for passwords or anything that is encrypted on the server and is later returned encrypted
+ * to the user (like datasource passwords).
+ */
+export const SecretFormField: FunctionComponent<Props> = ({
+  label,
+  labelWidth,
+  inputWidth,
+  onReset,
+  isConfigured,
+  placeholder,
+  ...inputProps
+}: Props) => {
+  return (
+    <FormField
+      label={label!}
+      labelWidth={labelWidth}
+      inputEl={
+        isConfigured ? (
+          <>
+            <input
+              type="text"
+              className={`gf-form-input width-${inputWidth! - 2}`}
+              disabled={true}
+              value="configured"
+              {...omit(inputProps, 'value')}
+            />
+            <button className="btn btn-secondary gf-form-btn" onClick={onReset}>
+              reset
+            </button>
+          </>
+        ) : (
+          <input
+            type="password"
+            className={`gf-form-input width-${inputWidth}`}
+            placeholder={placeholder}
+            {...inputProps}
+          />
+        )
+      }
+    />
+  );
+};
+
+SecretFormField.defaultProps = defaultProps;
+SecretFormField.displayName = 'SecretFormField';

+ 0 - 2
packages/grafana-ui/src/components/Table/Table.story.tsx

@@ -40,8 +40,6 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
       const suffix = (rowId + 1).toString();
       return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
     }),
-    type: 'table',
-    columnMap: {},
   };
 }
 

+ 13 - 3
packages/grafana-ui/src/components/Table/Table.tsx

@@ -8,6 +8,7 @@ import {
   CellMeasurerCache,
   CellMeasurer,
   GridCellProps,
+  Index,
 } from 'react-virtualized';
 import { Themeable } from '../../types/theme';
 
@@ -26,6 +27,7 @@ import { stringToJsRegex } from '../../utils/index';
 export interface Props extends Themeable {
   data: TableData;
 
+  minColumnWidth: number;
   showHeader: boolean;
   fixedHeader: boolean;
   fixedColumns: number;
@@ -46,6 +48,7 @@ interface State {
 
 interface ColumnRenderInfo {
   header: string;
+  width: number;
   builder: TableCellBuilder;
 }
 
@@ -64,6 +67,7 @@ export class Table extends Component<Props, State> {
     fixedHeader: true,
     fixedColumns: 0,
     rotate: false,
+    minColumnWidth: 150,
   };
 
   constructor(props: Props) {
@@ -76,7 +80,7 @@ export class Table extends Component<Props, State> {
     this.renderer = this.initColumns(props);
     this.measurer = new CellMeasurerCache({
       defaultHeight: 30,
-      defaultWidth: 150,
+      fixedWidth: true,
     });
   }
 
@@ -110,7 +114,8 @@ export class Table extends Component<Props, State> {
 
   /** Given the configuration, setup how each column gets rendered */
   initColumns(props: Props): ColumnRenderInfo[] {
-    const { styles, data } = props;
+    const { styles, data, width, minColumnWidth } = props;
+    const columnWidth = Math.max(width / data.columns.length, minColumnWidth);
 
     return data.columns.map((col, index) => {
       let title = col.text;
@@ -131,6 +136,7 @@ export class Table extends Component<Props, State> {
 
       return {
         header: title,
+        width: columnWidth,
         builder: getCellBuilder(col, style, this.props),
       };
     });
@@ -228,6 +234,10 @@ export class Table extends Component<Props, State> {
     );
   };
 
+  getColumnWidth = (col: Index): number => {
+    return this.renderer[col.index].width;
+  };
+
   render() {
     const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
     const { data } = this.state;
@@ -269,7 +279,7 @@ export class Table extends Component<Props, State> {
         rowCount={rowCount}
         overscanColumnCount={8}
         overscanRowCount={8}
-        columnWidth={this.measurer.columnWidth}
+        columnWidth={this.getColumnWidth}
         deferredMeasurementCache={this.measurer}
         cellRenderer={this.cellRenderer}
         rowHeight={this.measurer.rowHeight}

+ 1 - 1
packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap

@@ -17,7 +17,7 @@ exports[`Render should render with base threshold 1`] = `
       ],
       "results": Array [
         Object {
-          "isThrow": false,
+          "type": "return",
           "value": undefined,
         },
       ],

+ 1 - 0
packages/grafana-ui/src/components/index.ts

@@ -14,6 +14,7 @@ export { default as resetSelectStyles } from './Select/resetSelectStyles';
 // Forms
 export { FormLabel } from './FormLabel/FormLabel';
 export { FormField } from './FormField/FormField';
+export { SecretFormField } from './SecretFormFied/SecretFormField';
 
 export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
 export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';

+ 3 - 17
packages/grafana-ui/src/types/data.ts

@@ -51,27 +51,13 @@ export enum NullValueMode {
 export type TimeSeriesVMs = TimeSeriesVM[];
 
 export interface Column {
-  text: string;
-  title?: string;
-  type?: string;
-  sort?: boolean;
-  desc?: boolean;
-  filterable?: boolean;
+  text: string; // The column name
+  type?: 'time' | 'number' | 'string' | 'object'; // not used anywhere? can we remove?
+  filterable?: boolean; // currently only set by elasticsearch, and used in the table panel
   unit?: string;
 }
 
 export interface TableData {
   columns: Column[];
   rows: any[];
-  type: string;
-  columnMap: any;
-}
-
-export type SingleStatValue = number | string | null;
-
-/*
- * So we can add meta info like tags & series name
- */
-export interface SingleStatValueInfo {
-  value: SingleStatValue;
 }

+ 2 - 7
packages/grafana-ui/src/types/panel.ts

@@ -1,12 +1,12 @@
 import { ComponentClass } from 'react';
-import { TimeSeries, LoadingState, TableData } from './data';
+import { LoadingState, TableData } from './data';
 import { TimeRange } from './time';
 import { ScopedVars } from './datasource';
 
 export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
 
 export interface PanelProps<T = any> {
-  panelData: PanelData;
+  data?: TableData[];
   timeRange: TimeRange;
   loading: LoadingState;
   options: T;
@@ -16,11 +16,6 @@ export interface PanelProps<T = any> {
   replaceVariables: InterpolateFunction;
 }
 
-export interface PanelData {
-  timeSeries?: TimeSeries[];
-  tableData?: TableData;
-}
-
 export interface PanelEditorProps<T = any> {
   options: T;
   onOptionsChange: (options: T) => void;

+ 0 - 4
packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap

@@ -2,7 +2,6 @@
 
 exports[`processTableData basic processing should generate a header and fix widths 1`] = `
 Object {
-  "columnMap": Object {},
   "columns": Array [
     Object {
       "text": "Column 1",
@@ -31,13 +30,11 @@ Object {
       null,
     ],
   ],
-  "type": "table",
 }
 `;
 
 exports[`processTableData basic processing should read header and two rows 1`] = `
 Object {
-  "columnMap": Object {},
   "columns": Array [
     Object {
       "text": "a",
@@ -61,6 +58,5 @@ Object {
       6,
     ],
   ],
-  "type": "table",
 }
 `;

+ 1 - 1
packages/grafana-ui/src/utils/index.ts

@@ -1,5 +1,5 @@
 export * from './processTimeSeries';
-export * from './singlestat';
+export * from './processTableData';
 export * from './valueFormats/valueFormats';
 export * from './colors';
 export * from './namedColorsPalette';

+ 39 - 1
packages/grafana-ui/src/utils/processTableData.test.ts

@@ -1,4 +1,4 @@
-import { parseCSV } from './processTableData';
+import { parseCSV, toTableData } from './processTableData';
 
 describe('processTableData', () => {
   describe('basic processing', () => {
@@ -18,3 +18,41 @@ describe('processTableData', () => {
     });
   });
 });
+
+describe('toTableData', () => {
+  it('converts timeseries to table skipping nulls', () => {
+    const input1 = {
+      target: 'Field Name',
+      datapoints: [[100, 1], [200, 2]],
+    };
+    const input2 = {
+      // without target
+      target: '',
+      datapoints: [[100, 1], [200, 2]],
+    };
+    const data = toTableData([null, input1, input2, null, null]);
+    expect(data.length).toBe(2);
+    expect(data[0].columns[0].text).toBe(input1.target);
+    expect(data[0].rows).toBe(input1.datapoints);
+
+    // Default name
+    expect(data[1].columns[0].text).toEqual('Value');
+  });
+
+  it('keeps tableData unchanged', () => {
+    const input = {
+      columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
+      rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
+    };
+    const data = toTableData([null, input, null, null]);
+    expect(data.length).toBe(1);
+    expect(data[0]).toBe(input);
+  });
+
+  it('supports null values OK', () => {
+    expect(toTableData([null, null, null, null])).toEqual([]);
+    expect(toTableData(undefined)).toEqual([]);
+    expect(toTableData((null as unknown) as any[])).toEqual([]);
+    expect(toTableData([])).toEqual([]);
+  });
+});

+ 40 - 7
packages/grafana-ui/src/utils/processTableData.ts

@@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
 import Papa, { ParseError, ParseMeta } from 'papaparse';
 
 // Types
-import { TableData, Column } from '../types';
+import { TableData, Column, TimeSeries } from '../types';
 
 // Subset of all parse options
 export interface TableParseOptions {
@@ -70,8 +70,6 @@ export function matchRowSizes(table: TableData): TableData {
   return {
     columns,
     rows: fixedRows,
-    type: table.type,
-    columnMap: table.columnMap,
   };
 }
 
@@ -118,8 +116,6 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
     return {
       columns: [],
       rows: [],
-      type: 'table',
-      columnMap: {},
     };
   }
 
@@ -130,11 +126,48 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
   return matchRowSizes({
     columns: makeColumns(header),
     rows: results.data,
-    type: 'table',
-    columnMap: {},
   });
 }
 
+function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
+  return {
+    columns: [
+      {
+        text: timeSeries.target || 'Value',
+        unit: timeSeries.unit,
+      },
+      {
+        text: 'Time',
+        type: 'time',
+        unit: 'dateTimeAsIso',
+      },
+    ],
+    rows: timeSeries.datapoints,
+  };
+}
+
+export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
+
+export const toTableData = (results?: any[]): TableData[] => {
+  if (!results) {
+    return [];
+  }
+
+  return results
+    .filter(d => !!d)
+    .map(data => {
+      if (data.hasOwnProperty('columns')) {
+        return data as TableData;
+      }
+      if (data.hasOwnProperty('datapoints')) {
+        return convertTimeSeriesToTableData(data);
+      }
+      // TODO, try to convert JSON to table?
+      console.warn('Can not convert', data);
+      throw new Error('Unsupported data format');
+    });
+};
+
 export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
   if (isNumber(sortIndex)) {
     const copy = {

+ 31 - 12
packages/grafana-ui/src/utils/processTimeSeries.ts

@@ -4,24 +4,43 @@ import isNumber from 'lodash/isNumber';
 import { colors } from './colors';
 
 // Types
-import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
 import { getFlotPairs } from './flotPairs';
+import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types';
 
 interface Options {
-  timeSeries: TimeSeries[];
+  data: TableData[];
+  xColumn?: number; // Time (or null to guess)
+  yColumn?: number; // Value (or null to guess)
   nullValueMode: NullValueMode;
 }
 
-export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
-  const vmSeries = timeSeries.map((item, index) => {
+// NOTE: this should move to processTableData.ts
+// I left it as is so the merge changes are more clear.
+export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs {
+  const vmSeries = data.map((item, index) => {
+    if (!isNumber(xColumn)) {
+      xColumn = 1; // Default timeseries colum.  TODO, find first time field!
+    }
+    if (!isNumber(yColumn)) {
+      yColumn = 0; // TODO, find first non-time field
+    }
+
+    // TODO? either % or throw error?
+    if (xColumn >= item.columns.length) {
+      throw new Error('invalid colum: ' + xColumn);
+    }
+    if (yColumn >= item.columns.length) {
+      throw new Error('invalid colum: ' + yColumn);
+    }
+
     const colorIndex = index % colors.length;
-    const label = item.target;
+    const label = item.columns[yColumn].text;
 
     // Use external calculator just to make sure it works :)
     const result = getFlotPairs({
-      rows: item.datapoints,
-      xIndex: 1,
-      yIndex: 0,
+      rows: item.rows,
+      xIndex: xColumn,
+      yIndex: yColumn,
       nullValueMode,
     });
 
@@ -50,9 +69,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
     let previousValue = 0;
     let previousDeltaUp = true;
 
-    for (let i = 0; i < item.datapoints.length; i++) {
-      currentValue = item.datapoints[i][0];
-      currentTime = item.datapoints[i][1];
+    for (let i = 0; i < item.rows.length; i++) {
+      currentValue = item.rows[i][yColumn];
+      currentTime = item.rows[i][xColumn];
 
       if (typeof currentTime !== 'number') {
         continue;
@@ -103,7 +122,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
           if (previousValue > currentValue) {
             // counter reset
             previousDeltaUp = false;
-            if (i === item.datapoints.length - 1) {
+            if (i === item.rows.length - 1) {
               // reset on last
               delta += currentValue;
             }

+ 0 - 33
packages/grafana-ui/src/utils/singlestat.ts

@@ -1,33 +0,0 @@
-import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
-import { processTimeSeries } from './processTimeSeries';
-
-export interface SingleStatProcessingOptions {
-  panelData: PanelData;
-  stat: string;
-}
-
-//
-// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
-//
-export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
-  const { panelData, stat } = options;
-
-  if (panelData.timeSeries) {
-    const timeSeries = processTimeSeries({
-      timeSeries: panelData.timeSeries,
-      nullValueMode: NullValueMode.Null,
-    });
-
-    return timeSeries.map((series, index) => {
-      const value = stat !== 'name' ? series.stats[stat] : series.label;
-
-      return {
-        value: value,
-      };
-    });
-  } else if (panelData.tableData) {
-    throw { message: 'Panel data not supported' };
-  }
-
-  return [];
-}

+ 1 - 1
packages/grafana-ui/src/utils/storybook/UseState.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 interface StateHolderProps<T> {
   initialState: T;
-  children: (currentState: T, updateState: (nextState: T) => void) => JSX.Element;
+  children: (currentState: T, updateState: (nextState: T) => void) => React.ReactNode;
 }
 
 export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T; initialState: T }> {

+ 1 - 1
pkg/infra/remotecache/database_storage.go

@@ -101,7 +101,7 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration
 
 	// insert or update depending on if item already exist
 	if has {
-		sql := `UPDATE cache_data SET data=?, created=?, expire=? WHERE cache_key='?'`
+		sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
 		_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
 	} else {
 		sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`

+ 17 - 1
pkg/infra/remotecache/database_storage_test.go

@@ -5,7 +5,6 @@ import (
 	"time"
 
 	"github.com/bmizerany/assert"
-
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/services/sqlstore"
 )
@@ -54,3 +53,20 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) {
 	_, err = db.Get("key5")
 	assert.Equal(t, err, nil)
 }
+
+func TestSecondSet(t *testing.T) {
+	var err error
+	sqlstore := sqlstore.InitTestDB(t)
+
+	db := &databaseCache{
+		SQLStore: sqlstore,
+		log:      log.New("remotecache.database"),
+	}
+
+	obj := &CacheableStruct{String: "hey!"}
+
+	err = db.Set("killa-gorilla", obj, 0)
+	err = db.Set("killa-gorilla", obj, 0)
+
+	assert.Equal(t, err, nil)
+}

+ 4 - 0
pkg/models/org_user.go

@@ -36,6 +36,10 @@ func (r RoleType) Includes(other RoleType) bool {
 		return other != ROLE_ADMIN
 	}
 
+	if r == ROLE_VIEWER {
+		return other == ROLE_VIEWER
+	}
+
 	return false
 }
 

+ 8 - 1
public/app/core/angular_wrappers.ts

@@ -9,7 +9,7 @@ import { TagFilter } from './components/TagFilter/TagFilter';
 import { SideMenu } from './components/sidemenu/SideMenu';
 import { MetricSelect } from './components/Select/MetricSelect';
 import AppNotificationList from './components/AppNotifications/AppNotificationList';
-import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
+import { ColorPicker, SeriesColorPickerPopoverWithTheme, SecretFormField } from '@grafana/ui';
 import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
 
 export function registerAngularDirectives() {
@@ -59,4 +59,11 @@ export function registerAngularDirectives() {
     ['datasource', { watchDepth: 'reference' }],
     ['templateSrv', { watchDepth: 'reference' }],
   ]);
+  react2AngularDirective('secretFormField', SecretFormField, [
+    'value',
+    'isConfigured',
+    'inputWidth',
+    ['onReset', { watchDepth: 'reference', wrapApply: true }],
+    ['onChange', { watchDepth: 'reference', wrapApply: true }],
+  ]);
 }

+ 3 - 3
public/app/core/components/Animations/FadeIn.tsx

@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import React, { FC, CSSProperties } from 'react';
 import Transition, { ExitHandler } from 'react-transition-group/Transition';
 
 interface Props {
@@ -10,12 +10,12 @@ interface Props {
 }
 
 export const FadeIn: FC<Props> = props => {
-  const defaultStyle = {
+  const defaultStyle: CSSProperties = {
     transition: `opacity ${props.duration}ms linear`,
     opacity: 0,
   };
 
-  const transitionStyles = {
+  const transitionStyles: { [str: string]: CSSProperties } = {
     exited: { opacity: 0, display: 'none' },
     entering: { opacity: 0 },
     entered: { opacity: 1 },

+ 12 - 4
public/app/core/components/Animations/SlideDown.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { CSSProperties, FC } from 'react';
 import Transition from 'react-transition-group/Transition';
 
 interface Style {
@@ -16,11 +16,18 @@ export const defaultStyle: Style = {
   overflow: 'hidden',
 };
 
-export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
+export interface Props {
+  children: React.ReactNode;
+  in: boolean;
+  maxHeight?: number;
+  style?: CSSProperties;
+}
+
+export const SlideDown: FC<Props> = ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
   // There are 4 main states a Transition can be in:
   // ENTERING, ENTERED, EXITING, EXITED
-  // https://reactcommunity.org/react-transition-group/
-  const transitionStyles = {
+  // https://reactcommunity.or[g/react-transition-group/
+  const transitionStyles: { [str: string]: CSSProperties } = {
     exited: { maxHeight: 0 },
     entering: { maxHeight: maxHeight },
     entered: { maxHeight: 'unset', overflow: 'visible' },
@@ -34,6 +41,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de
           style={{
             ...style,
             ...transitionStyles[state],
+            inProp,
           }}
         >
           {children}

+ 2 - 2
public/app/core/components/CopyToClipboard/CopyToClipboard.tsx

@@ -11,10 +11,10 @@ interface Props {
 }
 
 export class CopyToClipboard extends PureComponent<Props> {
-  clipboardjs: any;
+  clipboardjs: ClipboardJS;
   myRef: any;
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     this.myRef = React.createRef();
   }

+ 2 - 2
public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import renderer from 'react-test-renderer';
+import { shallow } from 'enzyme';
 import EmptyListCTA from './EmptyListCTA';
 
 const model = {
@@ -16,7 +16,7 @@ const model = {
 
 describe('EmptyListCTA', () => {
   it('renders correctly', () => {
-    const tree = renderer.create(<EmptyListCTA model={model} />).toJSON();
+    const tree = shallow(<EmptyListCTA model={model} />);
     expect(tree).toMatchSnapshot();
   });
 });

+ 0 - 1
public/app/core/components/JSONFormatter/JSONFormatter.tsx

@@ -1,5 +1,4 @@
 import React, { PureComponent, createRef } from 'react';
-// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js';
 import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
 
 interface Props {

+ 21 - 14
public/app/core/components/form_dropdown/form_dropdown.ts

@@ -1,7 +1,8 @@
 import _ from 'lodash';
 import coreModule from '../../core_module';
+import { ISCEService, IQService } from 'angular';
 
-function typeaheadMatcher(this: any, item) {
+function typeaheadMatcher(this: any, item: string) {
   let str = this.query;
   if (str === '') {
     return true;
@@ -16,8 +17,8 @@ function typeaheadMatcher(this: any, item) {
 }
 
 export class FormDropdownCtrl {
-  inputElement: any;
-  linkElement: any;
+  inputElement: JQLite;
+  linkElement: JQLite;
   model: any;
   display: any;
   text: any;
@@ -37,7 +38,13 @@ export class FormDropdownCtrl {
   debounce: number;
 
   /** @ngInject */
-  constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
+  constructor(
+    private $scope: any,
+    $element: JQLite,
+    private $sce: ISCEService,
+    private templateSrv: any,
+    private $q: IQService
+  ) {
     this.inputElement = $element.find('input').first();
     this.linkElement = $element.find('a').first();
     this.linkMode = true;
@@ -99,7 +106,7 @@ export class FormDropdownCtrl {
     }
   }
 
-  getOptionsInternal(query) {
+  getOptionsInternal(query: string) {
     const result = this.getOptions({ $query: query });
     if (this.isPromiseLike(result)) {
       return result;
@@ -107,7 +114,7 @@ export class FormDropdownCtrl {
     return this.$q.when(result);
   }
 
-  isPromiseLike(obj) {
+  isPromiseLike(obj: any) {
     return obj && typeof obj.then === 'function';
   }
 
@@ -117,7 +124,7 @@ export class FormDropdownCtrl {
     } else {
       // if we have text use it
       if (this.lookupText) {
-        this.getOptionsInternal('').then(options => {
+        this.getOptionsInternal('').then((options: any) => {
           const item = _.find(options, { value: this.model });
           this.updateDisplay(item ? item.text : this.model);
         });
@@ -127,12 +134,12 @@ export class FormDropdownCtrl {
     }
   }
 
-  typeaheadSource(query, callback) {
-    this.getOptionsInternal(query).then(options => {
+  typeaheadSource(query: string, callback: (res: any) => void) {
+    this.getOptionsInternal(query).then((options: any) => {
       this.optionCache = options;
 
       // extract texts
-      const optionTexts = _.map(options, op => {
+      const optionTexts = _.map(options, (op: any) => {
         return _.escape(op.text);
       });
 
@@ -147,7 +154,7 @@ export class FormDropdownCtrl {
     });
   }
 
-  typeaheadUpdater(text) {
+  typeaheadUpdater(text: string) {
     if (text === this.text) {
       clearTimeout(this.cancelBlur);
       this.inputElement.focus();
@@ -159,7 +166,7 @@ export class FormDropdownCtrl {
     return text;
   }
 
-  switchToLink(fromClick) {
+  switchToLink(fromClick: boolean) {
     if (this.linkMode && !fromClick) {
       return;
     }
@@ -178,7 +185,7 @@ export class FormDropdownCtrl {
     this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200);
   }
 
-  updateValue(text) {
+  updateValue(text: string) {
     text = _.unescape(text);
 
     if (text === '' || this.text === text) {
@@ -214,7 +221,7 @@ export class FormDropdownCtrl {
     });
   }
 
-  updateDisplay(text) {
+  updateDisplay(text: string) {
     this.text = text;
     this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text));
   }

+ 1 - 1
public/app/core/components/json_explorer/json_explorer.ts

@@ -232,7 +232,7 @@ export class JsonExplorer {
 
     // some pretty handling of number arrays
     if (this.isNumberArray()) {
-      this.json.forEach((val, index) => {
+      this.json.forEach((val: any, index: number) => {
         if (index > 0) {
           arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ','));
         }

+ 4 - 4
public/app/core/components/layout_selector/layout_selector.ts

@@ -16,7 +16,7 @@ export class LayoutSelectorCtrl {
   mode: string;
 
   /** @ngInject */
-  constructor(private $rootScope) {
+  constructor(private $rootScope: any) {
     this.mode = store.get('grafana.list.layout.mode') || 'grid';
   }
 
@@ -46,18 +46,18 @@ export function layoutSelector() {
 }
 
 /** @ngInject */
-export function layoutMode($rootScope) {
+export function layoutMode($rootScope: any) {
   return {
     restrict: 'A',
     scope: {},
-    link: (scope, elem) => {
+    link: (scope: any, elem: any) => {
       const layout = store.get('grafana.list.layout.mode') || 'grid';
       let className = 'card-list-layout-' + layout;
       elem.addClass(className);
 
       $rootScope.onAppEvent(
         'layout-mode-changed',
-        (evt, newLayout) => {
+        (evt: any, newLayout: any) => {
           elem.removeClass(className);
           className = 'card-list-layout-' + newLayout;
           elem.addClass(className);

+ 15 - 6
public/app/core/services/ng_react.ts

@@ -9,6 +9,7 @@
 // - reactComponent (generic directive for delegating off to React Components)
 // - reactDirective (factory for creating specific directives that correspond to reactComponent directives)
 
+import { kebabCase } from 'lodash';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import angular from 'angular';
@@ -155,11 +156,17 @@ function getPropExpression(prop) {
   return Array.isArray(prop) ? prop[0] : prop;
 }
 
-// find the normalized attribute knowing that React props accept any type of capitalization
-function findAttribute(attrs, propName) {
-  const index = Object.keys(attrs).filter(attr => {
-    return attr.toLowerCase() === propName.toLowerCase();
-  })[0];
+/**
+ * Finds the normalized attribute knowing that React props accept any type of capitalization and it also handles
+ * kabab case attributes which can be used in case the attribute would also be a standard html attribute and would be
+ * evaluated by the browser as such.
+ * @param attrs All attributes of the component.
+ * @param propName Name of the prop that react component expects.
+ */
+function findAttribute(attrs: string, propName: string): string {
+  const index = Object.keys(attrs).find(attr => {
+    return attr.toLowerCase() === propName.toLowerCase() || attr.toLowerCase() === kebabCase(propName);
+  });
   return attrs[index];
 }
 
@@ -274,7 +281,9 @@ const reactDirective = $injector => {
         // watch each property name and trigger an update whenever something changes,
         // to update scope.props with new values
         const propExpressions = props.map(prop => {
-          return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop];
+          return Array.isArray(prop)
+            ? [findAttribute(attrs, prop[0]), getPropConfig(prop)]
+            : findAttribute(attrs, prop);
         });
 
         // If we don't have any props, then our watch statement won't fire.

+ 8 - 7
public/app/core/table_model.ts

@@ -1,17 +1,18 @@
 import _ from 'lodash';
+import { Column, TableData } from '@grafana/ui';
 
-interface Column {
-  text: string;
+/**
+ * Extends the standard Column class with variables that get
+ * mutated in the angular table panel.
+ */
+interface MutableColumn extends Column {
   title?: string;
-  type?: string;
   sort?: boolean;
   desc?: boolean;
-  filterable?: boolean;
-  unit?: string;
 }
 
-export default class TableModel {
-  columns: Column[];
+export default class TableModel implements TableData {
+  columns: MutableColumn[];
   rows: any[];
   type: string;
   columnMap: any;

+ 1 - 1
public/app/features/api-keys/ApiKeysPage.tsx

@@ -7,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
 import { getApiKeys, getApiKeysCount } from './state/selectors';
 import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
 import Page from 'app/core/components/Page/Page';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
 import ApiKeysAddedModal from './ApiKeysAddedModal';
 import config from 'app/core/config';
 import appEvents from 'app/core/app_events';

+ 1 - 1
public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import { Tooltip } from '@grafana/ui';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
 import { StoreState, FolderInfo } from 'app/types';
 import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
 import {

+ 6 - 22
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -11,16 +11,15 @@ import {
   DataQueryResponse,
   DataQueryError,
   LoadingState,
-  PanelData,
   TableData,
   TimeRange,
-  TimeSeries,
   ScopedVars,
+  toTableData,
 } from '@grafana/ui';
 
 interface RenderProps {
   loading: LoadingState;
-  panelData: PanelData;
+  data: TableData[];
 }
 
 export interface Props {
@@ -44,7 +43,7 @@ export interface State {
   isFirstLoad: boolean;
   loading: LoadingState;
   response: DataQueryResponse;
-  panelData: PanelData;
+  data?: TableData[];
 }
 
 export class DataPanel extends Component<Props, State> {
@@ -64,7 +63,6 @@ export class DataPanel extends Component<Props, State> {
       response: {
         data: [],
       },
-      panelData: {},
       isFirstLoad: true,
     };
   }
@@ -149,7 +147,7 @@ export class DataPanel extends Component<Props, State> {
       this.setState({
         loading: LoadingState.Done,
         response: resp,
-        panelData: this.getPanelData(resp),
+        data: toTableData(resp.data),
         isFirstLoad: false,
       });
     } catch (err) {
@@ -172,23 +170,9 @@ export class DataPanel extends Component<Props, State> {
     }
   };
 
-  getPanelData(response: DataQueryResponse) {
-    if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') {
-      return {
-        tableData: response.data[0] as TableData,
-        timeSeries: null,
-      };
-    }
-
-    return {
-      timeSeries: response.data as TimeSeries[],
-      tableData: null,
-    };
-  }
-
   render() {
     const { queries } = this.props;
-    const { loading, isFirstLoad, panelData } = this.state;
+    const { loading, isFirstLoad, data } = this.state;
 
     // do not render component until we have first data
     if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
@@ -206,7 +190,7 @@ export class DataPanel extends Component<Props, State> {
     return (
       <>
         {loading === LoadingState.Loading && this.renderLoadingState()}
-        {this.props.children({ loading, panelData })}
+        {this.props.children({ loading, data })}
       </>
     );
   }

+ 7 - 7
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -11,7 +11,7 @@ import { DataPanel } from './DataPanel';
 import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
 
 // Utils
-import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
+import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
 import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
 import { profiler } from 'app/core/profiler';
 import config from 'app/core/config';
@@ -19,7 +19,7 @@ import config from 'app/core/config';
 // Types
 import { DashboardModel, PanelModel } from '../state';
 import { PanelPlugin } from 'app/types';
-import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
+import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui';
 import { ScopedVars } from '@grafana/ui';
 
 import templateSrv from 'app/features/templating/template_srv';
@@ -139,10 +139,10 @@ export class PanelChrome extends PureComponent<Props, State> {
   }
 
   get getDataForPanel() {
-    return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
+    return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : null;
   }
 
-  renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
+  renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element {
     const { panel, plugin } = this.props;
     const { timeRange, renderCounter } = this.state;
     const PanelComponent = plugin.exports.reactPanel.panel;
@@ -157,7 +157,7 @@ export class PanelChrome extends PureComponent<Props, State> {
       <div className="panel-content">
         <PanelComponent
           loading={loading}
-          panelData={panelData}
+          data={data}
           timeRange={timeRange}
           options={panel.getOptions(plugin.exports.reactPanel.defaults)}
           width={width - 2 * config.theme.panelPadding.horizontal}
@@ -188,8 +188,8 @@ export class PanelChrome extends PureComponent<Props, State> {
             onDataResponse={this.onDataResponse}
             onError={this.onDataError}
           >
-            {({ loading, panelData }) => {
-              return this.renderPanelPlugin(loading, panelData, width, height);
+            {({ loading, data }) => {
+              return this.renderPanelPlugin(loading, data, width, height);
             }}
           </DataPanel>
         ) : (

+ 6 - 6
public/app/features/dashboard/state/PanelModel.ts

@@ -111,12 +111,12 @@ export class PanelModel {
   cachedPluginOptions?: any;
   legend?: { show: boolean };
 
-  constructor(model) {
+  constructor(model: any) {
     this.events = new Emitter();
 
     // copy properties from persisted model
     for (const property in model) {
-      this[property] = model[property];
+      (this as any)[property] = model[property];
     }
 
     // defaults
@@ -150,7 +150,7 @@ export class PanelModel {
     }
   }
 
-  getOptions(panelDefaults) {
+  getOptions(panelDefaults: any) {
     return _.defaultsDeep(this.options || {}, panelDefaults);
   }
 
@@ -227,7 +227,7 @@ export class PanelModel {
       }
       return {
         ...acc,
-        [property]: this[property],
+        [property]: (this as any)[property],
       };
     }, {});
   }
@@ -236,7 +236,7 @@ export class PanelModel {
     const prevOptions = this.cachedPluginOptions[pluginId] || {};
 
     Object.keys(prevOptions).map(property => {
-      this[property] = prevOptions[property];
+      (this as any)[property] = prevOptions[property];
     });
   }
 
@@ -252,7 +252,7 @@ export class PanelModel {
         continue;
       }
 
-      delete this[key];
+      delete (this as any)[key];
     }
 
     this.cachedPluginOptions[oldPluginId] = oldOptions;

+ 1 - 18
public/app/features/dashboard/utils/panel.ts

@@ -4,8 +4,7 @@ import store from 'app/core/store';
 // Models
 import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
 import { PanelModel } from 'app/features/dashboard/state/PanelModel';
-import { PanelData, TimeRange, TimeSeries } from '@grafana/ui';
-import { TableData } from '@grafana/ui/src';
+import { TimeRange } from '@grafana/ui';
 
 // Utils
 import { isString as _isString } from 'lodash';
@@ -170,19 +169,3 @@ export function getResolution(panel: PanelModel): number {
 
   return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24));
 }
-
-const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints');
-const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
-export const snapshotDataToPanelData = (panel: PanelModel): PanelData => {
-  const snapshotData = panel.snapshotData;
-  if (isTimeSeries(snapshotData[0])) {
-    return {
-      timeSeries: snapshotData,
-    } as PanelData;
-  } else if (isTableData(snapshotData[0])) {
-    return {
-      tableData: snapshotData[0],
-    } as PanelData;
-  }
-  throw new Error('snapshotData is invalid:' + snapshotData.toString());
-};

+ 1 - 1
public/app/features/folders/FolderPermissions.tsx

@@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 import Page from 'app/core/components/Page/Page';
 import { Tooltip } from '@grafana/ui';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { NavModel, StoreState, FolderState } from 'app/types';
 import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';

+ 3 - 1
public/app/features/playlist/playlist_srv.ts

@@ -41,7 +41,9 @@ export class PlaylistSrv {
 
     const dash = this.dashboards[this.index];
     const queryParams = this.$location.search();
-    const filteredParams = _.pickBy(queryParams, value => value !== null);
+    const filteredParams = _.pickBy(queryParams, key => {
+      return key === 'kiosk' || key === 'autofitpanels' || key === 'orgId';
+    });
     const nextDashboardUrl = locationUtil.stripBaseFromUrl(dash.url);
 
     // this is done inside timeout to make sure digest happens after

+ 1 - 1
public/app/features/teams/TeamGroupSync.tsx

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import { connect } from 'react-redux';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
 import { Tooltip } from '@grafana/ui';
 import { TeamGroup } from '../../types';
 import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';

+ 1 - 1
public/app/features/teams/TeamMembers.tsx

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import { connect } from 'react-redux';
-import SlideDown from 'app/core/components/Animations/SlideDown';
+import { SlideDown } from 'app/core/components/Animations/SlideDown';
 import { UserPicker } from 'app/core/components/Select/UserPicker';
 import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
 import { TeamMember, User } from 'app/types';

+ 14 - 0
public/app/plugins/datasource/mssql/config_ctrl.ts

@@ -1,3 +1,5 @@
+import { SyntheticEvent } from 'react';
+
 export class MssqlConfigCtrl {
   static templateUrl = 'partials/config.html';
 
@@ -7,4 +9,16 @@ export class MssqlConfigCtrl {
   constructor($scope) {
     this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false';
   }
+
+  onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
+    event.preventDefault();
+    this.current.secureJsonFields.password = false;
+    this.current.secureJsonData = this.current.secureJsonData || {};
+    this.current.secureJsonData.password = '';
+  };
+
+  onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
+    this.current.secureJsonData = this.current.secureJsonData || {};
+    this.current.secureJsonData.password = event.currentTarget.value;
+  };
 }

+ 9 - 9
public/app/plugins/datasource/mssql/partials/config.html

@@ -17,15 +17,15 @@
 			<span class="gf-form-label width-7">User</span>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
 		</div>
-		<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password">
-			<span class="gf-form-label width-7">Password</span>
-			<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>
-		</div>
-		<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password">
-			<span class="gf-form-label width-7">Password</span>
-			<input type="text" class="gf-form-input" disabled="disabled" value="configured">
-			<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
-		</div>
+    <div class="gf-form">
+      <secret-form-field
+        isConfigured="ctrl.current.secureJsonFields.password"
+        value="ctrl.current.secureJsonData.password"
+        on-reset="ctrl.onPasswordReset"
+        on-change="ctrl.onPasswordChange"
+        inputWidth="9"
+      />
+    </div>
 	</div>
 
 	<div class="gf-form">

+ 13 - 0
public/app/plugins/datasource/postgres/config_ctrl.ts

@@ -1,4 +1,5 @@
 import _ from 'lodash';
+import { SyntheticEvent } from 'react';
 
 export class PostgresConfigCtrl {
   static templateUrl = 'partials/config.html';
@@ -52,6 +53,18 @@ export class PostgresConfigCtrl {
     this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
   }
 
+  onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
+    event.preventDefault();
+    this.current.secureJsonFields.password = false;
+    this.current.secureJsonData = this.current.secureJsonData || {};
+    this.current.secureJsonData.password = '';
+  };
+
+  onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
+    this.current.secureJsonData = this.current.secureJsonData || {};
+    this.current.secureJsonData.password = event.currentTarget.value;
+  };
+
   // the value portion is derived from postgres server_version_num/100
   postgresVersions = [
     { name: '9.3', value: 903 },

+ 9 - 8
public/app/plugins/datasource/postgres/partials/config.html

@@ -17,16 +17,17 @@
 			<span class="gf-form-label width-7">User</span>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
 		</div>
-		<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password">
-			<span class="gf-form-label width-7">Password</span>
-			<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>
-    </div>
-    <div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password">
-      <span class="gf-form-label width-7">Password</span>
-      <input type="text" class="gf-form-input" disabled="disabled" value="configured">
-      <a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
+    <div class="gf-form">
+      <secret-form-field
+        isConfigured="ctrl.current.secureJsonFields.password"
+        value="ctrl.current.secureJsonData.password"
+        on-reset="ctrl.onPasswordReset"
+        on-change="ctrl.onPasswordChange"
+        inputWidth="9"
+      />
     </div>
 	</div>
+
 	<div class="gf-form">
 		<label class="gf-form-label width-7">SSL Mode</label>
 		<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">

+ 1 - 1
public/app/plugins/datasource/prometheus/specs/datasource.test.ts

@@ -351,7 +351,7 @@ const timeSrv = {
 };
 
 describe('PrometheusDatasource', () => {
-  describe('When querying prometheus with one target using query editor target spec', async () => {
+  describe('When querying prometheus with one target using query editor target spec', () => {
     let results;
     const query = {
       range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },

+ 2 - 2
public/app/plugins/panel/bargauge/BarGaugePanel.tsx

@@ -32,14 +32,14 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
   };
 
   render() {
-    const { height, width, options, panelData, renderCounter } = this.props;
+    const { height, width, options, data, renderCounter } = this.props;
     return (
       <ProcessedValuesRepeater
         getProcessedValues={this.getProcessedValues}
         renderValue={this.renderValue}
         width={width}
         height={height}
-        source={panelData}
+        source={data}
         renderCounter={renderCounter}
         orientation={options.orientation}
       />

+ 2 - 2
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -37,14 +37,14 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
   };
 
   render() {
-    const { height, width, options, panelData, renderCounter } = this.props;
+    const { height, width, options, data, renderCounter } = this.props;
     return (
       <ProcessedValuesRepeater
         getProcessedValues={this.getProcessedValues}
         renderValue={this.renderValue}
         width={width}
         height={height}
-        source={panelData}
+        source={data}
         renderCounter={renderCounter}
         orientation={options.orientation}
       />

+ 3 - 3
public/app/plugins/panel/graph2/GraphPanel.tsx

@@ -16,13 +16,13 @@ interface Props extends PanelProps<Options> {}
 
 export class GraphPanel extends PureComponent<Props> {
   render() {
-    const { panelData, timeRange, width, height } = this.props;
+    const { data, timeRange, width, height } = this.props;
     const { showLines, showBars, showPoints } = this.props.options;
 
     let vmSeries: TimeSeriesVMs;
-    if (panelData.timeSeries) {
+    if (data) {
       vmSeries = processTimeSeries({
-        timeSeries: panelData.timeSeries,
+        data,
         nullValueMode: NullValueMode.Ignore,
       });
     }

+ 2 - 2
public/app/plugins/panel/singlestat/module.ts

@@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn';
 import config from 'app/core/config';
 import TimeSeries from 'app/core/time_series2';
 import { MetricsPanelCtrl } from 'app/plugins/sdk';
-import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
+import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName, isTableData } from '@grafana/ui';
 
 class SingleStatCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
@@ -112,7 +112,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       scopedVars: _.extend({}, this.panel.scopedVars),
     };
 
-    if (dataList.length > 0 && dataList[0].type === 'table') {
+    if (dataList.length > 0 && isTableData(dataList[0])) {
       this.dataType = 'table';
       const tableData = dataList.map(this.tableHandler.bind(this));
       this.setTableValues(tableData, data);

+ 16 - 11
public/app/plugins/panel/singlestat2/SingleStatPanel.tsx

@@ -4,28 +4,33 @@ import React, { PureComponent, CSSProperties } from 'react';
 // Types
 import { SingleStatOptions, SingleStatBaseOptions } from './types';
 
-import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui';
+import { DisplayValue, PanelProps, processTimeSeries, NullValueMode } from '@grafana/ui';
 import { config } from 'app/core/config';
 import { getDisplayProcessor } from '@grafana/ui';
 import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
 
 export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => {
-  const { panelData, replaceVariables, options } = props;
+  const { data, replaceVariables, options } = props;
   const { valueOptions, valueMappings } = options;
+  const { unit, decimals, stat } = valueOptions;
+
   const processor = getDisplayProcessor({
-    unit: valueOptions.unit,
-    decimals: valueOptions.decimals,
+    unit,
+    decimals,
     mappings: valueMappings,
     thresholds: options.thresholds,
-
     prefix: replaceVariables(valueOptions.prefix),
     suffix: replaceVariables(valueOptions.suffix),
     theme: config.theme,
   });
-  return processSingleStatPanelData({
-    panelData: panelData,
-    stat: valueOptions.stat,
-  }).map(stat => processor(stat.value));
+
+  return processTimeSeries({
+    data,
+    nullValueMode: NullValueMode.Null,
+  }).map((series, index) => {
+    const value = stat !== 'name' ? series.stats[stat] : series.label;
+    return processor(value);
+  });
 };
 
 export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
@@ -50,14 +55,14 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
   };
 
   render() {
-    const { height, width, options, panelData, renderCounter } = this.props;
+    const { height, width, options, data, renderCounter } = this.props;
     return (
       <ProcessedValuesRepeater
         getProcessedValues={this.getProcessedValues}
         renderValue={this.renderValue}
         width={width}
         height={height}
-        source={panelData}
+        source={data}
         renderCounter={renderCounter}
         orientation={options.orientation}
       />

+ 2 - 1
public/app/plugins/panel/table/module.ts

@@ -6,6 +6,7 @@ import { transformDataToTable } from './transformers';
 import { tablePanelEditor } from './editor';
 import { columnOptionsTab } from './column_options';
 import { TableRenderer } from './renderer';
+import { isTableData } from '@grafana/ui';
 
 class TablePanelCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
@@ -104,7 +105,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
 
     // automatically correct transform mode based on data
     if (this.dataRaw && this.dataRaw.length) {
-      if (this.dataRaw[0].type === 'table') {
+      if (isTableData(this.dataRaw[0])) {
         this.panel.transform = 'table';
       } else {
         if (this.dataRaw[0].type === 'docs') {

+ 3 - 3
public/app/plugins/panel/table2/TablePanel.tsx

@@ -14,15 +14,15 @@ export class TablePanel extends Component<Props> {
   }
 
   render() {
-    const { panelData, options } = this.props;
+    const { data, options } = this.props;
 
-    if (!panelData || !panelData.tableData) {
+    if (data.length < 1) {
       return <div>No Table Data...</div>;
     }
 
     return (
       <ThemeContext.Consumer>
-        {theme => <Table {...this.props} {...options} theme={theme} data={panelData.tableData} />}
+        {theme => <Table {...this.props} {...options} theme={theme} data={data[0]} />}
       </ThemeContext.Consumer>
     );
   }

+ 0 - 0
public/app/types/jquery.d.ts → public/app/types/jquery/jquery.d.ts


+ 1 - 2
scripts/webpack/sass.rule.js

@@ -12,8 +12,7 @@ module.exports = function(options) {
         options: {
           importLoaders: 2,
           url: options.preserveUrl,
-          sourceMap: options.sourceMap,
-          minimize: options.minimize,
+          sourceMap: options.sourceMap
         },
       },
       {

+ 2 - 9
scripts/webpack/webpack.dev.js

@@ -19,13 +19,6 @@ module.exports = merge(common, {
     light: './public/sass/grafana.light.scss',
   },
 
-  output: {
-    path: path.resolve(__dirname, '../../public/build'),
-    filename: '[name].[hash].js',
-    // Keep publicPath relative for host.com/grafana/ deployments
-    publicPath: "public/build/",
-  },
-
   module: {
     rules: [
       {
@@ -50,7 +43,7 @@ module.exports = merge(common, {
           },
         },
       },
-      require('./sass.rule.js')({ sourceMap: false, minimize: false, preserveUrl: false }),
+      require('./sass.rule.js')({ sourceMap: false, preserveUrl: false }),
       {
         test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
         loader: 'file-loader'
@@ -59,7 +52,7 @@ module.exports = merge(common, {
   },
 
   plugins: [
-    new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
+    new CleanWebpackPlugin(),
     new MiniCssExtractPlugin({
       filename: "grafana.[name].[hash].css"
     }),

+ 2 - 2
scripts/webpack/webpack.hot.js

@@ -56,7 +56,7 @@ module.exports = merge(common, {
               plugins: [
                 [require('@rtsao/plugin-proposal-class-properties'), { loose: true }],
                 'angularjs-annotate',
-                'syntax-dynamic-import', // needed for `() => import()` in routes.ts
+                '@babel/plugin-syntax-dynamic-import', // needed for `() => import()` in routes.ts
                 'react-hot-loader/babel',
               ],
               presets: [
@@ -98,7 +98,7 @@ module.exports = merge(common, {
   },
 
   plugins: [
-    new CleanWebpackPlugin('../public/build', { allowExternal: true }),
+    new CleanWebpackPlugin(),
     new HtmlWebpackPlugin({
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       template: path.resolve(__dirname, '../../public/views/index-template.html'),

+ 4 - 4
scripts/webpack/webpack.prod.js

@@ -1,7 +1,7 @@
 'use strict';
 
 const merge = require('webpack-merge');
-const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
+const TerserPlugin = require('terser-webpack-plugin');
 const common = require('./webpack.common.js');
 const path = require('path');
 const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
@@ -43,14 +43,14 @@ module.exports = merge(common, {
         },
       },
       require('./sass.rule.js')({
-        sourceMap: false, minimize: false, preserveUrl: false
+        sourceMap: false, preserveUrl: false
       })
     ]
   },
   optimization: {
     minimizer: [
-      new UglifyJsPlugin({
-        cache: true,
+      new TerserPlugin({
+        cache: false,
         parallel: true,
         sourceMap: true
       }),

+ 1 - 1
tsconfig.json

@@ -27,7 +27,7 @@
     "noUnusedLocals": true,
     "baseUrl": "public",
     "pretty": true,
-    "typeRoots": ["node_modules/@types", "types"],
+    "typeRoots": ["node_modules/@types", "public/app/types"],
     "paths": {
       "app": ["app"],
       "sass": ["sass"]

File diff suppressed because it is too large
+ 499 - 156
yarn.lock


Some files were not shown because too many files changed in this diff