فهرست منبع

HUE-9392 [core] Move UI config repo to Typescript

Johan Ahlen 5 سال پیش
والد
کامیت
3d45d9c56a

+ 0 - 4
desktop/core/src/desktop/js/api/apiHelper.js

@@ -2516,10 +2516,6 @@ class ApiHelper {
     });
   }
 
-  getClusterConfig(data) {
-    return $.post(URLS.FETCH_CONFIG_API, data);
-  }
-
   fetchHueDocsInteractive(query) {
     const deferred = $.Deferred();
     const request = $.post(URLS.INTERACTIVE_SEARCH_API, {

+ 25 - 28
desktop/core/src/desktop/js/ko/components/assist/ko.assistFunctionsPanel.test.js

@@ -13,35 +13,33 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-import $ from 'jquery';
 import * as ko from 'knockout';
 
-import ApiHelper from 'api/apiHelper';
-import * as apiUtils from 'sql/reference/apiUtils';
+import * as apiUtils from 'api/apiUtils';
+import * as refApiUtils from 'sql/reference/apiUtils';
 import AssistFunctionsPanel from './ko.assistFunctionsPanel';
 import { refreshConfig } from 'utils/hueConfig';
 import { sleep } from 'utils/hueUtils';
 
 describe('ko.assistFunctionsPanel.js', () => {
-  jest.spyOn(apiUtils, 'fetchUdfs').mockImplementation(() => Promise.resolve([]));
+  jest.spyOn(refApiUtils, 'fetchUdfs').mockImplementation(() => Promise.resolve([]));
 
   it('should handle cluster config updates', async () => {
-    const spy = jest.spyOn(ApiHelper, 'getClusterConfig').mockImplementation(() =>
-      $.Deferred()
-        .resolve({
-          status: 0,
-          app_config: {
-            editor: {
-              interpreters: [
-                { type: 'pig', dialect: 'pig', displayName: 'Pig' },
-                { type: 'impala', dialect: 'impala', displayName: 'Impala' },
-                { type: 'banana', dialect: 'banana', displayName: 'Banana' }
-              ]
-            }
+    const spy = jest.spyOn(apiUtils, 'simplePostAsync').mockImplementation(async () =>
+      Promise.resolve({
+        status: 0,
+        app_config: {
+          editor: {
+            interpreters: [
+              { type: 'pig', dialect: 'pig', displayName: 'Pig' },
+              { type: 'impala', dialect: 'impala', displayName: 'Impala' },
+              { type: 'banana', dialect: 'banana', displayName: 'Banana' }
+            ]
           }
-        })
-        .promise()
+        }
+      })
     );
+
     await refreshConfig();
     const connector = ko.observable({ dialect: 'impala' });
     const subject = new AssistFunctionsPanel({ activeConnector: connector });
@@ -60,18 +58,17 @@ describe('ko.assistFunctionsPanel.js', () => {
 
     spy.mockRestore();
 
-    const changeSpy = jest.spyOn(ApiHelper, 'getClusterConfig').mockImplementation(() =>
-      $.Deferred()
-        .resolve({
-          status: 0,
-          app_config: {
-            editor: {
-              interpreters: [{ type: 'pig', dialect: 'pig', displayName: 'Pig' }]
-            }
+    const changeSpy = jest.spyOn(apiUtils, 'simplePostAsync').mockImplementation(async () =>
+      Promise.resolve({
+        status: 0,
+        app_config: {
+          editor: {
+            interpreters: [{ type: 'pig', dialect: 'pig', displayName: 'Pig' }]
           }
-        })
-        .promise()
+        }
+      })
     );
+
     await refreshConfig();
     expect(changeSpy).toHaveBeenCalled();
     changeSpy.mockRestore();

+ 18 - 22
desktop/core/src/desktop/js/ko/components/assist/ko.assistLangRefPanel.test.js

@@ -16,9 +16,8 @@
 
 import * as ko from 'knockout';
 
+import * as apiUtils from 'api/apiUtils';
 import AssistLangRefPanel from './ko.assistLangRefPanel';
-import apiHelper from 'api/apiHelper';
-import $ from 'jquery';
 import { refreshConfig } from 'utils/hueConfig';
 import { sleep } from 'utils/hueUtils';
 
@@ -29,17 +28,15 @@ describe('ko.assistLangRefPanel.js', () => {
   });
 
   it('should handle cluster config updates', async () => {
-    const spy = jest.spyOn(apiHelper, 'getClusterConfig').mockImplementation(() =>
-      $.Deferred()
-        .resolve({
-          status: 0,
-          app_config: {
-            editor: {
-              interpreters: [{ dialect: 'hive' }, { dialect: 'impala' }, { dialect: 'banana' }]
-            }
+    const spy = jest.spyOn(apiUtils, 'simplePostAsync').mockImplementation(async () =>
+      Promise.resolve({
+        status: 0,
+        app_config: {
+          editor: {
+            interpreters: [{ dialect: 'hive' }, { dialect: 'impala' }, { dialect: 'banana' }]
           }
-        })
-        .promise()
+        }
+      })
     );
     await refreshConfig();
     const connector = ko.observable({ dialect: 'impala' });
@@ -51,18 +48,17 @@ describe('ko.assistLangRefPanel.js', () => {
 
     spy.mockRestore();
 
-    const changeSpy = jest.spyOn(apiHelper, 'getClusterConfig').mockImplementation(() =>
-      $.Deferred()
-        .resolve({
-          status: 0,
-          app_config: {
-            editor: {
-              interpreters: [{ dialect: 'impala' }]
-            }
+    const changeSpy = jest.spyOn(apiUtils, 'simplePostAsync').mockImplementation(async () =>
+      Promise.resolve({
+        status: 0,
+        app_config: {
+          editor: {
+            interpreters: [{ dialect: 'impala' }]
           }
-        })
-        .promise()
+        }
+      })
     );
+
     await refreshConfig();
     expect(changeSpy).toHaveBeenCalled();
     changeSpy.mockRestore();

+ 1 - 1
desktop/core/src/desktop/js/sql/reference/apiCache.ts

@@ -16,7 +16,7 @@
 
 import localForage from 'localforage';
 import { UdfCategory } from 'sql/reference/types';
-import { Connector } from 'types';
+import { Connector } from 'types/config';
 
 const GLOBAL_UDF_CACHE_KEY = 'HUE_GLOBAL_UDF_KEY';
 const VERSION = '0';

+ 1 - 1
desktop/core/src/desktop/js/sql/reference/apiUtils.ts

@@ -17,7 +17,7 @@
 import { simplePostAsync } from 'api/apiUtils';
 import { AUTOCOMPLETE_API_PREFIX } from 'api/urls';
 import { UdfArgument, UdfDetails } from 'sql/reference/types';
-import { Connector } from 'types';
+import { Connector } from 'types/types';
 import I18n from 'utils/i18n';
 
 export interface ApiUdf {

+ 13 - 2
desktop/core/src/desktop/js/sql/reference/sqlReferenceRepository.test.ts

@@ -15,12 +15,23 @@
 // limitations under the License.
 
 import { UdfArgument } from 'sql/reference/types';
+import { Connector } from 'types/config';
 import { getArgumentDetailsForUdf } from './sqlReferenceRepository';
 import * as apiUtils from 'sql/reference/apiUtils';
 
 describe('sqlReferenceRepository.js', () => {
-  const hiveConn = { dialect: 'hive', id: 'hive' };
-  const impalaConn = { dialect: 'impala', id: 'impala' };
+  const createTestConnector = (dialect: string, id: string): Connector => ({
+    dialect: dialect,
+    id: id,
+    buttonName: '',
+    displayName: '',
+    page: '',
+    tooltip: '',
+    type: ''
+  });
+
+  const hiveConn: Connector = createTestConnector('hive', 'hive');
+  const impalaConn = createTestConnector('impala', 'impala');
 
   jest.mock('sql/reference/impala/udfReference', () => ({
     UDF_CATEGORIES: [

+ 8 - 5
desktop/core/src/desktop/js/sql/reference/sqlReferenceRepository.ts

@@ -21,7 +21,7 @@ import {
   UdfCategoryFunctions,
   UdfDetails
 } from 'sql/reference/types';
-import { Connector } from 'types';
+import { Connector } from 'types/config';
 import { matchesType } from './typeUtils';
 import I18n from 'utils/i18n';
 import huePubSub from 'utils/huePubSub';
@@ -56,7 +56,7 @@ const getMergedUdfKey = (connector: Connector, database?: string): string => {
 };
 
 export const hasUdfCategories = (connector: Connector): boolean =>
-  typeof UDF_REFS[connector.dialect] !== 'undefined';
+  !!connector.dialect && typeof UDF_REFS[connector.dialect] !== 'undefined';
 
 const findUdfsToAdd = (
   apiUdfs: UdfDetails[],
@@ -115,7 +115,7 @@ export const getUdfCategories = async (
         resolve(cachedCategories);
       }
       let categories: UdfCategory[] = [];
-      if (UDF_REFS[connector.dialect]) {
+      if (connector.dialect && UDF_REFS[connector.dialect]) {
         const module = await UDF_REFS[connector.dialect]();
         if (module.UDF_CATEGORIES) {
           categories = module.UDF_CATEGORIES;
@@ -190,7 +190,10 @@ export const getUdfsWithReturnTypes = async (
     ) {
       Object.keys(category.functions).forEach(udfName => {
         const udf = category.functions[udfName];
-        if (!returnTypes || matchesType(connector.dialect, returnTypes, udf.returnTypes)) {
+        if (
+          !returnTypes ||
+          (connector.dialect && matchesType(connector.dialect, returnTypes, udf.returnTypes))
+        ) {
           result.push(udf);
         }
       });
@@ -223,7 +226,7 @@ export const getArgumentDetailsForUdf = async (
 };
 
 export const getSetOptions = async (connector: Connector): Promise<SetOptions> => {
-  if (SET_REFS[connector.dialect]) {
+  if (connector.dialect && SET_REFS[connector.dialect]) {
     const module = await SET_REFS[connector.dialect]();
     if (module.SET_OPTIONS) {
       return module.SET_OPTIONS;

+ 0 - 20
desktop/core/src/desktop/js/types.ts

@@ -1,20 +0,0 @@
-// Licensed to Cloudera, Inc. under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  Cloudera, Inc. licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-export interface Connector {
-  id: string;
-  dialect: string;
-}

+ 109 - 0
desktop/core/src/desktop/js/types/config.ts

@@ -0,0 +1,109 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { GenericApiResponse } from 'types/types';
+
+export interface Cluster {
+  credentials: Record<string, unknown>;
+  id: string;
+  name: string;
+  type: string;
+}
+
+export interface AppConfig<T extends Interpreter> {
+  buttonName: string;
+  displayName: string;
+  interpreter_names?: string[];
+  interpreters: T[];
+  name: string;
+  page?: string;
+}
+
+export interface EditorConfig extends AppConfig<EditorInterpreter> {
+  default_limit: number | null;
+  default_sql_interpreter: string;
+}
+
+export enum AppType {
+  browser = 'browser',
+  editor = 'editor',
+  dashboard = 'dashboard',
+  scheduler = 'scheduler'
+}
+
+export interface HueConfig extends GenericApiResponse {
+  app_config: {
+    [AppType.browser]?: AppConfig<BrowserInterpreter>;
+    catalogs?: CatalogInterpreter[];
+    [AppType.dashboard]?: AppConfig<DashboardInterpreter>;
+    [AppType.editor]?: EditorConfig;
+    home?: AppConfig<Interpreter>;
+    [AppType.scheduler]?: AppConfig<SchedulerInterpreter>;
+  };
+  button_action: AppConfig<Interpreter>[];
+  cluster_type: string;
+  clusters: Cluster[];
+  default_sql_interpreter: string;
+  documents: {
+    types: string[];
+  };
+  has_computes: boolean;
+  main_button_action: AppConfig<Interpreter>;
+  status: number;
+  hue_config: {
+    enable_sharing: boolean;
+  };
+}
+
+export interface Interpreter {
+  buttonName: string;
+  displayName: string;
+  page: string;
+  tooltip: string;
+  type: string;
+}
+
+export interface IdentifiableInterpreter extends Interpreter {
+  dialect?: string;
+  id: string;
+}
+
+/* eslint-disable @typescript-eslint/no-empty-interface */
+export interface Connector extends IdentifiableInterpreter {}
+
+export interface EditorInterpreter extends IdentifiableInterpreter {
+  dialect_properties: Record<string, unknown> | null;
+  is_batchable: boolean;
+  is_sql: boolean;
+  name: string;
+  optimizer: string;
+}
+
+/* eslint-disable @typescript-eslint/no-empty-interface */
+export interface BrowserInterpreter extends Interpreter {}
+
+export interface CatalogInterpreter extends IdentifiableInterpreter {
+  is_catalog: boolean;
+  is_sql: boolean;
+  name: string;
+}
+
+export interface DashboardInterpreter extends IdentifiableInterpreter {
+  is_sql: string;
+}
+
+/* eslint-disable @typescript-eslint/no-empty-interface */
+export interface SchedulerInterpreter extends Interpreter {}

+ 4 - 0
desktop/core/src/desktop/js/types/types.ts

@@ -0,0 +1,4 @@
+export interface GenericApiResponse {
+  status: number;
+  message?: string;
+}

+ 0 - 128
desktop/core/src/desktop/js/utils/hueConfig.js

@@ -1,128 +0,0 @@
-// Licensed to Cloudera, Inc. under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  Cloudera, Inc. licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-import $ from 'jquery';
-
-import apiHelper from 'api/apiHelper';
-import huePubSub from 'utils/huePubSub';
-
-export const REFRESH_CONFIG_EVENT = 'cluster.config.refresh.config';
-export const CONFIG_REFRESHED_EVENT = 'cluster.config.set.config';
-export const GET_KNOWN_CONFIG_EVENT = 'cluster.config.get.config';
-
-let lastConfigPromise = undefined;
-let lastKnownConfig = undefined;
-
-export const refreshConfig = async () => {
-  lastConfigPromise = new Promise((resolve, reject) => {
-    apiHelper
-      .getClusterConfig()
-      .done(data => {
-        if (data.status === 0) {
-          lastKnownConfig = data;
-          resolve(data);
-        } else {
-          $(document).trigger('error', data.message);
-          reject();
-        }
-      })
-      .fail(reject);
-  });
-
-  lastConfigPromise
-    .then(config => {
-      huePubSub.publish(CONFIG_REFRESHED_EVENT, config);
-    })
-    .catch(() => {
-      huePubSub.publish(CONFIG_REFRESHED_EVENT);
-    });
-
-  return lastConfigPromise;
-};
-
-const validConnectorConfig = (config, type) => {
-  if (
-    !config ||
-    !config.app_config ||
-    !config.app_config[type] ||
-    !config.app_config[type].interpreters
-  ) {
-    console.error(`No "interpreters" attribute present in the config for type "${type}".`);
-    return false;
-  }
-  return true;
-};
-
-export const getLastKnownConfig = () => lastKnownConfig;
-
-const CONNECTOR_TYPES = {
-  editor: 'editor',
-  browser: 'browser',
-  dashboard: 'dashboard'
-};
-
-const findConnector = (connectorTest, type) => {
-  if (validConnectorConfig(lastKnownConfig, type)) {
-    const connectors = lastKnownConfig.app_config[type].interpreters;
-    return connectors.find(connectorTest);
-  }
-};
-
-const filterConnectors = (connectorTest, type) => {
-  if (validConnectorConfig(lastKnownConfig, type)) {
-    const connectors = lastKnownConfig.app_config[type].interpreters;
-    return connectors.filter(connectorTest);
-  }
-  return [];
-};
-
-export const findBrowserConnector = connectorTest =>
-  findConnector(connectorTest, CONNECTOR_TYPES.browser);
-
-export const findDashboardConnector = connectorTest =>
-  findConnector(connectorTest, CONNECTOR_TYPES.dashboard);
-
-export const findEditorConnector = connectorTest =>
-  findConnector(connectorTest, CONNECTOR_TYPES.editor);
-
-export const filterEditorConnectors = connectorTest =>
-  filterConnectors(connectorTest, CONNECTOR_TYPES.editor);
-
-const rootPathRegex = /.*%3A%2F%2F(.+)$/;
-
-/**
- * This takes the initial path from the "browser" config, used in cases where the users can't access '/'
- * for abfs etc.
- */
-export const getRootFilePath = connector => {
-  if (!connector || connector.type === 'hdfs') {
-    return '';
-  }
-  const match = connector.page.match(rootPathRegex);
-  if (match) {
-    return match[1] + '/';
-  }
-
-  return '';
-};
-
-huePubSub.subscribe(REFRESH_CONFIG_EVENT, refreshConfig);
-
-// TODO: Replace GET_KNOWN_CONFIG_EVENT pubSub with sync getKnownConfig const
-huePubSub.subscribe(GET_KNOWN_CONFIG_EVENT, callback => {
-  if (lastConfigPromise && callback) {
-    lastConfigPromise.then(callback).catch(callback);
-  }
-});

+ 130 - 0
desktop/core/src/desktop/js/utils/hueConfig.ts

@@ -0,0 +1,130 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import { simplePostAsync } from 'api/apiUtils';
+import { FETCH_CONFIG_API } from 'api/urls';
+import {
+  AppType,
+  BrowserInterpreter,
+  DashboardInterpreter,
+  EditorInterpreter,
+  HueConfig,
+  Interpreter
+} from 'types/config';
+import huePubSub from 'utils/huePubSub';
+
+export const REFRESH_CONFIG_EVENT = 'cluster.config.refresh.config';
+export const CONFIG_REFRESHED_EVENT = 'cluster.config.set.config';
+export const GET_KNOWN_CONFIG_EVENT = 'cluster.config.get.config';
+
+let lastConfigPromise: Promise<HueConfig> | undefined;
+let lastKnownConfig: HueConfig | undefined;
+
+const fetchConfig = async (): Promise<HueConfig> =>
+  await simplePostAsync(FETCH_CONFIG_API, {}, { silenceErrors: true });
+
+export const refreshConfig = async (): Promise<HueConfig> => {
+  lastConfigPromise = new Promise<HueConfig>(async (resolve, reject) => {
+    try {
+      const apiResponse = await fetchConfig();
+      if (apiResponse.status == 0) {
+        lastKnownConfig = apiResponse;
+        resolve(lastKnownConfig);
+      } else {
+        $(document).trigger('error', apiResponse.message);
+        reject();
+      }
+    } catch (err) {
+      reject(err);
+    }
+  });
+
+  lastConfigPromise
+    .then(config => {
+      huePubSub.publish(CONFIG_REFRESHED_EVENT, config);
+    })
+    .catch(() => {
+      huePubSub.publish(CONFIG_REFRESHED_EVENT);
+    });
+
+  return lastConfigPromise;
+};
+
+export const getLastKnownConfig = (): HueConfig | undefined => lastKnownConfig;
+
+const getInterpreters = (appType: AppType): Interpreter[] => {
+  if (!lastKnownConfig || !lastKnownConfig.app_config) {
+    return [];
+  }
+  const appConfig = lastKnownConfig.app_config[appType];
+  if (!appConfig) {
+    console.warn(`No app config for type ${appType}`);
+    return [];
+  }
+  if (!appConfig.interpreters) {
+    console.warn(`No interpreters configured for type ${appType}`);
+    return [];
+  }
+  return appConfig.interpreters;
+};
+
+export const findDashboardConnector = (
+  connectorTest: (connector: Interpreter) => boolean
+): DashboardInterpreter | undefined =>
+  (getInterpreters(AppType.dashboard) as DashboardInterpreter[]).find(connectorTest);
+
+export const findBrowserConnector = (
+  connectorTest: (connector: Interpreter) => boolean
+): BrowserInterpreter | undefined =>
+  (getInterpreters(AppType.browser) as BrowserInterpreter[]).find(connectorTest);
+
+export const findEditorConnector = (
+  connectorTest: (connector: Interpreter) => boolean
+): EditorInterpreter | undefined =>
+  (getInterpreters(AppType.editor) as EditorInterpreter[]).find(connectorTest);
+
+export const filterEditorConnectors = (
+  connectorTest: (connector: Interpreter) => boolean
+): EditorInterpreter[] | undefined =>
+  (getInterpreters(AppType.editor) as EditorInterpreter[]).filter(connectorTest);
+
+const rootPathRegex = /.*%3A%2F%2F(.+)$/;
+
+/**
+ * This takes the initial path from the "browser" config, used in cases where the users can't access '/'
+ * for abfs etc.
+ */
+export const getRootFilePath = (connector: BrowserInterpreter): string => {
+  if (!connector || connector.type === 'hdfs') {
+    return '';
+  }
+  const match = connector.page.match(rootPathRegex);
+  if (match) {
+    return match[1] + '/';
+  }
+
+  return '';
+};
+
+huePubSub.subscribe(REFRESH_CONFIG_EVENT, refreshConfig);
+
+// TODO: Replace GET_KNOWN_CONFIG_EVENT pubSub with sync getKnownConfig const
+huePubSub.subscribe(GET_KNOWN_CONFIG_EVENT, (callback?: (appConfig: HueConfig) => void) => {
+  if (lastConfigPromise && callback) {
+    lastConfigPromise.then(callback).catch(callback);
+  }
+});

+ 34 - 3
package-lock.json

@@ -5267,12 +5267,27 @@
         "pretty-format": "^25.2.1"
       }
     },
+    "@types/jquery": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.0.tgz",
+      "integrity": "sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw==",
+      "dev": true,
+      "requires": {
+        "@types/sizzle": "*"
+      }
+    },
     "@types/json-schema": {
       "version": "7.0.5",
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
       "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
       "dev": true
     },
+    "@types/knockout": {
+      "version": "3.4.67",
+      "resolved": "https://registry.npmjs.org/@types/knockout/-/knockout-3.4.67.tgz",
+      "integrity": "sha512-VYg5VcZecApmAILpSsTtD0UUAHUIGWi7/um9WQjbk/ajPWinGZAKkH9U8zbFmvdxfafFGvcRm655XRm6vc6hoQ==",
+      "dev": true
+    },
     "@types/minimatch": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -5309,6 +5324,21 @@
       "integrity": "sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ==",
       "dev": true
     },
+    "@types/semver": {
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.1.tgz",
+      "integrity": "sha512-ooD/FJ8EuwlDKOI6D9HWxgIgJjMg2cuziXm/42npDC8y4NjxplBUn9loewZiBNCt44450lHAU0OSb51/UqXeag==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/sizzle": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
+      "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
+      "dev": true
+    },
     "@types/stack-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@@ -9219,11 +9249,12 @@
       }
     },
     "eslint-plugin-vue": {
-      "version": "7.0.0-alpha.6",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.0.0-alpha.6.tgz",
-      "integrity": "sha512-gxRMxp3njmTez+aKGvVTUauAvt4Pd9WlU2wHW/sOzwHln1JTa7vV86iETRXBVxhqf0nDBsSfuDS7k6HLefvSPg==",
+      "version": "7.0.0-alpha.8",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.0.0-alpha.8.tgz",
+      "integrity": "sha512-wCQs9aCZJ/rOkcn5ZMn0GogpYf8lYl4cuB1uMLHNLsUOliqNuFXOu8wVwdseAMYqUMPWXuNvCUC6h1g6XM7mYw==",
       "dev": true,
       "requires": {
+        "@types/semver": "^7.2.0",
         "eslint-utils": "^2.0.0",
         "natural-compare": "^1.4.0",
         "semver": "^7.3.2",

+ 2 - 0
package.json

@@ -72,6 +72,8 @@
     "@babel/preset-env": "7.10.2",
     "@babel/preset-typescript": "7.10.1",
     "@types/jest": "26.0.0",
+    "@types/jquery": "3.5.0",
+    "@types/knockout": "3.4.67",
     "@typescript-eslint/eslint-plugin": "3.4.0",
     "@typescript-eslint/parser": "3.4.0",
     "babel-eslint": "10.1.0",