You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ju...@apache.org on 2023/08/24 23:54:14 UTC

[superset] branch master updated: chore(sqllab): typescript for getInitialState (#25047)

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

justinpark pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4d9e9a4b1c chore(sqllab): typescript for getInitialState (#25047)
4d9e9a4b1c is described below

commit 4d9e9a4b1c605d6ce98a99f4d8084fb920874c20
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Thu Aug 24 16:54:06 2023 -0700

    chore(sqllab): typescript for getInitialState (#25047)
---
 .../src/SqlLab/components/SouthPane/index.tsx      |   2 +-
 .../src/SqlLab/components/SqlEditor/index.jsx      |   2 +-
 .../SqlLab/components/SqlEditorLeftBar/index.tsx   |   3 +-
 .../SqlLab/components/SqlEditorTabHeader/index.tsx |   2 +-
 .../src/SqlLab/components/TableElement/index.tsx   |  11 +--
 .../src/SqlLab/hooks/useQueryEditor/index.ts       |   2 +-
 .../src/SqlLab/reducers/getInitialState.test.ts    | 108 +++++++++++++++++----
 .../{getInitialState.js => getInitialState.ts}     |  61 ++++++------
 superset-frontend/src/SqlLab/types.ts              |  22 ++++-
 .../src/hooks/apiResources/queryApi.ts             |   1 +
 .../src/hooks/apiResources/sqlLab.test.ts          | 100 +++++++++++++++++++
 superset-frontend/src/hooks/apiResources/sqlLab.ts |  77 +++++++++++++++
 12 files changed, 324 insertions(+), 67 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
index 4e38a76f9c..42ee4830fd 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
@@ -117,7 +117,7 @@ const SouthPane = ({
             queries[dataPreviewQueryId],
         )
         .map(({ name, dataPreviewQueryId }) => ({
-          ...queries[dataPreviewQueryId],
+          ...queries[dataPreviewQueryId || ''],
           tableName: name,
         }));
       const editorQueries = Object.values(queries).filter(
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
index cda252f1e3..2626d70d73 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
@@ -225,7 +225,7 @@ const SqlEditor = ({
   const { database, latestQuery, hideLeftBar } = useSelector(
     ({ sqlLab: { unsavedQueryEditor, databases, queries } }) => {
       let { dbId, latestQueryId, hideLeftBar } = queryEditor;
-      if (unsavedQueryEditor.id === queryEditor.id) {
+      if (unsavedQueryEditor?.id === queryEditor.id) {
         dbId = unsavedQueryEditor.dbId || dbId;
         latestQueryId = unsavedQueryEditor.latestQueryId || latestQueryId;
         hideLeftBar = unsavedQueryEditor.hideLeftBar || hideLeftBar;
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index e89bdc57b7..169acd82ce 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -27,6 +27,7 @@ import React, {
 import { useDispatch } from 'react-redux';
 import querystring from 'query-string';
 
+import { Table } from 'src/SqlLab/types';
 import {
   queryEditorSetDb,
   addTable,
@@ -52,7 +53,7 @@ import {
   LocalStorageKeys,
   setItem,
 } from 'src/utils/localStorageHelpers';
-import TableElement, { Table } from '../TableElement';
+import TableElement from '../TableElement';
 
 interface ExtendedTable extends Table {
   expanded: boolean;
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
index 1e1b22a81d..487af451a5 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
@@ -55,7 +55,7 @@ const SqlEditorTabHeader: React.FC<Props> = ({ queryEditor }) => {
   const qe = useSelector<SqlLabRootState, QueryEditor>(
     ({ sqlLab: { unsavedQueryEditor } }) => ({
       ...queryEditor,
-      ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
+      ...(queryEditor.id === unsavedQueryEditor?.id && unsavedQueryEditor),
     }),
     shallowEqual,
   );
diff --git a/superset-frontend/src/SqlLab/components/TableElement/index.tsx b/superset-frontend/src/SqlLab/components/TableElement/index.tsx
index 0559307fdf..b003655634 100644
--- a/superset-frontend/src/SqlLab/components/TableElement/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TableElement/index.tsx
@@ -18,6 +18,7 @@
  */
 import React, { useState, useRef, useEffect } from 'react';
 import { useDispatch } from 'react-redux';
+import type { Table } from 'src/SqlLab/types';
 import Collapse from 'src/components/Collapse';
 import Card from 'src/components/Card';
 import ButtonGroup from 'src/components/ButtonGroup';
@@ -49,16 +50,6 @@ export interface Column {
   type: string;
 }
 
-export interface Table {
-  id: string;
-  dbId: number;
-  schema: string;
-  name: string;
-  dataPreviewQueryId?: string | null;
-  expanded?: boolean;
-  initialized?: boolean;
-}
-
 export interface TableElementProps {
   table: Table;
 }
diff --git a/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts b/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts
index 7044e77798..89301716c0 100644
--- a/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts
+++ b/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts
@@ -29,7 +29,7 @@ export default function useQueryEditor<T extends keyof QueryEditor>(
       pick(
         {
           ...queryEditors.find(({ id }) => id === sqlEditorId),
-          ...(sqlEditorId === unsavedQueryEditor.id && unsavedQueryEditor),
+          ...(sqlEditorId === unsavedQueryEditor?.id && unsavedQueryEditor),
         },
         ['id'].concat(attributes),
       ) as Pick<QueryEditor, T | 'id'>,
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts b/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
index af074c3cc4..c4f064f650 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
@@ -17,37 +17,49 @@
  * under the License.
  */
 
+import { DEFAULT_COMMON_BOOTSTRAP_DATA } from 'src/constants';
 import getInitialState, { dedupeTabHistory } from './getInitialState';
 
 const apiData = {
-  common: {
-    conf: {
-      DEFAULT_SQLLAB_LIMIT: 1,
-    },
-  },
-  active_tab: null,
+  common: DEFAULT_COMMON_BOOTSTRAP_DATA,
   tab_state_ids: [],
   databases: [],
-  queries: [],
-  requested_query: null,
+  queries: {},
   user: {
     userId: 1,
     username: 'some name',
+    isActive: true,
+    isAnonymous: false,
+    firstName: 'first name',
+    lastName: 'last name',
+    permissions: {},
+    roles: {},
   },
 };
 const apiDataWithTabState = {
   ...apiData,
-  tab_state_ids: [{ id: 1 }],
-  active_tab: { id: 1, table_schemas: [] },
+  tab_state_ids: [{ id: 1, label: 'test' }],
+  active_tab: {
+    id: 1,
+    user_id: 1,
+    label: 'editor1',
+    active: true,
+    database_id: 1,
+    sql: '',
+    table_schemas: [],
+    saved_query: null,
+    template_params: null,
+    latest_query: null,
+  },
 };
 describe('getInitialState', () => {
   it('should output the user that is passed in', () => {
-    expect(getInitialState(apiData).user.userId).toEqual(1);
+    expect(getInitialState(apiData).user?.userId).toEqual(1);
   });
   it('should return undefined instead of null for templateParams', () => {
     expect(
-      getInitialState(apiDataWithTabState).sqlLab.queryEditors[0]
-        .templateParams,
+      getInitialState(apiDataWithTabState).sqlLab?.queryEditors?.[0]
+        ?.templateParams,
     ).toBeUndefined();
   });
 
@@ -55,13 +67,65 @@ describe('getInitialState', () => {
     it('should dedupe the tab history', () => {
       [
         { value: [], expected: [] },
-        { value: [12, 3, 4, 5, 6], expected: [12, 3, 4, 5, 6] },
-        { value: [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], expected: [1, 2] },
         {
-          value: [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3],
-          expected: [1, 2, 3],
+          value: ['12', '3', '4', '5', '6'],
+          expected: ['12', '3', '4', '5', '6'],
+        },
+        {
+          value: [
+            '1',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+          ],
+          expected: ['1', '2'],
+        },
+        {
+          value: [
+            '1',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '3',
+          ],
+          expected: ['1', '2', '3'],
+        },
+        {
+          value: [
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '2',
+            '3',
+          ],
+          expected: ['2', '3'],
         },
-        { value: [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3], expected: [2, 3] },
       ].forEach(({ value, expected }) => {
         expect(dedupeTabHistory(value)).toEqual(expected);
       });
@@ -92,6 +156,11 @@ describe('getInitialState', () => {
         ...apiData,
         active_tab: {
           id: 1,
+          user_id: 1,
+          label: 'editor1',
+          active: true,
+          database_id: 1,
+          sql: '',
           table_schemas: [
             {
               id: 1,
@@ -116,6 +185,9 @@ describe('getInitialState', () => {
               },
             },
           ],
+          saved_query: null,
+          template_params: null,
+          latest_query: null,
         },
       }).sqlLab.tables;
       expect(initializedTables.map(({ id }) => id)).toEqual([1, 2, 6]);
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.js b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
similarity index 82%
rename from superset-frontend/src/SqlLab/reducers/getInitialState.js
rename to superset-frontend/src/SqlLab/reducers/getInitialState.ts
index 5bc225824c..24db593530 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.js
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
@@ -18,9 +18,17 @@
  */
 import { t } from '@superset-ui/core';
 import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages';
+import type { BootstrapData } from 'src/types/bootstrapTypes';
+import type { InitialState } from 'src/hooks/apiResources/sqlLab';
+import type {
+  QueryEditor,
+  UnsavedQueryEditor,
+  SqlLabRootState,
+  Table,
+} from 'src/SqlLab/types';
 
-export function dedupeTabHistory(tabHistory) {
-  return tabHistory.reduce(
+export function dedupeTabHistory(tabHistory: string[]) {
+  return tabHistory.reduce<string[]>(
     (result, tabId) =>
       result.slice(-1)[0] === tabId ? result : result.concat(tabId),
     [],
@@ -34,7 +42,7 @@ export default function getInitialState({
   databases,
   queries: queries_,
   user,
-}) {
+}: BootstrapData & Partial<InitialState>) {
   /**
    * Before YYYY-MM-DD, the state for SQL Lab was stored exclusively in the
    * browser's localStorage. The feature flag `SQLLAB_BACKEND_PERSISTENCE`
@@ -43,54 +51,42 @@ export default function getInitialState({
    * To allow for a transparent migration, the initial state is a combination
    * of the backend state (if any) with the browser state (if any).
    */
-  let queryEditors = {};
+  let queryEditors: Record<string, QueryEditor> = {};
   const defaultQueryEditor = {
-    id: null,
     loaded: true,
     name: t('Untitled query'),
     sql: 'SELECT *\nFROM\nWHERE',
-    selectedText: null,
     latestQueryId: null,
     autorun: false,
-    templateParams: null,
     dbId: common.conf.SQLLAB_DEFAULT_DBID,
     queryLimit: common.conf.DEFAULT_SQLLAB_LIMIT,
-    validationResult: {
-      id: null,
-      errors: [],
-      completed: false,
-    },
     hideLeftBar: false,
+    remoteId: null,
   };
-  let unsavedQueryEditor = {};
+  let unsavedQueryEditor: UnsavedQueryEditor = {};
 
   /**
    * Load state from the backend. This will be empty if the feature flag
    * `SQLLAB_BACKEND_PERSISTENCE` is off.
    */
   tabStateIds.forEach(({ id, label }) => {
-    let queryEditor;
+    let queryEditor: QueryEditor;
     if (activeTab && activeTab.id === id) {
       queryEditor = {
         id: id.toString(),
         loaded: true,
         name: activeTab.label,
-        sql: activeTab.sql || undefined,
+        sql: activeTab.sql || '',
         selectedText: undefined,
         latestQueryId: activeTab.latest_query
           ? activeTab.latest_query.id
           : null,
-        remoteId: activeTab.saved_query?.id,
-        autorun: activeTab.autorun,
+        remoteId: activeTab.saved_query?.id || null,
+        autorun: Boolean(activeTab.autorun),
         templateParams: activeTab.template_params || undefined,
         dbId: activeTab.database_id,
         schema: activeTab.schema,
         queryLimit: activeTab.query_limit,
-        validationResult: {
-          id: null,
-          errors: [],
-          completed: false,
-        },
         hideLeftBar: activeTab.hide_left_bar,
       };
     } else {
@@ -109,7 +105,8 @@ export default function getInitialState({
   });
 
   const tabHistory = activeTab ? [activeTab.id.toString()] : [];
-  let tables = {};
+  let tables = {} as Record<string, Table>;
+  const editorTabLastUpdatedAt = Date.now();
   if (activeTab) {
     activeTab.table_schemas
       .filter(tableSchema => tableSchema.description !== null)
@@ -140,17 +137,18 @@ export default function getInitialState({
    * hasn't used SQL Lab after it has been turned on, the state will be stored
    * in the browser's local storage.
    */
-  if (
-    localStorage.getItem('redux') &&
-    JSON.parse(localStorage.getItem('redux')).sqlLab
-  ) {
-    const { sqlLab } = JSON.parse(localStorage.getItem('redux'));
+  const localStorageData = localStorage.getItem('redux');
+  const sqlLabCacheData = localStorageData
+    ? (JSON.parse(localStorageData) as Pick<SqlLabRootState, 'sqlLab'>)
+    : undefined;
+  if (localStorageData && sqlLabCacheData?.sqlLab) {
+    const { sqlLab } = sqlLabCacheData;
 
     if (sqlLab.queryEditors.length === 0) {
       // migration was successful
       localStorage.removeItem('redux');
     } else {
-      unsavedQueryEditor = sqlLab.unsavedQueryEditor || {};
+      unsavedQueryEditor = sqlLab.unsavedQueryEditor || unsavedQueryEditor;
       // add query editors and tables to state with a special flag so they can
       // be migrated if the `SQLLAB_BACKEND_PERSISTENCE` feature flag is on
       sqlLab.queryEditors.forEach(qe => {
@@ -199,11 +197,12 @@ export default function getInitialState({
       tabHistory: dedupeTabHistory(tabHistory),
       tables: Object.values(tables),
       queriesLastUpdate: Date.now(),
-      unsavedQueryEditor,
+      editorTabLastUpdatedAt,
       queryCostEstimates: {},
+      unsavedQueryEditor,
     },
     messageToasts: getToastsFromPyFlashMessages(
-      (common || {}).flash_messages || [],
+      (common || {})?.flash_messages || [],
     ),
     localStorageUsageInKilobytes: 0,
     common: {
diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts
index eb0d89a98d..d8a78a858d 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -33,7 +33,8 @@ export interface QueryEditor {
   id: string;
   dbId?: number;
   name: string;
-  schema: string;
+  title?: string; // keep it optional for backward compatibility
+  schema?: string;
   autorun: boolean;
   sql: string;
   remoteId: number | null;
@@ -43,6 +44,8 @@ export interface QueryEditor {
   selectedText?: string;
   queryLimit?: number;
   description?: string;
+  loaded?: boolean;
+  inLocalStorage?: boolean;
 }
 
 export type toastState = {
@@ -53,6 +56,19 @@ export type toastState = {
   noDuplicate: boolean;
 };
 
+export type UnsavedQueryEditor = Partial<QueryEditor>;
+
+export interface Table {
+  id: string;
+  dbId: number;
+  schema: string;
+  name: string;
+  queryEditorId: QueryEditor['id'];
+  dataPreviewQueryId: string | null;
+  expanded?: boolean;
+  initialized?: boolean;
+}
+
 export type SqlLabRootState = {
   sqlLab: {
     activeSouthPaneTab: string | number; // default is string; action.newQuery.id is number
@@ -63,10 +79,10 @@ export type SqlLabRootState = {
     queries: Record<string, QueryResponse>;
     queryEditors: QueryEditor[];
     tabHistory: string[]; // default is activeTab ? [activeTab.id.toString()] : []
-    tables: Record<string, any>[];
+    tables: Table[];
     queriesLastUpdate: number;
     errorMessage: string | null;
-    unsavedQueryEditor: Partial<QueryEditor>;
+    unsavedQueryEditor: UnsavedQueryEditor;
     queryCostEstimates?: Record<string, QueryCostEstimate>;
     editorTabLastUpdatedAt?: number;
   };
diff --git a/superset-frontend/src/hooks/apiResources/queryApi.ts b/superset-frontend/src/hooks/apiResources/queryApi.ts
index 2881fc2252..3461c8443e 100644
--- a/superset-frontend/src/hooks/apiResources/queryApi.ts
+++ b/superset-frontend/src/hooks/apiResources/queryApi.ts
@@ -71,6 +71,7 @@ export const api = createApi({
     'DatabaseFunctions',
     'QueryValidations',
     'TableMetadatas',
+    'SqlLabInitialState',
   ],
   endpoints: () => ({}),
   baseQuery: supersetClientQuery,
diff --git a/superset-frontend/src/hooks/apiResources/sqlLab.test.ts b/superset-frontend/src/hooks/apiResources/sqlLab.test.ts
new file mode 100644
index 0000000000..31c7a68b44
--- /dev/null
+++ b/superset-frontend/src/hooks/apiResources/sqlLab.test.ts
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 fetchMock from 'fetch-mock';
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+  createWrapper,
+  defaultStore as store,
+} from 'spec/helpers/testing-library';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { DEFAULT_COMMON_BOOTSTRAP_DATA } from 'src/constants';
+
+import { useSqlLabInitialState } from './sqlLab';
+
+const fakeApiResult = {
+  result: {
+    common: DEFAULT_COMMON_BOOTSTRAP_DATA,
+    tab_state_ids: [],
+    databases: [],
+    queries: {},
+    user: {
+      userId: 1,
+      username: 'some name',
+      isActive: true,
+      isAnonymous: false,
+      firstName: 'first name',
+      lastName: 'last name',
+      permissions: {},
+      roles: {},
+    },
+  },
+};
+
+const expectedResult = fakeApiResult.result;
+const sqlLabInitialStateApiRoute = `glob:*/api/v1/sqllab/`;
+
+afterEach(() => {
+  fetchMock.reset();
+  act(() => {
+    store.dispatch(api.util.resetApiState());
+  });
+});
+
+beforeEach(() => {
+  fetchMock.get(sqlLabInitialStateApiRoute, fakeApiResult);
+});
+
+test('returns api response mapping json result', async () => {
+  const { result, waitFor } = renderHook(() => useSqlLabInitialState(), {
+    wrapper: createWrapper({
+      useRedux: true,
+      store,
+    }),
+  });
+  await waitFor(() =>
+    expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1),
+  );
+  expect(result.current.data).toEqual(expectedResult);
+  expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1);
+  // clean up cache
+  act(() => {
+    store.dispatch(api.util.invalidateTags(['SqlLabInitialState']));
+  });
+  await waitFor(() =>
+    expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(2),
+  );
+  expect(result.current.data).toEqual(expectedResult);
+});
+
+test('returns cached data without api request', async () => {
+  const { result, waitFor, rerender } = renderHook(
+    () => useSqlLabInitialState(),
+    {
+      wrapper: createWrapper({
+        store,
+        useRedux: true,
+      }),
+    },
+  );
+  await waitFor(() => expect(result.current.data).toEqual(expectedResult));
+  expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1);
+  rerender();
+  await waitFor(() => expect(result.current.data).toEqual(expectedResult));
+  expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1);
+});
diff --git a/superset-frontend/src/hooks/apiResources/sqlLab.ts b/superset-frontend/src/hooks/apiResources/sqlLab.ts
new file mode 100644
index 0000000000..dac53043c3
--- /dev/null
+++ b/superset-frontend/src/hooks/apiResources/sqlLab.ts
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 type { QueryResponse } from '@superset-ui/core';
+import type { JsonResponse } from './queryApi';
+import { api } from './queryApi';
+
+export type InitialState = {
+  active_tab: {
+    id: number;
+    user_id: number;
+    label: string;
+    active: boolean;
+    database_id: number;
+    schema?: string;
+    table_schemas: {
+      id: number;
+      table: string;
+      description: {
+        columns?: {
+          name: string;
+          type: string;
+        }[];
+        dataPreviewQueryId?: string;
+      } & Record<string, any>;
+      schema?: string;
+      tab_state_id: number;
+      database_id?: number;
+      expanded?: boolean;
+    }[];
+    sql: string;
+    query_limit?: number;
+    latest_query: QueryResponse | null;
+    autorun?: boolean;
+    template_params: string | null;
+    hide_left_bar?: boolean;
+    saved_query: { id: number } | null;
+    extra_json?: object;
+  };
+  databases: object[];
+  queries: Record<string, QueryResponse & { inLocalStorage?: boolean }>;
+  tab_state_ids: {
+    id: number;
+    label: string;
+  }[];
+};
+
+const queryValidationApi = api.injectEndpoints({
+  endpoints: builder => ({
+    sqlLabInitialState: builder.query<InitialState, void>({
+      providesTags: ['SqlLabInitialState'],
+      query: () => ({
+        endpoint: `/api/v1/sqllab/`,
+        headers: { 'Content-Type': 'application/json' },
+        transformResponse: ({ json }: JsonResponse) => json.result,
+      }),
+    }),
+  }),
+});
+
+export const { useSqlLabInitialStateQuery: useSqlLabInitialState } =
+  queryValidationApi;