From 752b2c7519be22e44a99edfd9434008b44236fb0 Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Thu, 18 Nov 2021 18:09:55 +0000 Subject: [PATCH 01/13] replace runItSettings and configurator with OAuthConfigProvider --- packages/extension-utils/package.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/extension-utils/package.json b/packages/extension-utils/package.json index 06f220051..e59525a39 100644 --- a/packages/extension-utils/package.json +++ b/packages/extension-utils/package.json @@ -1,8 +1,8 @@ { "name": "@looker/extension-utils", - "version": "0.1.3", + "version": "0.1.0", "description": "Looker Extension Utilities", - "main": "lib/index.js", + "main": "dist/bundle.js", "module": "lib/esm/index.js", "sideEffects": false, "typings": "lib/index.d.ts", @@ -22,13 +22,17 @@ "private": false, "homepage": "https://github.com/looker-open-source/sdk-codegen/tree/main/packages/extension-utils", "scripts": { + "analyze": "export ANALYZE_MODE=static && yarn bundle", + "bundle": "tsc && webpack --config webpack.prod.config.js", + "deploy": "bin/deploy", + "develop": "webpack serve --hot --disable-host-check --port 8080 --https --config webpack.dev.config.js", "watch": "yarn lerna exec --scope @looker/extension-utils --stream 'BABEL_ENV=build babel src --root-mode upward --out-dir lib/esm --source-maps --extensions .ts,.tsx --no-comments --watch'" }, "dependencies": { "@looker/components": "^2.8.1", "@looker/extension-sdk": "^21.20.0", "@looker/extension-sdk-react": "^21.20.0", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk-rtl": "^21.2.0", "react": "^16.13.1" }, "devDependencies": { From 612c04ba78006103b4de610587298cccb30b3913 Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Fri, 3 Dec 2021 19:15:42 +0000 Subject: [PATCH 02/13] move specs to redux and cleanup oauth related code - Work around non-reentrant code - Cleanup OAuthScene and more tweaks to initSpecs - Better memory handling - chore: clean up extension-utils package.json - feat: move specs to redux --- packages/api-explorer/e2e/e2e.spec.ts | 2 +- packages/api-explorer/src/ApiExplorer.tsx | 107 +++++--------- .../src/StandaloneApiExplorer.tsx | 89 +++++------- .../src/components/Header/Header.tsx | 13 +- .../ApiSpecSelector.spec.tsx | 70 +++++---- .../SelectorContainer/ApiSpecSelector.tsx | 20 ++- .../SelectorContainer/SelectorContainer.tsx | 14 +- .../src/components/SideNav/SideNav.tsx | 4 - packages/api-explorer/src/index.ts | 1 + .../api-explorer/src/routes/AppRouter.tsx | 62 ++++---- .../src/scenes/DiffScene/DiffScene.tsx | 4 +- .../src/scenes/MethodScene/MethodScene.tsx | 14 +- packages/api-explorer/src/state/index.ts | 1 + .../src/state/specs}/index.ts | 4 +- .../src/state/specs/sagas.spec.ts | 135 ++++++++++++++++++ .../api-explorer/src/state/specs/sagas.ts | 83 +++++++++++ .../src/state/specs/selectors.ts} | 12 +- .../api-explorer/src/state/specs/slice.ts | 111 ++++++++++++++ packages/api-explorer/src/state/store.ts | 58 ++++++++ .../api-explorer/src/test-utils/redux.tsx | 20 ++- .../src/utils/adaptorUtils.ts} | 98 +------------ .../api-explorer/src/utils/apixAdaptor.ts | 72 ++++++++++ packages/api-explorer/src/utils/index.ts | 2 + packages/api-explorer/src/utils/path.ts | 5 + packages/extension-api-explorer/package.json | 4 +- .../src/ExtensionApiExplorer.tsx | 70 +++++---- packages/extension-utils/package.json | 9 +- .../src}/OAuthScene.tsx | 82 +++++------ packages/extension-utils/src/adaptorUtils.ts | 10 +- packages/extension-utils/src/authUtils.ts | 5 + .../extension-utils/src/browserAdaptor.ts | 10 +- .../extension-utils/src/extensionAdaptor.ts | 1 + packages/extension-utils/src/index.ts | 1 + packages/redux/src/createStore/index.ts | 2 +- packages/run-it/src/RunIt.tsx | 8 +- .../components/ConfigForm/ConfigForm.spec.tsx | 34 +---- .../src/components/ConfigForm/ConfigForm.tsx | 74 +++++----- .../run-it/src/components/ConfigForm/index.ts | 2 +- .../{configUtils.spec.ts => utils.spec.ts} | 2 +- .../run-it/src/components/ConfigForm/utils.ts | 78 ++++++++++ .../components/LoginForm/LoginForm.spec.tsx | 29 +++- .../src/components/LoginForm/LoginForm.tsx | 17 ++- .../components/RequestForm/RequestForm.tsx | 3 - packages/run-it/src/index.ts | 1 - .../src/testUtils/testUtils.ts | 2 +- .../sdk-codegen/src/testUtils/testUtils.ts | 2 +- packages/sdk-node/src/testUtils/testUtils.ts | 2 +- packages/sdk-rtl/src/oauthSession.ts | 15 +- yarn.lock | 111 +++++++++++--- 49 files changed, 1012 insertions(+), 563 deletions(-) rename packages/{run-it/src/scenes => api-explorer/src/state/specs}/index.ts (95%) create mode 100644 packages/api-explorer/src/state/specs/sagas.spec.ts create mode 100644 packages/api-explorer/src/state/specs/sagas.ts rename packages/{run-it/src/scenes/OAuthScene/index.ts => api-explorer/src/state/specs/selectors.ts} (73%) create mode 100644 packages/api-explorer/src/state/specs/slice.ts rename packages/{run-it/src/components/ConfigForm/configUtils.ts => api-explorer/src/utils/adaptorUtils.ts} (66%) create mode 100644 packages/api-explorer/src/utils/apixAdaptor.ts rename packages/{run-it/src/scenes/OAuthScene => extension-utils/src}/OAuthScene.tsx (50%) rename packages/run-it/src/components/ConfigForm/{configUtils.spec.ts => utils.spec.ts} (98%) create mode 100644 packages/run-it/src/components/ConfigForm/utils.ts diff --git a/packages/api-explorer/e2e/e2e.spec.ts b/packages/api-explorer/e2e/e2e.spec.ts index 2f7f46854..3003e602a 100644 --- a/packages/api-explorer/e2e/e2e.spec.ts +++ b/packages/api-explorer/e2e/e2e.spec.ts @@ -173,7 +173,7 @@ describe('API Explorer', () => { 'ul[aria-label="spec selector"] > li:last-child' ) await expect(page).toMatchElement('h2', { - text: 'Looker API 4.0 (Experimental) Reference', + text: 'Looker API 4.0 (Beta) Reference', }) }) }) diff --git a/packages/api-explorer/src/ApiExplorer.tsx b/packages/api-explorer/src/ApiExplorer.tsx index d30a48dcd..9cd0e8b29 100644 --- a/packages/api-explorer/src/ApiExplorer.tsx +++ b/packages/api-explorer/src/ApiExplorer.tsx @@ -25,8 +25,7 @@ */ import type { FC } from 'react' -import React, { useReducer, useState, useEffect, useCallback } from 'react' -import { useLocation } from 'react-router' +import React, { useState, useEffect, useCallback } from 'react' import styled, { createGlobalStyle } from 'styled-components' import { Aside, @@ -38,27 +37,24 @@ import { Page, Space, } from '@looker/components' -import type { SpecList } from '@looker/sdk-codegen' -import type { RunItSetter } from '@looker/run-it' -import { funFetch, fallbackFetch, OAuthScene } from '@looker/run-it' import { FirstPage } from '@styled-icons/material/FirstPage' import { LastPage } from '@styled-icons/material/LastPage' - -import type { IEnvironmentAdaptor } from '@looker/extension-utils' import { registerEnvAdaptor, unregisterEnvAdaptor, } from '@looker/extension-utils' -import { oAuthPath } from './utils' +import { useSelector } from 'react-redux' +import { useLocation } from 'react-router-dom' + +import type { IApixAdaptor } from './utils' import { Header, SideNav, ErrorBoundary, - Loader, SelectorContainer, HEADER_TOGGLE_LABEL, + Loader, } from './components' -import { specReducer, initDefaultSpecState, updateSpecApi } from './reducers' import { AppRouter } from './routes' import { apixFilesHost } from './utils/lodeUtils' import { @@ -66,12 +62,15 @@ import { useSettingStoreState, useLodeActions, useLodesStoreState, + useSpecActions, + useSpecStoreState, + selectCurrentSpec, + selectSpecs, } from './state' +import { specKeyFromPath } from './utils/path' export interface ApiExplorerProps { - specs: SpecList - adaptor: IEnvironmentAdaptor - setVersionsUrl: RunItSetter + adaptor: IApixAdaptor examplesLodeUrl?: string declarationsLodeUrl?: string headless?: boolean @@ -80,25 +79,21 @@ export interface ApiExplorerProps { const BodyOverride = createGlobalStyle` html { height: 100%; overflow: hidden; } ` export const ApiExplorer: FC = ({ - specs, adaptor, - setVersionsUrl, examplesLodeUrl = 'https://raw.githubusercontent.com/looker-open-source/sdk-codegen/main/examplesIndex.json', declarationsLodeUrl = `${apixFilesHost}/declarationsIndex.json`, headless = false, }) => { - const { initialized } = useSettingStoreState() + useSettingStoreState() useLodesStoreState() + const { working, description } = useSpecStoreState() + const specs = useSelector(selectSpecs) + const spec = useSelector(selectCurrentSpec) const { initLodesAction } = useLodeActions() const { initSettingsAction } = useSettingActions() - const location = useLocation() - const oauthReturn = location.pathname === `/${oAuthPath}` - const [specState, specDispatch] = useReducer( - specReducer, - initDefaultSpecState(specs, location) - ) - const { spec } = specState + const { initSpecsAction } = useSpecActions() + const location = useLocation() const [hasNavigation, setHasNavigation] = useState(true) const toggleNavigation = (target?: boolean) => setHasNavigation(target || !hasNavigation) @@ -109,11 +104,14 @@ export const ApiExplorer: FC = ({ } }, []) + registerEnvAdaptor(adaptor) + useEffect(() => { - registerEnvAdaptor(adaptor) initSettingsAction() initLodesAction({ examplesLodeUrl, declarationsLodeUrl }) + const specKey = specKeyFromPath(location.pathname) + initSpecsAction({ specKey }) return () => unregisterEnvAdaptor() }, []) @@ -128,26 +126,6 @@ export const ApiExplorer: FC = ({ } }, [headless, hasNavigationToggle]) - useEffect(() => { - const loadSpec = async () => { - if (!spec.api) { - try { - const newSpec = { ...spec } - const api = await fallbackFetch(newSpec, funFetch) - if (api) { - spec.api = api - specDispatch(updateSpecApi(spec.key, api)) - } - } catch (error) { - console.error(error) - } - } - } - if (!oauthReturn) { - loadSpec() - } - }, [spec, location]) - const themeOverrides = adaptor.themeOverrides() return ( @@ -156,18 +134,13 @@ export const ApiExplorer: FC = ({ loadGoogleFonts={themeOverrides.loadGoogleFonts} themeCustomizations={themeOverrides.themeCustomizations} > - {!initialized ? ( - + {working ? ( + ) : ( {!headless && ( -
+
)} = ({ <> )} )} - {hasNavigation && ( - - )} + {hasNavigation && } - {oauthReturn && } - {!oauthReturn && spec.api && ( - - )} + diff --git a/packages/api-explorer/src/StandaloneApiExplorer.tsx b/packages/api-explorer/src/StandaloneApiExplorer.tsx index 0add51f8d..26b3548f0 100644 --- a/packages/api-explorer/src/StandaloneApiExplorer.tsx +++ b/packages/api-explorer/src/StandaloneApiExplorer.tsx @@ -26,19 +26,15 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' -import { - RunItProvider, - loadSpecsFromVersions, - RunItConfigKey, - RunItNoConfig, - initRunItSdk, -} from '@looker/run-it' -import type { SpecList } from '@looker/sdk-codegen' +import { initRunItSdk, RunItProvider } from '@looker/run-it' +import type { OAuthConfigProvider } from '@looker/extension-utils' +import { OAuthScene } from '@looker/extension-utils' import { Provider } from 'react-redux' -import { BrowserAdaptor } from '@looker/extension-utils' +import { useLocation } from 'react-router' import { ApiExplorer } from './ApiExplorer' import { store } from './state' +import { oAuthPath, ApixAdaptor } from './utils' import { Loader } from './components' export interface StandaloneApiExplorerProps { @@ -46,59 +42,48 @@ export interface StandaloneApiExplorerProps { versionsUrl: string } -const browserAdaptor = new BrowserAdaptor(initRunItSdk()) - -const loadVersions = async (current: string) => { - const data = await browserAdaptor.localStorageGetItem(RunItConfigKey) - const config = data ? JSON.parse(data) : RunItNoConfig - let url = config.base_url ? `${config.base_url}/versions` : current - let response = await loadSpecsFromVersions(url) - if (response.fetchResult) { - console.error( - `Reverting to ${current} due to ${url} error: ${response.fetchResult}` - ) - // The stored server location has an error so default to current - url = current - response = await loadSpecsFromVersions(url) - } - return { url, response } -} - export const StandaloneApiExplorer: FC = ({ headless = false, - versionsUrl = '', }) => { - const [specs, setSpecs] = useState() - const [currentVersionsUrl, setCurrentVersionsUrl] = - useState(versionsUrl) + const [browserAdaptor] = useState( + new ApixAdaptor(initRunItSdk(), window.origin) + ) + const location = useLocation() + const oauthReturn = location.pathname === `/${oAuthPath}` + const sdk = browserAdaptor.sdk + const canLogin = + (sdk.authSession.settings as OAuthConfigProvider).authIsConfigured() && + !sdk.authSession.isAuthenticated() && + !oauthReturn useEffect(() => { - if (currentVersionsUrl) { - loadVersions(currentVersionsUrl).then((result) => { - setCurrentVersionsUrl(result.url) - const response = result.response - setSpecs(response.specs) - }) - } else { - setSpecs(undefined) + const login = async () => await browserAdaptor.login() + if (canLogin) { + login() } - }, [versionsUrl, currentVersionsUrl]) + }, []) + + const { looker_url } = ( + sdk.authSession.settings as OAuthConfigProvider + ).getStoredConfig() return ( - <> - {specs ? ( - - ) : ( - - )} - + {canLogin ? ( + + ) : ( + <> + {oauthReturn ? ( + + ) : ( + + )} + + )} ) diff --git a/packages/api-explorer/src/components/Header/Header.tsx b/packages/api-explorer/src/components/Header/Header.tsx index 4056e5d0d..1cac35d49 100644 --- a/packages/api-explorer/src/components/Header/Header.tsx +++ b/packages/api-explorer/src/components/Header/Header.tsx @@ -24,7 +24,7 @@ */ -import type { FC, Dispatch } from 'react' +import type { FC } from 'react' import React from 'react' import styled from 'styled-components' import { @@ -36,19 +36,14 @@ import { } from '@looker/components' import { LookerLogo } from '@looker/icons' import { Menu } from '@styled-icons/material/Menu' -import type { SpecList, SpecItem } from '@looker/sdk-codegen' +import type { SpecItem } from '@looker/sdk-codegen' import { Link } from '../Link' -import type { SpecAction } from '../../reducers' import { SelectorContainer } from '../SelectorContainer' interface HeaderProps { - /** Specs to choose from */ - specs: SpecList /** Current selected spec */ spec: SpecItem - /** Spec state setter */ - specDispatch: Dispatch /** Nav state setter */ toggleNavigation: (target?: boolean) => void className?: string @@ -63,9 +58,7 @@ export const HEADER_TOGGLE_LABEL = 'Toggle Navigation' */ export const HeaderLayout: FC = ({ className, - specs, spec, - specDispatch, toggleNavigation, }) => ( = ({ - + ) diff --git a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx index cb09519c5..d879c2d80 100644 --- a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx @@ -24,12 +24,16 @@ */ import React from 'react' -import { act, screen, waitFor } from '@testing-library/react' -import { renderWithTheme } from '@looker/components-test-utils' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import * as reactRedux from 'react-redux' import { specs, getLoadedSpecState } from '../../test-data' -import { renderWithRouterAndReduxProvider } from '../../test-utils' +import { + renderWithReduxProvider, + renderWithRouterAndReduxProvider, +} from '../../test-utils' +import { useSpecActions } from '../../state' import { ApiSpecSelector } from './ApiSpecSelector' const specState = getLoadedSpecState() @@ -44,31 +48,24 @@ jest.mock('react-router-dom', () => { } }) +jest.mock('../../state/specs', () => ({ + ...(jest.requireActual('../../state/specs') as Record), + useSpecActions: jest + .fn() + .mockReturnValue({ setCurrentSpecAction: jest.fn() }), +})) + describe('ApiSpecSelector', () => { - const specDispatch = jest.fn() Element.prototype.scrollIntoView = jest.fn() - test('the default spec is selected by default', () => { - renderWithTheme( - - ) - + test('the current spec is selected by default', () => { + renderWithReduxProvider() const selector = screen.getByRole('textbox') expect(selector).toHaveValue(`${specState.spec.key}`) }) test('it lists all available specs', async () => { - renderWithTheme( - - ) + renderWithReduxProvider() userEvent.click(screen.getByRole('textbox')) await waitFor(() => { expect(screen.getAllByRole('option')).toHaveLength( @@ -77,25 +74,22 @@ describe('ApiSpecSelector', () => { }) }) - test('it fires a SELECT_SPEC action when another spec is selected', async () => { - renderWithRouterAndReduxProvider( - - ) + test('fetches selected spec', async () => { + const mockDispatch = jest.fn() + jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch) + const { setCurrentSpecAction } = useSpecActions() - await act(async () => { - await userEvent.click(screen.getByRole('textbox')) - await userEvent.click(screen.getByRole('option', { name: '3.1 current' })) - await waitFor(() => { - expect(specDispatch).toHaveBeenCalledTimes(1) - expect(specDispatch).toHaveBeenCalledWith({ - type: 'SELECT_SPEC', - payload: '3.1', - }) - }) + renderWithRouterAndReduxProvider() + userEvent.click(screen.getByRole('textbox')) + await waitFor(() => { + expect(screen.getAllByRole('option')).toHaveLength( + Object.keys(specs).length + ) + }) + const button = screen.getByText('3.1') + userEvent.click(button) + expect(setCurrentSpecAction).toHaveBeenCalledWith({ + currentSpecKey: '3.1', }) }) }) diff --git a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx index 538d77774..8818edab9 100644 --- a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx @@ -24,28 +24,24 @@ */ -import type { FC, Dispatch } from 'react' +import type { FC } from 'react' import React from 'react' import { Select } from '@looker/components' import { useHistory, useLocation } from 'react-router-dom' +import type { SpecItem } from '@looker/sdk-codegen' +import { useSelector } from 'react-redux' -import type { SpecList, SpecItem } from '@looker/sdk-codegen' -import type { SpecAction } from '../../reducers' -import { selectSpec } from '../../reducers' +import { selectSpecs, useSpecActions } from '../../state' interface ApiSpecSelectorProps { - specs: SpecList spec: SpecItem - specDispatch: Dispatch } -export const ApiSpecSelector: FC = ({ - specs, - spec, - specDispatch, -}) => { +export const ApiSpecSelector: FC = ({ spec }) => { const history = useHistory() const location = useLocation() + const specs = useSelector(selectSpecs) + const { setCurrentSpecAction } = useSpecActions() const options = Object.entries(specs).map(([key, spec]) => ({ value: key, label: key, @@ -53,7 +49,7 @@ export const ApiSpecSelector: FC = ({ })) const handleChange = (specKey: string) => { - specDispatch(selectSpec(specKey)) + setCurrentSpecAction({ currentSpecKey: specKey }) const matchPath = location.pathname.replace(`/${spec.key}`, `/${specKey}`) history.push(matchPath) } diff --git a/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx b/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx index 7b90be579..de7738717 100644 --- a/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx @@ -24,25 +24,21 @@ */ -import type { FC, Dispatch } from 'react' +import type { FC } from 'react' import React from 'react' import type { SpaceHelperProps } from '@looker/components' import { Space, IconButton } from '@looker/components' import { ChangeHistory } from '@styled-icons/material/ChangeHistory' -import type { SpecList, SpecItem } from '@looker/sdk-codegen' +import type { SpecItem } from '@looker/sdk-codegen' + import { Link } from '../Link' -import type { SpecAction } from '../../reducers' import { diffPath } from '../../utils' import { SdkLanguageSelector } from './SdkLanguageSelector' import { ApiSpecSelector } from './ApiSpecSelector' interface SelectorContainerProps extends SpaceHelperProps { - /** Specs to choose from */ - specs: SpecList /** Current selected spec */ spec: SpecItem - /** Spec state setter */ - specDispatch: Dispatch } export const HEADER_REM = 4 @@ -51,14 +47,12 @@ export const HEADER_REM = 4 * Renders a container for selectors */ export const SelectorContainer: FC = ({ - specs, spec, - specDispatch, ...spaceProps }) => ( - + } export const SideNav: FC = ({ headless = false, spec }) => { diff --git a/packages/api-explorer/src/index.ts b/packages/api-explorer/src/index.ts index b958d63ac..63db161ef 100644 --- a/packages/api-explorer/src/index.ts +++ b/packages/api-explorer/src/index.ts @@ -27,3 +27,4 @@ export * from './ApiExplorer' export * from './StandaloneApiExplorer' export * from './components' export * from './state' +export * from './utils' diff --git a/packages/api-explorer/src/routes/AppRouter.tsx b/packages/api-explorer/src/routes/AppRouter.tsx index 13b1afd7a..3303d321e 100644 --- a/packages/api-explorer/src/routes/AppRouter.tsx +++ b/packages/api-explorer/src/routes/AppRouter.tsx @@ -28,8 +28,6 @@ import type { FC } from 'react' import React from 'react' import { Redirect, Route, Switch } from 'react-router-dom' import type { ApiModel, SpecList } from '@looker/sdk-codegen' -import type { RunItSetter } from '@looker/run-it' -import type { IEnvironmentAdaptor } from '@looker/extension-utils' import { HomeScene, @@ -42,47 +40,37 @@ import { DiffScene } from '../scenes/DiffScene' import { diffPath } from '../utils' interface AppRouterProps { - api: ApiModel specKey: string specs: SpecList + api: ApiModel toggleNavigation: (target?: boolean) => void - adaptor: IEnvironmentAdaptor - setVersionsUrl: RunItSetter } export const AppRouter: FC = ({ - specKey, api, + specKey, specs, toggleNavigation, - adaptor, - setVersionsUrl, -}) => { - return ( - - - - - - - - - - - - - - - - - - - - - - ) -} +}) => ( + + + + + + + + + + + + + + + + + + + + + +) diff --git a/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx b/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx index 3f6413843..59a09b9fa 100644 --- a/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx +++ b/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx @@ -38,9 +38,9 @@ import { SelectMulti, } from '@looker/components' import { SyncAlt } from '@styled-icons/material/SyncAlt' -import { fallbackFetch, funFetch } from '@looker/run-it' + import { getDefaultSpecKey } from '../../reducers/spec/utils' -import { diffPath } from '../../utils' +import { diffPath, fallbackFetch, funFetch } from '../../utils' import { ApixSection } from '../../components' import { diffSpecs, standardDiffToggles } from './diffUtils' import { DocDiff } from './DocDiff' diff --git a/packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx b/packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx index 263dd1860..e1282e0df 100644 --- a/packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx +++ b/packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx @@ -36,13 +36,13 @@ import { } from '@looker/components' import { Beaker } from '@looker/icons' import { useHistory, useParams } from 'react-router-dom' -import type { RunItSetter } from '@looker/run-it' import { RunIt, RunItFormKey } from '@looker/run-it' import type { ApiModel } from '@looker/sdk-codegen' import { typeRefs } from '@looker/sdk-codegen' import { useSelector } from 'react-redux' - import type { IEnvironmentAdaptor } from '@looker/extension-utils' + +import { getApixAdaptor } from '../../utils' import { ApixSection, DocActivityType, @@ -62,8 +62,6 @@ import { DocOperation, DocRequestBody } from './components' interface MethodSceneProps { api: ApiModel - adaptor: IEnvironmentAdaptor - setVersionsUrl: RunItSetter } interface MethodSceneParams { @@ -77,11 +75,8 @@ const showRunIt = async (adaptor: IEnvironmentAdaptor) => { return !!data } -export const MethodScene: FC = ({ - api, - adaptor, - setVersionsUrl, -}) => { +export const MethodScene: FC = ({ api }) => { + const adaptor = getApixAdaptor() const history = useHistory() const sdkLanguage = useSelector(selectSdkLanguage) const { specKey, methodTag, methodName } = useParams() @@ -175,7 +170,6 @@ export const MethodScene: FC = ({ sdkLanguage={sdkLanguage} api={api} method={method} - setVersionsUrl={setVersionsUrl} /> diff --git a/packages/api-explorer/src/state/index.ts b/packages/api-explorer/src/state/index.ts index dcb854d53..53aecd36a 100644 --- a/packages/api-explorer/src/state/index.ts +++ b/packages/api-explorer/src/state/index.ts @@ -26,3 +26,4 @@ export * from './store' export * from './settings' export * from './lodes' +export * from './specs' diff --git a/packages/run-it/src/scenes/index.ts b/packages/api-explorer/src/state/specs/index.ts similarity index 95% rename from packages/run-it/src/scenes/index.ts rename to packages/api-explorer/src/state/specs/index.ts index abf350e64..a06152c6f 100644 --- a/packages/run-it/src/scenes/index.ts +++ b/packages/api-explorer/src/state/specs/index.ts @@ -23,5 +23,5 @@ SOFTWARE. */ - -export { OAuthScene } from './OAuthScene' +export * from './selectors' +export * from './slice' diff --git a/packages/api-explorer/src/state/specs/sagas.spec.ts b/packages/api-explorer/src/state/specs/sagas.spec.ts new file mode 100644 index 000000000..4274c2029 --- /dev/null +++ b/packages/api-explorer/src/state/specs/sagas.spec.ts @@ -0,0 +1,135 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ +import ReduxSagaTester from 'redux-saga-tester' +import { registerTestEnvAdaptor } from '@looker/extension-utils' +import { initRunItSdk } from '@looker/run-it' +import cloneDeep from 'lodash/cloneDeep' +import { ApixAdaptor } from '../../utils' + +import { getLoadedSpecs } from '../../test-data' +import { specActions, specsSlice } from './slice' +import * as sagas from './sagas' + +describe('Specs Sagas', () => { + let sagaTester: ReduxSagaTester + const adaptor = new ApixAdaptor(initRunItSdk(), '') + registerTestEnvAdaptor(adaptor) + const specState = getLoadedSpecs() + const mockError = new Error('boom') + + beforeEach(() => { + jest.resetAllMocks() + sagaTester = new ReduxSagaTester({ + initialState: { specs: { specs: specState } }, + reducers: { + specs: specsSlice.reducer, + }, + }) + sagaTester.start(sagas.saga) + }) + + describe('initSaga', () => { + const { initSpecsAction, initSpecsFailureAction, initSpecsSuccessAction } = + specActions + + it('sends initSpecsFailureAction on error', async () => { + jest.spyOn(adaptor, 'fetchSpecList').mockRejectedValueOnce(mockError) + + sagaTester.dispatch(initSpecsAction({ specKey: null })) + await sagaTester.waitFor('specs/initSpecsFailureAction') + const calledActions = sagaTester.getCalledActions() + expect(calledActions).toHaveLength(2) + expect(calledActions[0]).toEqual(initSpecsAction({ specKey: null })) + expect(calledActions[1]).toEqual(initSpecsFailureAction(mockError)) + }) + + it('sends initSpecsSuccessAction on success', async () => { + // fetchSpecList returns the test specs + jest + .spyOn(adaptor, 'fetchSpecList') + .mockResolvedValueOnce(cloneDeep(specState)) + + const currentSpec = specState['3.1'] + jest.spyOn(adaptor, 'fetchSpec').mockResolvedValueOnce(currentSpec!) + + // expected state is all the specs with the current spec containing the ApiModel + const expected = cloneDeep(specState) + expected[currentSpec.key] = currentSpec + + sagaTester.dispatch(initSpecsAction({ specKey: null })) + await sagaTester.waitFor('specs/initSpecsSuccessAction') + const calledActions = sagaTester.getCalledActions() + expect(calledActions).toHaveLength(2) + expect(calledActions[0]).toEqual(initSpecsAction({ specKey: null })) + expect(calledActions[1]).toEqual( + initSpecsSuccessAction({ + specs: expected, + currentSpecKey: currentSpec.key, + }) + ) + }) + }) + + describe('setCurrentSpecSaga', () => { + const { + setCurrentSpecAction, + setCurrentSpecSuccessAction, + setCurrentSpecFailureAction, + } = specActions + const spec = specState['4.0'] + + it('sends setCurrentSpecSuccessAction on success', async () => { + jest.spyOn(adaptor, 'fetchSpec').mockResolvedValueOnce(spec) + + sagaTester.dispatch(setCurrentSpecAction({ currentSpecKey: spec.key })) + await sagaTester.waitFor('specs/setCurrentSpecAction') + const calledActions = sagaTester.getCalledActions() + expect(calledActions).toHaveLength(2) + expect(calledActions[0]).toEqual( + setCurrentSpecAction({ currentSpecKey: spec.key }) + ) + expect(calledActions[1]).toEqual( + setCurrentSpecSuccessAction({ + api: spec.api!, + currentSpecKey: spec.key, + }) + ) + }) + + it('sends setCurrentSpecFailureAction on failure', async () => { + jest.spyOn(adaptor, 'fetchSpec').mockRejectedValueOnce(mockError) + + sagaTester.dispatch(setCurrentSpecAction({ currentSpecKey: spec.key })) + await sagaTester.waitFor('specs/setCurrentSpecAction') + const calledActions = sagaTester.getCalledActions() + expect(calledActions).toHaveLength(2) + expect(calledActions[0]).toEqual( + setCurrentSpecAction({ currentSpecKey: spec.key }) + ) + expect(calledActions[1]).toEqual(setCurrentSpecFailureAction(mockError)) + }) + }) +}) diff --git a/packages/api-explorer/src/state/specs/sagas.ts b/packages/api-explorer/src/state/specs/sagas.ts new file mode 100644 index 000000000..10b497997 --- /dev/null +++ b/packages/api-explorer/src/state/specs/sagas.ts @@ -0,0 +1,83 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ +import type { SpecItem, SpecList } from '@looker/sdk-codegen' +import { call, put, takeEvery, select } from 'typed-redux-saga' +import type { PayloadAction } from '@reduxjs/toolkit' + +import { getApixAdaptor } from '../../utils' +import type { RootState } from '../store' +import type { InitSpecsAction, SetCurrentSpecAction } from './slice' +import { specActions } from './slice' + +function* initSaga(action: PayloadAction) { + const { initSpecsSuccessAction, initSpecsFailureAction } = specActions + const adaptor = getApixAdaptor() + + try { + const specs: SpecList = yield* call([adaptor, 'fetchSpecList']) + let currentSpecKey = action.payload.specKey + if (!currentSpecKey) { + currentSpecKey = Object.values(specs).find( + (spec) => spec.status === 'current' + )!.key + } + const spec = yield* call([adaptor, 'fetchSpec'], specs[currentSpecKey]) + specs[currentSpecKey] = spec + yield* put(initSpecsSuccessAction({ specs, currentSpecKey })) + } catch (error: any) { + yield* put(initSpecsFailureAction(new Error(error.message))) + } +} + +export function* setCurrentSpecSaga( + action: PayloadAction +) { + const { setCurrentSpecSuccessAction, setCurrentSpecFailureAction } = + specActions + const adaptor = getApixAdaptor() + const spec: SpecItem = yield* select( + (state: RootState) => state.specs.specs[action.payload.currentSpecKey] + ) + + try { + const newSpec = yield* call([adaptor, 'fetchSpec'], spec) + yield* put( + setCurrentSpecSuccessAction({ + api: newSpec.api!, + currentSpecKey: action.payload.currentSpecKey, + }) + ) + } catch (error: any) { + yield put(setCurrentSpecFailureAction(new Error(error.message))) + } +} + +export function* saga() { + const { initSpecsAction, setCurrentSpecAction } = specActions + + yield* takeEvery(initSpecsAction, initSaga) + yield* takeEvery(setCurrentSpecAction, setCurrentSpecSaga) +} diff --git a/packages/run-it/src/scenes/OAuthScene/index.ts b/packages/api-explorer/src/state/specs/selectors.ts similarity index 73% rename from packages/run-it/src/scenes/OAuthScene/index.ts rename to packages/api-explorer/src/state/specs/selectors.ts index abf350e64..a840cb578 100644 --- a/packages/run-it/src/scenes/OAuthScene/index.ts +++ b/packages/api-explorer/src/state/specs/selectors.ts @@ -23,5 +23,15 @@ SOFTWARE. */ +import type { SpecItem } from '@looker/sdk-codegen' -export { OAuthScene } from './OAuthScene' +import type { RootState } from '../store' + +const selectSpecsState = (state: RootState) => state.specs + +export const selectSpecs = (state: RootState) => selectSpecsState(state).specs + +export const selectCurrentSpec = (state: RootState): SpecItem => { + const specState = selectSpecsState(state) + return specState.specs[specState.currentSpecKey] +} diff --git a/packages/api-explorer/src/state/specs/slice.ts b/packages/api-explorer/src/state/specs/slice.ts new file mode 100644 index 000000000..cff789cdc --- /dev/null +++ b/packages/api-explorer/src/state/specs/slice.ts @@ -0,0 +1,111 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import type { ApiModel, SpecList } from '@looker/sdk-codegen' +import { createSliceHooks } from '@looker/redux' + +import { saga } from './sagas' + +export interface SpecState { + specs: SpecList + currentSpecKey: string + error?: Error + working: boolean + description?: string +} + +export const defaultSpecsState: SpecState = { + specs: {}, + currentSpecKey: '', + working: true, + description: undefined, +} + +export interface InitSpecsAction { + specKey: string | null +} + +export interface InitSpecsSuccessPayload { + specs: SpecList + currentSpecKey: string + // api: ApiModel +} + +export interface SetCurrentSpecAction { + currentSpecKey: string +} + +interface SetCurrentSpecSuccessAction { + api: ApiModel + currentSpecKey: string +} + +export const specsSlice = createSlice({ + name: 'specs', + initialState: defaultSpecsState, + reducers: { + initSpecsAction(state, _action: PayloadAction) { + state.working = true + state.description = 'Fetching specifications...' + }, + initSpecsSuccessAction( + state, + action: PayloadAction + ) { + state.specs = action.payload.specs + state.currentSpecKey = action.payload.currentSpecKey + // state.specs[action.payload.currentSpecKey].api = action.payload.api + state.working = false + state.description = undefined + }, + initSpecsFailureAction(state, action: PayloadAction) { + state.working = false + state.error = action.payload + }, + setCurrentSpecAction(state, action: PayloadAction) { + state.working = true + state.description = `Fetching API ${action.payload.currentSpecKey} spec` + }, + setCurrentSpecSuccessAction( + state, + action: PayloadAction + ) { + state.currentSpecKey = action.payload.currentSpecKey + state.specs[state.currentSpecKey].api = action.payload.api + state.description = undefined + state.working = false + }, + setCurrentSpecFailureAction(state, action: PayloadAction) { + state.working = false + state.error = action.payload + }, + }, +}) + +export const specActions = specsSlice.actions +export const { useActions: useSpecActions, useStoreState: useSpecStoreState } = + createSliceHooks(specsSlice, saga) diff --git a/packages/api-explorer/src/state/store.ts b/packages/api-explorer/src/state/store.ts index ed0b437a1..bc419205f 100644 --- a/packages/api-explorer/src/state/store.ts +++ b/packages/api-explorer/src/state/store.ts @@ -24,24 +24,82 @@ */ import { createStore } from '@looker/redux' +import type { PayloadAction } from '@reduxjs/toolkit' +import type { Action } from 'redux' +import map from 'lodash/map' +import type { SpecList } from '@looker/sdk-codegen' import type { SettingState } from './settings' import { defaultSettingsState, settingsSlice } from './settings' import type { LodesState } from './lodes' import { lodesSlice, defaultLodesState } from './lodes' +import type { SpecState, InitSpecsSuccessPayload } from './specs' +import { defaultSpecsState, specsSlice } from './specs' + +const isInitSuccessAction = ( + action: any +): action is PayloadAction => !!action.payload?.specs + +const actionSanitizer = >( + action: A, + _id: number +): A => { + if (isInitSuccessAction(action)) { + action = { + ...action, + payload: { + ...action.payload, + specs: sanitizeSpecs(action.payload.specs), + }, + } + } + return action +} + +const stateSanitizer = >( + state: S, + _index: number +): S => { + if (state.specs) { + return { + ...state, + specs: { + ...state.specs, + specs: sanitizeSpecs(state.specs.specs), + }, + } + } + return state +} + +const sanitizeSpecs = (specList: SpecList) => + map(specList, (spec) => ({ + ...spec, + api: spec.api ? '' : undefined, + specContent: spec.specContent ? '' : undefined, + })) as unknown as SpecList + +const devTools = + process.env.NODE_ENV !== 'production' + ? { actionSanitizer, stateSanitizer } + : false export const store = createStore({ + devTools, preloadedState: { settings: defaultSettingsState, lodes: defaultLodesState, + specs: defaultSpecsState, }, reducer: { settings: settingsSlice.reducer, lodes: lodesSlice.reducer, + specs: specsSlice.reducer, }, }) export interface RootState { settings: SettingState lodes: LodesState + specs: SpecState } diff --git a/packages/api-explorer/src/test-utils/redux.tsx b/packages/api-explorer/src/test-utils/redux.tsx index 23a31e07b..18db62e33 100644 --- a/packages/api-explorer/src/test-utils/redux.tsx +++ b/packages/api-explorer/src/test-utils/redux.tsx @@ -34,19 +34,21 @@ import { createStore } from '@looker/redux' import { BrowserAdaptor, registerEnvAdaptor } from '@looker/extension-utils' import { initRunItSdk } from '@looker/run-it' -import type { LodesState, RootState, SettingState } from '../state' +import type { LodesState, RootState, SettingState, SpecState } from '../state' import { settingsSlice, defaultLodesState, defaultSettingsState, - store as defaultStore, lodesSlice, + defaultSpecsState, + specsSlice, } from '../state' +import { specs } from '../test-data' import { renderWithRouter } from './router' export const withReduxProvider = ( consumers: ReactElement, - store: Store = defaultStore + store: Store = createTestStore() ) => { registerEnvAdaptor(new BrowserAdaptor(initRunItSdk())) return {consumers} @@ -69,6 +71,7 @@ export const renderWithRouterAndReduxProvider = ( export const preloadedState: RootState = { settings: defaultSettingsState, lodes: defaultLodesState, + specs: defaultSpecsState, } type DeepPartial = { @@ -86,6 +89,15 @@ export const createTestStore = (overrides?: DeepPartial) => ...defaultLodesState, ...overrides?.lodes, } as LodesState, + specs: { + ...defaultSpecsState, + specs, + ...overrides?.specs, + } as SpecState, + }, + reducer: { + settings: settingsSlice.reducer, + lodes: lodesSlice.reducer, + specs: specsSlice.reducer, }, - reducer: { settings: settingsSlice.reducer, lodes: lodesSlice.reducer }, }) diff --git a/packages/run-it/src/components/ConfigForm/configUtils.ts b/packages/api-explorer/src/utils/adaptorUtils.ts similarity index 66% rename from packages/run-it/src/components/ConfigForm/configUtils.ts rename to packages/api-explorer/src/utils/adaptorUtils.ts index 8b6eaaf10..3046d2d1d 100644 --- a/packages/run-it/src/components/ConfigForm/configUtils.ts +++ b/packages/api-explorer/src/utils/adaptorUtils.ts @@ -25,21 +25,13 @@ */ import type { ILookerVersions, SpecItem, SpecList } from '@looker/sdk-codegen' -import { - ApiModel, - getSpecsFromVersions, - upgradeSpecObject, -} from '@looker/sdk-codegen' -import { BrowserTransport, DefaultSettings } from '@looker/sdk-rtl' +import { ApiModel, upgradeSpecObject } from '@looker/sdk-codegen' import { api_spec } from '@looker/sdk' import { getEnvAdaptor } from '@looker/extension-utils' - -import type { RunItValues } from '../..' +import type { RunItValues } from '@looker/run-it' +import { getUrl } from '@looker/run-it' export type StorageLocation = 'session' | 'local' -export const RunItConfigKey = 'RunItConfig' -export const RunItFormKey = 'RunItForm' -export const RunItNoConfig = { base_url: '', looker_url: '' } /** Object returned from storage service */ export interface IStorageValue { @@ -84,21 +76,6 @@ export interface IAPIXConfig extends ILookerVersions { headless?: boolean } -/** - * Validates URL and standardizes it - * @param url to validate - * @returns the standardized url.origin if it's valid, or an empty string if it's not - */ -export const validateUrl = (url: string): string => { - try { - const result = new URL(url) - if (url.endsWith(':')) return url - return result.origin - } catch { - return '' - } -} - /** * Convert content into an ApiModel * @param content to convert @@ -114,20 +91,6 @@ const makeApi = (content: string | RunItValues) => { return ApiModel.fromJson(json) } -/** - * Use the browser transport to GET a url - * @param url to fetch - */ -export const getUrl = async (url: string): Promise => { - const settings = { - ...DefaultSettings(), - ...{ base_url: url, verify_ssl: false }, - } - const xp = new BrowserTransport(settings) - const response = await xp.rawRequest('GET', url) - return response.body -} - /** * Ensure the URI is a full URL * @param uri possible relative path @@ -170,61 +133,6 @@ export const specUrlFetch = async (url: string): Promise => { } } -/** - * Load versions payload and retrieve all supported specs - * - * The versions payload should match the structure of Looker's /versions endpoint - * - * @param url that has an unauthenticated versions payload. For Looker, this is /versions - * @param content content of versions payload that may already be assigned - * @param defer true to defer fetching and parsing the spec. Defaults to true. - */ -export const loadSpecsFromVersions = async ( - url: string, - content: string | Record = '', - defer = true -): Promise => { - let fetchResult = '' - let specs: SpecList = {} - let baseUrl = '' - let webUrl = '' - let headless = false - try { - if (!content) { - content = await getUrl(url) - } - const versions = ( - typeof content === 'string' ? JSON.parse(content) : content - ) as IAPIXConfig - const origin = (window as any).location.origin - baseUrl = versions.api_server_url - webUrl = versions.web_server_url - if (versions.headless !== undefined) { - headless = versions.headless - } - const fetchSpec = async (spec: SpecItem) => { - if (spec.specURL) { - spec.specURL = fullify(spec.specURL, origin) - if (!defer) { - spec.api = await fallbackFetch(spec, funFetch) - } - } - return spec.api - } - specs = await getSpecsFromVersions(versions, fetchSpec) - } catch (e: any) { - fetchResult = e.message - } - - return { - baseUrl, - webUrl, - specs, - headless, - fetchResult: fetchResult, - } -} - /** * fetch and compile an API specification to an ApiModel if it's not already available * diff --git a/packages/api-explorer/src/utils/apixAdaptor.ts b/packages/api-explorer/src/utils/apixAdaptor.ts new file mode 100644 index 000000000..363bc1cbd --- /dev/null +++ b/packages/api-explorer/src/utils/apixAdaptor.ts @@ -0,0 +1,72 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ +import type { IEnvironmentAdaptor } from '@looker/extension-utils' +import { BrowserAdaptor, getEnvAdaptor } from '@looker/extension-utils' +import type { IAPIMethods } from '@looker/sdk-rtl' +import type { SpecItem, SpecList } from '@looker/sdk-codegen' +import { getSpecsFromVersions } from '@looker/sdk-codegen' +import { RunItConfigKey, RunItNoConfig } from '@looker/run-it' +import cloneDeep from 'lodash/cloneDeep' + +import { fullify, fallbackFetch, funFetch } from './adaptorUtils' + +export interface IApixAdaptor extends IEnvironmentAdaptor { + fetchSpecList(versionsUrl?: string): Promise + fetchSpec(spec: SpecItem): Promise +} + +export const getApixAdaptor = () => getEnvAdaptor() as IApixAdaptor + +export class ApixAdaptor extends BrowserAdaptor implements IApixAdaptor { + constructor(sdk: IAPIMethods, private readonly fallbackVersionsUrl: string) { + super(sdk) + } + + async fetchSpecList(versionsUrl?: string): Promise { + const data = await this.localStorageGetItem(RunItConfigKey) + const config = data ? JSON.parse(data) : RunItNoConfig + let url: string + + if (versionsUrl) { + url = versionsUrl + } else { + url = config.base_url + ? `${config.base_url}/versions` + : `${this.fallbackVersionsUrl}/versions.json` + } + + const versions = await this.sdk.authSession.transport.rawRequest('GET', url) + const specs = await getSpecsFromVersions(JSON.parse(versions.body)) + return specs + } + + async fetchSpec(spec: SpecItem): Promise { + const _spec = cloneDeep(spec) + _spec.specURL = fullify(spec.specURL!, origin) + _spec.api = await fallbackFetch(_spec, funFetch) + return _spec + } +} diff --git a/packages/api-explorer/src/utils/index.ts b/packages/api-explorer/src/utils/index.ts index e20ee732c..6be75641b 100644 --- a/packages/api-explorer/src/utils/index.ts +++ b/packages/api-explorer/src/utils/index.ts @@ -33,3 +33,5 @@ export { } from './path' export { getLoded } from './lodeUtils' export { useWindowSize } from './useWindowSize' +export * from './apixAdaptor' +export * from './adaptorUtils' diff --git a/packages/api-explorer/src/utils/path.ts b/packages/api-explorer/src/utils/path.ts index 45b3cbb32..7a3ec8f56 100644 --- a/packages/api-explorer/src/utils/path.ts +++ b/packages/api-explorer/src/utils/path.ts @@ -98,3 +98,8 @@ export const buildPath = ( } return path } + +export const specKeyFromPath = (path: string) => { + const match = path.match(/^\/(\w+.\w+).*$/) + return match ? match[1] : null +} diff --git a/packages/extension-api-explorer/package.json b/packages/extension-api-explorer/package.json index de959ebea..95c1970ed 100644 --- a/packages/extension-api-explorer/package.json +++ b/packages/extension-api-explorer/package.json @@ -16,10 +16,10 @@ }, "dependencies": { "@looker/api-explorer": "^0.9.26", - "@looker/extension-utils": "^0.1.3", "@looker/components": "^2.8.1", "@looker/extension-sdk": "^21.20.0", "@looker/extension-sdk-react": "^21.20.0", + "@looker/extension-utils": "^0.1.3", "@looker/icons": "^1.5.3", "@looker/run-it": "^0.9.26", "@looker/sdk": "^21.20.0", @@ -27,6 +27,7 @@ "@styled-icons/material": "^10.28.0", "@styled-icons/material-outlined": "^10.28.0", "@styled-icons/material-rounded": "^10.28.0", + "lodash": "^4.17.21", "path-browserify": "^1.0.1", "process": "^0.11.10", "react": "^16.13.1", @@ -36,6 +37,7 @@ "redux": "^4.0.5" }, "devDependencies": { + "@types/lodash": "^4.14.178", "@types/redux": "^3.6.0", "webpack-bundle-analyzer": "^4.2.0", "webpack-cli": "^4.6.0", diff --git a/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx b/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx index efa29bd41..1526956d1 100644 --- a/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx +++ b/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx @@ -25,40 +25,42 @@ */ import type { FC } from 'react' -import React, { useContext, useEffect, useState } from 'react' -import { RunItProvider, runItNoSet, sdkSpecFetch } from '@looker/run-it' +import React, { useContext } from 'react' +import { RunItProvider } from '@looker/run-it' import type { ExtensionContextData } from '@looker/extension-sdk-react' import { ExtensionContext } from '@looker/extension-sdk-react' -import type { SpecItem, SpecList } from '@looker/sdk-codegen' -import { getSpecsFromVersions } from '@looker/sdk-codegen' -import { ApiExplorer, store, Loader } from '@looker/api-explorer' +import type { IApixAdaptor } from '@looker/api-explorer' +import { ApiExplorer, store, sdkSpecFetch } from '@looker/api-explorer' import { getExtensionSDK } from '@looker/extension-sdk' import { Provider } from 'react-redux' import { ExtensionAdaptor } from '@looker/extension-utils' +import type { SpecItem } from '@looker/sdk-codegen' +import { getSpecsFromVersions } from '@looker/sdk-codegen' +import cloneDeep from 'lodash/cloneDeep' +import type { ILooker40SDK } from '@looker/sdk' -export const ExtensionApiExplorer: FC = () => { - const extensionContext = useContext(ExtensionContext) - const [specs, setSpecs] = useState() - - const sdk = extensionContext.core40SDK - - useEffect(() => { - /** Load Looker /versions information and retrieve all supported specs */ - async function loadSpecs() { - const versions = await sdk.ok(sdk.versions()) - const result = await getSpecsFromVersions(versions, (spec: SpecItem) => - sdkSpecFetch(spec, (version, name) => - sdk.ok(sdk.api_spec(version, name)) - ) - ) +class ApixExtensionAdaptor extends ExtensionAdaptor implements IApixAdaptor { + async fetchSpecList() { + const sdk = this.sdk as ILooker40SDK + const versions = await sdk.ok(sdk.versions()) + const result = await getSpecsFromVersions(versions) + return result + } - setSpecs(result) - } + async fetchSpec(spec: SpecItem): Promise { + const sdk = this.sdk as ILooker40SDK + const _spec = cloneDeep(spec) + _spec.api = await sdkSpecFetch(spec, (version, name) => + sdk.ok(sdk.api_spec(version, name)) + ) + return _spec + } +} - if (sdk && !specs) loadSpecs().catch((err) => console.error(err)) - }, [specs, sdk]) +export const ExtensionApiExplorer: FC = () => { + const extensionContext = useContext(ExtensionContext) - const extensionAdaptor = new ExtensionAdaptor( + const extensionAdaptor = new ApixExtensionAdaptor( getExtensionSDK(), extensionContext.core40SDK ) @@ -66,19 +68,11 @@ export const ExtensionApiExplorer: FC = () => { return ( - <> - {specs ? ( - - ) : ( - - )} - + ) diff --git a/packages/extension-utils/package.json b/packages/extension-utils/package.json index e59525a39..c3cbe0d3f 100644 --- a/packages/extension-utils/package.json +++ b/packages/extension-utils/package.json @@ -1,8 +1,8 @@ { "name": "@looker/extension-utils", - "version": "0.1.0", + "version": "0.1.3", "description": "Looker Extension Utilities", - "main": "dist/bundle.js", + "main": "lib/index.js", "module": "lib/esm/index.js", "sideEffects": false, "typings": "lib/index.d.ts", @@ -22,10 +22,6 @@ "private": false, "homepage": "https://github.com/looker-open-source/sdk-codegen/tree/main/packages/extension-utils", "scripts": { - "analyze": "export ANALYZE_MODE=static && yarn bundle", - "bundle": "tsc && webpack --config webpack.prod.config.js", - "deploy": "bin/deploy", - "develop": "webpack serve --hot --disable-host-check --port 8080 --https --config webpack.dev.config.js", "watch": "yarn lerna exec --scope @looker/extension-utils --stream 'BABEL_ENV=build babel src --root-mode upward --out-dir lib/esm --source-maps --extensions .ts,.tsx --no-comments --watch'" }, "dependencies": { @@ -36,6 +32,7 @@ "react": "^16.13.1" }, "devDependencies": { + "@types/react-router": "^5.1.18", "@types/redux": "^3.6.0", "webpack-bundle-analyzer": "^4.2.0", "webpack-cli": "^4.6.0", diff --git a/packages/run-it/src/scenes/OAuthScene/OAuthScene.tsx b/packages/extension-utils/src/OAuthScene.tsx similarity index 50% rename from packages/run-it/src/scenes/OAuthScene/OAuthScene.tsx rename to packages/extension-utils/src/OAuthScene.tsx index aacf6f8a0..12c4b6c1b 100644 --- a/packages/run-it/src/scenes/OAuthScene/OAuthScene.tsx +++ b/packages/extension-utils/src/OAuthScene.tsx @@ -25,58 +25,58 @@ */ import type { FC } from 'react' -import React, { useEffect, useState } from 'react' -import { useHistory } from 'react-router-dom' +import React, { useEffect } from 'react' import type { BrowserSession } from '@looker/sdk-rtl' -import { getEnvAdaptor } from '@looker/extension-utils' +import { + ComponentsProvider, + Flex, + FlexItem, + Heading, + ProgressCircular, +} from '@looker/components' +import { useHistory } from 'react-router' -import { Loading } from '../../components' +import type { IEnvironmentAdaptor } from './adaptorUtils' -export const OAuthScene: FC = () => { - const origin = (window as any).location.origin - const [loading, setLoading] = useState(true) - const [auth, setAuth] = useState() - const [oldUrl, setOldUrl] = useState() - const history = useHistory() - const sdk = getEnvAdaptor().sdk +interface OAuthSceneProps { + adaptor: IEnvironmentAdaptor +} - useEffect(() => { - if (sdk) { - const authSession = sdk.authSession as BrowserSession - setAuth(authSession) - /** capture the stored return URL before `OAuthSession.login()` clears it */ - const old = authSession.returnUrl || `/` - setOldUrl(old) - } else { - setAuth(undefined) - setOldUrl(undefined) - } - }, [sdk]) +/** + * OAuth scene for sdk session handling and redirection to OAuth flow initiation + * route + */ +export const OAuthScene: FC = ({ adaptor }) => { + const history = useHistory() + const authSession = adaptor.sdk.authSession as BrowserSession + const oldUrl = authSession.returnUrl || `/` useEffect(() => { const maybeLogin = async () => { - if (auth) { - try { - const res = await auth.login() - if (!auth.isAuthenticated()) { - console.error(`Authentication failed ${res}`) - } - } catch (error) { - console.error(error) - } - setLoading(false) - if (oldUrl) { - history.push(oldUrl) - } + const token = await adaptor.login() + if (token) { + history.push(oldUrl) } } maybeLogin() - }, [auth, history]) + }, []) + const themeOverrides = adaptor.themeOverrides() return ( - + + + + + + + + {`Returning to ${oldUrl} after OAuth login ...`} + + + + ) } diff --git a/packages/extension-utils/src/adaptorUtils.ts b/packages/extension-utils/src/adaptorUtils.ts index 38d88b18d..16fd4e4bf 100644 --- a/packages/extension-utils/src/adaptorUtils.ts +++ b/packages/extension-utils/src/adaptorUtils.ts @@ -33,7 +33,7 @@ export interface IAuthAdaptor { get sdk(): IAPIMethods /** Method for authenticating against the API server. Auth mechanism is dependent on the authSession implementation * used for the sdk. */ - login(): void + login(): Promise } /** @@ -108,7 +108,9 @@ let extensionAdaptor: IEnvironmentAdaptor | undefined * Register the environment adaptor. Used when initializing the application * @param adaptor to register */ -export const registerEnvAdaptor = (adaptor: IEnvironmentAdaptor) => { +export const registerEnvAdaptor = ( + adaptor: T +) => { extensionAdaptor = adaptor } @@ -133,7 +135,9 @@ export const getEnvAdaptor = () => { * Used by some unit tests * @param adaptor to use for testing */ -export const registerTestEnvAdaptor = (adaptor?: IEnvironmentAdaptor) => { +export const registerTestEnvAdaptor = ( + adaptor?: T +) => { const mockSdk = {} as unknown as IAPIMethods registerEnvAdaptor(adaptor || new BrowserAdaptor(mockSdk)) } diff --git a/packages/extension-utils/src/authUtils.ts b/packages/extension-utils/src/authUtils.ts index 08506b10c..8bf755892 100644 --- a/packages/extension-utils/src/authUtils.ts +++ b/packages/extension-utils/src/authUtils.ts @@ -68,6 +68,11 @@ export class OAuthConfigProvider extends ApiSettings { } } + isConfigured(): boolean { + // Required to be true otherwise SDK initialization fails + return true + } + getStoredConfig() { const storage = this.getStorage(this.configKey) let config = { base_url: '', looker_url: '' } diff --git a/packages/extension-utils/src/browserAdaptor.ts b/packages/extension-utils/src/browserAdaptor.ts index 546e80ca4..041339b20 100644 --- a/packages/extension-utils/src/browserAdaptor.ts +++ b/packages/extension-utils/src/browserAdaptor.ts @@ -25,18 +25,24 @@ */ import type { IAPIMethods } from '@looker/sdk-rtl' -import { getThemeOverrides, hostedInternally } from './adaptorUtils' import type { IAuthAdaptor, IEnvironmentAdaptor, ThemeOverrides, } from './adaptorUtils' +import { getThemeOverrides, hostedInternally } from './adaptorUtils' +import type { OAuthConfigProvider } from './authUtils' export class BrowserAuthAdaptor implements IAuthAdaptor { constructor(public readonly sdk: IAPIMethods) {} async login() { - await this.sdk.authSession.login() + let token + const settings = this.sdk.authSession.settings as OAuthConfigProvider + if (settings.authIsConfigured()) { + token = await this.sdk.authSession.login() + } + return !!token } } diff --git a/packages/extension-utils/src/extensionAdaptor.ts b/packages/extension-utils/src/extensionAdaptor.ts index c944d5d88..a89bd8ef7 100644 --- a/packages/extension-utils/src/extensionAdaptor.ts +++ b/packages/extension-utils/src/extensionAdaptor.ts @@ -38,6 +38,7 @@ export class ExtensionAuthAdaptor implements IAuthAdaptor { async login() { // Noop for extensions. Authentication is not required in an extension context + return true } } diff --git a/packages/extension-utils/src/index.ts b/packages/extension-utils/src/index.ts index fb9917b1f..c084b4ed5 100644 --- a/packages/extension-utils/src/index.ts +++ b/packages/extension-utils/src/index.ts @@ -28,3 +28,4 @@ export * from './adaptorUtils' export * from './browserAdaptor' export * from './extensionAdaptor' export * from './authUtils' +export * from './OAuthScene' diff --git a/packages/redux/src/createStore/index.ts b/packages/redux/src/createStore/index.ts index 8883471a0..0b3b307b8 100644 --- a/packages/redux/src/createStore/index.ts +++ b/packages/redux/src/createStore/index.ts @@ -67,7 +67,7 @@ export const createStore = ({ const sagasSet = new WeakSet() const sagaMiddleware = createSagaMiddleware() const store = configureStore({ - devTools: process.env.NODE_ENV !== 'production' || devTools, + devTools: devTools || process.env.NODE_ENV !== 'production', middleware: [sagaMiddleware, ...middleware], reducer: currentReducers, preloadedState, diff --git a/packages/run-it/src/RunIt.tsx b/packages/run-it/src/RunIt.tsx index 42d86debc..9723d2854 100644 --- a/packages/run-it/src/RunIt.tsx +++ b/packages/run-it/src/RunIt.tsx @@ -60,8 +60,7 @@ import { prepareInputs, createInputs, } from './utils' -import type { RunItSetter } from '.' -import { runItNoSet, RunItContext } from '.' +import { RunItContext } from '.' export type RunItHttpMethod = 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE' @@ -106,8 +105,6 @@ interface RunItProps { api: ApiModel /** Method to test */ method: IMethod - /** Set versions Url callback */ - setVersionsUrl: RunItSetter /** Sdk language to use for generating call syntax */ sdkLanguage?: string } @@ -120,7 +117,6 @@ export const RunIt: FC = ({ adaptor, api, method, - setVersionsUrl = runItNoSet, sdkLanguage = 'All', }) => { const httpMethod = method.httpMethod as RunItHttpMethod @@ -241,7 +237,6 @@ export const RunIt: FC = ({ isExtension={isExtension} validationMessage={validationMessage} setValidationMessage={setValidationMessage} - setVersionsUrl={setVersionsUrl} /> @@ -276,7 +271,6 @@ export const RunIt: FC = ({ diff --git a/packages/run-it/src/components/ConfigForm/ConfigForm.spec.tsx b/packages/run-it/src/components/ConfigForm/ConfigForm.spec.tsx index ba64898dd..320068726 100644 --- a/packages/run-it/src/components/ConfigForm/ConfigForm.spec.tsx +++ b/packages/run-it/src/components/ConfigForm/ConfigForm.spec.tsx @@ -30,8 +30,8 @@ import { renderWithTheme } from '@looker/components-test-utils' import userEvent from '@testing-library/user-event' import { BrowserAdaptor, registerTestEnvAdaptor } from '@looker/extension-utils' -import { initRunItSdk, runItNoSet } from '../..' -import { ConfigForm, loadSpecsFromVersions, RunItConfigKey } from '.' +import { initRunItSdk } from '../..' +import { ConfigForm, RunItConfigKey } from '.' describe('ConfigForm', () => { const adaptor = new BrowserAdaptor(initRunItSdk()) @@ -44,9 +44,7 @@ describe('ConfigForm', () => { }) test('it creates an empty config form without stored config', async () => { - renderWithTheme( - - ) + renderWithTheme() expect( screen.getByRole('heading', { name: 'RunIt Configuration' }) ).toBeInTheDocument() @@ -81,9 +79,7 @@ describe('ConfigForm', () => { }) test('it disables and enables verify for bad and good urls', async () => { - renderWithTheme( - - ) + renderWithTheme() const apiUrl = screen.getByRole('textbox', { name: apiLabel, }) as HTMLInputElement @@ -114,28 +110,14 @@ describe('ConfigForm', () => { test('it can have a custom title', () => { const title = 'New title' - renderWithTheme( - - ) + renderWithTheme() expect(screen.getByRole('heading', { name: title })).toBeInTheDocument() }) describe('storage', () => { test.skip('it saves and clears storage', async () => { // TODO need to rewrite this test - ;(loadSpecsFromVersions as jest.Mock).mockReturnValue( - Promise.resolve({ - base_url: 'http://locb', - web_server_url: 'http://local', - }) - ) - renderWithTheme( - - ) + renderWithTheme() const apiUrl = screen.getByRole('textbox', { name: apiLabel, }) as HTMLInputElement @@ -185,9 +167,7 @@ describe('ConfigForm', () => { }) ) - renderWithTheme( - - ) + renderWithTheme() expect( screen.getByRole('heading', { name: 'RunIt Configuration' }) ).toBeInTheDocument() diff --git a/packages/run-it/src/components/ConfigForm/ConfigForm.tsx b/packages/run-it/src/components/ConfigForm/ConfigForm.tsx index 8018b488d..cbeda95f7 100644 --- a/packages/run-it/src/components/ConfigForm/ConfigForm.tsx +++ b/packages/run-it/src/components/ConfigForm/ConfigForm.tsx @@ -43,41 +43,35 @@ import { } from '@looker/components' import { CodeCopy } from '@looker/code-editor' import { getEnvAdaptor } from '@looker/extension-utils' +import type { ILookerVersions } from '@looker/sdk-codegen' -import type { RunItSetter, RunItValues } from '../..' -import { - CollapserCard, - RunItFormKey, - RunItHeading, - DarkSpan, - readyToLogin, - RunItNoConfig, -} from '../..' -import type { ILoadedSpecs } from './configUtils' +import type { RunItValues } from '../..' +import { CollapserCard, RunItHeading, DarkSpan, readyToLogin } from '../..' import { + getVersions, RunItConfigKey, + RunItNoConfig, + RunItFormKey, validateUrl, - loadSpecsFromVersions, -} from './configUtils' +} from './utils' const POSITIVE: MessageBarIntent = 'positive' -interface IFieldValues extends ILoadedSpecs { +interface IFieldValues { + baseUrl: string + webUrl: string fetchIntent: MessageBarIntent + fetchResult: string } const defaultFieldValues: IFieldValues = { baseUrl: '', webUrl: '', - /** not currently used but declared for property compatibility for ILoadedSpecs */ - headless: false, - specs: {}, fetchResult: '', fetchIntent: POSITIVE, } interface ConfigFormProps { - setVersionsUrl: RunItSetter /** A collection type react state to store path, query and body parameters as entered by the user */ requestContent: RunItValues /** Title for the config form */ @@ -87,7 +81,6 @@ interface ConfigFormProps { } export const ConfigForm: FC = ({ - setVersionsUrl, title, requestContent, setHasConfig, @@ -105,7 +98,8 @@ export const ConfigForm: FC = ({ "enabled": true } ` - const sdk = getEnvAdaptor().sdk + const adaptor = getEnvAdaptor() + const sdk = adaptor.sdk // See https://codesandbox.io/s/youthful-surf-0g27j?file=/src/index.tsx for a prototype from Luke // TODO see about useReducer to clean this up a bit more title = title || 'RunIt Configuration' @@ -114,7 +108,7 @@ export const ConfigForm: FC = ({ // TODO: This is temporary until config settings are available in redux. // get configuration from storage, or default it const data = localStorage.getItem(RunItConfigKey) - const result = data ? JSON.parse(data) : { base_url: '', looker_url: '' } // TODO why is RunItNoConfig undefined here? + const result = data ? JSON.parse(data) : RunItNoConfig return result } @@ -170,28 +164,26 @@ export const ConfigForm: FC = ({ } const updateForm = async (_e: BaseSyntheticEvent, save: boolean) => { - // e.preventDefault() + updateMessage('inform', '') + const versionsUrl = `${fields.baseUrl}/versions` try { - updateMessage('inform', '') - const versionsUrl = `${fields.baseUrl}/versions` - const { webUrl, baseUrl } = await loadSpecsFromVersions(versionsUrl) - if (!baseUrl || !webUrl) { - fetchError('Invalid server configuration') - } else { - updateFields({ [BASE_URL]: baseUrl, [WEB_URL]: webUrl }) - updateMessage(POSITIVE, 'Configuration is valid') - if (save) { - const data = { base_url: baseUrl, looker_url: webUrl } - // TODO: replace when redux is introduced to run it - localStorage.setItem(RunItConfigKey, JSON.stringify(data)) - if (setHasConfig) setHasConfig(true) - setSaved(data) - setVersionsUrl(versionsUrl) - updateMessage(POSITIVE, `Saved ${webUrl} as OAuth server`) - } + const { web_server_url: webUrl, api_server_url: baseUrl } = + (await getVersions(versionsUrl)) as ILookerVersions + updateFields({ + [BASE_URL]: baseUrl, + [WEB_URL]: webUrl, + }) + updateMessage(POSITIVE, 'Configuration is valid') + if (save) { + const data = { base_url: baseUrl, looker_url: webUrl } + // TODO: replace when redux is introduced to run it + localStorage.setItem(RunItConfigKey, JSON.stringify(data)) + if (setHasConfig) setHasConfig(true) + setSaved(data) + updateMessage(POSITIVE, `Saved ${webUrl} as OAuth server`) } - } catch (err: any) { - fetchError(err.message) + } catch (e: any) { + fetchError(e.message) } } @@ -264,7 +256,7 @@ export const ConfigForm: FC = ({ localStorage.setItem(RunItFormKey, JSON.stringify(requestContent)) } // This will set storage variables and return to OAuthScene when successful - await sdk.authSession.login() + await adaptor.login() } return ( diff --git a/packages/run-it/src/components/ConfigForm/index.ts b/packages/run-it/src/components/ConfigForm/index.ts index fef10b385..89bcff556 100644 --- a/packages/run-it/src/components/ConfigForm/index.ts +++ b/packages/run-it/src/components/ConfigForm/index.ts @@ -25,4 +25,4 @@ */ export * from './ConfigForm' -export * from './configUtils' +export * from './utils' diff --git a/packages/run-it/src/components/ConfigForm/configUtils.spec.ts b/packages/run-it/src/components/ConfigForm/utils.spec.ts similarity index 98% rename from packages/run-it/src/components/ConfigForm/configUtils.spec.ts rename to packages/run-it/src/components/ConfigForm/utils.spec.ts index 9fa309872..782c3ea13 100644 --- a/packages/run-it/src/components/ConfigForm/configUtils.spec.ts +++ b/packages/run-it/src/components/ConfigForm/utils.spec.ts @@ -24,7 +24,7 @@ */ -import { validateUrl } from './configUtils' +import { validateUrl } from './utils' describe('configUtils', () => { describe('validateUrl', () => { diff --git a/packages/run-it/src/components/ConfigForm/utils.ts b/packages/run-it/src/components/ConfigForm/utils.ts new file mode 100644 index 000000000..0481231c1 --- /dev/null +++ b/packages/run-it/src/components/ConfigForm/utils.ts @@ -0,0 +1,78 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ +import type { ILookerVersions } from '@looker/sdk-codegen' +import { BrowserTransport, DefaultSettings } from '@looker/sdk-rtl' +import type { RunItValues } from '@looker/run-it' + +export const RunItConfigKey = 'RunItConfig' +export const RunItFormKey = 'RunItForm' +export const RunItNoConfig = { base_url: '', looker_url: '' } + +/** + * Use the browser transport to GET a url + * @param url to fetch + */ +export const getUrl = async (url: string): Promise => { + const settings = { + ...DefaultSettings(), + ...{ base_url: url, verify_ssl: false }, + } + const xp = new BrowserTransport(settings) + const response = await xp.rawRequest('GET', url) + return response.body +} + +/** + * Gets the versions payload given a versions url + * @param versionsUrl + */ +export const getVersions = async ( + versionsUrl: string +): Promise => { + let versions + try { + const content = await getUrl(versionsUrl) + versions = typeof content === 'string' ? JSON.parse(content) : content + } catch { + throw new Error('Invalid server configuration') + } + return versions +} + +/** + * Validates URL and standardizes it + * @param url to validate + * @returns the standardized url.origin if it's valid, or an empty string if it's not + */ +export const validateUrl = (url: string): string => { + try { + const result = new URL(url) + if (url.endsWith(':')) return url + return result.origin + } catch { + return '' + } +} diff --git a/packages/run-it/src/components/LoginForm/LoginForm.spec.tsx b/packages/run-it/src/components/LoginForm/LoginForm.spec.tsx index 051dc6ad7..2a611976c 100644 --- a/packages/run-it/src/components/LoginForm/LoginForm.spec.tsx +++ b/packages/run-it/src/components/LoginForm/LoginForm.spec.tsx @@ -28,22 +28,39 @@ import React from 'react' import { screen, waitFor } from '@testing-library/react' import { renderWithTheme } from '@looker/components-test-utils' import userEvent from '@testing-library/user-event' -import { BrowserAdaptor, registerTestEnvAdaptor } from '@looker/extension-utils' +import { + BrowserAdaptor, + registerTestEnvAdaptor, + OAuthConfigProvider, +} from '@looker/extension-utils' -import { readyToLogin } from '..' -import { initRunItSdk } from '../..' -import { LoginForm } from './LoginForm' +import { initRunItSdk, readyToLogin } from '../..' +import { LoginForm, notReadyToLogin } from './LoginForm' describe('LoginForm', () => { const sdk = initRunItSdk() registerTestEnvAdaptor(new BrowserAdaptor(sdk)) - test('it creates a login form', async () => { + test('it renders a login form with instructions if auth is not configured', async () => { + renderWithTheme() + const login = screen.getByRole('button', { + name: 'Login', + }) + await waitFor(() => { + userEvent.hover(login) + expect(screen.getByRole('tooltip')).toHaveTextContent(notReadyToLogin) + }) + }) + + test('it displays a ready to login message if auth is configured', async () => { + jest + .spyOn(OAuthConfigProvider.prototype, 'authIsConfigured') + .mockReturnValue(true) + renderWithTheme() const login = screen.getByRole('button', { name: 'Login', }) - expect(login).toBeInTheDocument() await waitFor(() => { userEvent.hover(login) expect(screen.getByRole('tooltip')).toHaveTextContent(readyToLogin) diff --git a/packages/run-it/src/components/LoginForm/LoginForm.tsx b/packages/run-it/src/components/LoginForm/LoginForm.tsx index d48de19ae..9ecb9de29 100644 --- a/packages/run-it/src/components/LoginForm/LoginForm.tsx +++ b/packages/run-it/src/components/LoginForm/LoginForm.tsx @@ -27,6 +27,7 @@ import type { BaseSyntheticEvent, FC } from 'react' import React from 'react' import { Button, Tooltip } from '@looker/components' +import type { OAuthConfigProvider } from '@looker/extension-utils' import { getEnvAdaptor } from '@looker/extension-utils' import { RunItFormKey } from '../ConfigForm' @@ -39,20 +40,30 @@ interface LoginFormProps { export const readyToLogin = 'OAuth is configured but your browser session is not authenticated. Click Login to enable RunIt.' +export const notReadyToLogin = + 'OAuth is not configured. Configure it to be able to Login.' + export const LoginForm: FC = ({ requestContent }) => { const adaptor = getEnvAdaptor() - const handleLogin = async (e: BaseSyntheticEvent) => { e.preventDefault() if (requestContent) { adaptor.localStorageSetItem(RunItFormKey, JSON.stringify(requestContent)) } // This will set storage variables and return to OAuthScene when successful - await adaptor.sdk.authSession.login() + await adaptor.login() } return ( - + ) diff --git a/packages/run-it/src/components/RequestForm/RequestForm.tsx b/packages/run-it/src/components/RequestForm/RequestForm.tsx index c507fe5c2..69862855f 100644 --- a/packages/run-it/src/components/RequestForm/RequestForm.tsx +++ b/packages/run-it/src/components/RequestForm/RequestForm.tsx @@ -36,7 +36,6 @@ import { } from '@looker/components' import type { RunItHttpMethod, RunItInput, RunItValues } from '../../RunIt' import { LoginForm } from '../LoginForm' -import type { RunItSetter } from '../..' import { createSimpleItem, createComplexItem, @@ -63,8 +62,6 @@ interface RequestFormProps { hasConfig: boolean /** Handle config button click */ handleConfig: (e: BaseSyntheticEvent) => void - /** Hook to refresh specifications */ - setVersionsUrl: RunItSetter /** A set state callback which if present allows for editing, setting or clearing OAuth configuration parameters */ setHasConfig?: Dispatch /** Validation message to display */ diff --git a/packages/run-it/src/index.ts b/packages/run-it/src/index.ts index a52ec5611..7e4ab93b0 100644 --- a/packages/run-it/src/index.ts +++ b/packages/run-it/src/index.ts @@ -28,4 +28,3 @@ export * from './RunIt' export * from './RunItProvider' export * from './utils' export * from './components' -export * from './scenes' diff --git a/packages/sdk-codegen-scripts/src/testUtils/testUtils.ts b/packages/sdk-codegen-scripts/src/testUtils/testUtils.ts index 55f85e928..844afccf4 100644 --- a/packages/sdk-codegen-scripts/src/testUtils/testUtils.ts +++ b/packages/sdk-codegen-scripts/src/testUtils/testUtils.ts @@ -89,7 +89,7 @@ export const TestConfig = (rootPath = ''): ITestConfig => { const apiTestModel = specFromFile(`${testPath}openApiRef.json`) return { apiTestModel, - baseUrl, + baseUrl: api_server_url, config, configContents, dataFile, diff --git a/packages/sdk-codegen/src/testUtils/testUtils.ts b/packages/sdk-codegen/src/testUtils/testUtils.ts index 82f62b164..8703aaa6e 100644 --- a/packages/sdk-codegen/src/testUtils/testUtils.ts +++ b/packages/sdk-codegen/src/testUtils/testUtils.ts @@ -91,7 +91,7 @@ export const TestConfig = (rootPath = ''): ITestConfig => { const apiTestModel = specFromFile(`${testPath}openApiRef.json`) return { apiTestModel, - baseUrl, + baseUrl: api_server_url, config, configContents, dataFile, diff --git a/packages/sdk-node/src/testUtils/testUtils.ts b/packages/sdk-node/src/testUtils/testUtils.ts index 40f6ad92d..0e3daa866 100644 --- a/packages/sdk-node/src/testUtils/testUtils.ts +++ b/packages/sdk-node/src/testUtils/testUtils.ts @@ -83,7 +83,7 @@ export const TestConfig = (rootPath = ''): ITestConfig => { if (!fs.existsSync(testIni)) testIni = '' return { - baseUrl, + baseUrl: api_server_url, dataFile, localIni, rootPath, diff --git a/packages/sdk-rtl/src/oauthSession.ts b/packages/sdk-rtl/src/oauthSession.ts index 7248a91b6..c54243808 100644 --- a/packages/sdk-rtl/src/oauthSession.ts +++ b/packages/sdk-rtl/src/oauthSession.ts @@ -50,6 +50,7 @@ interface IRefreshTokenGrantTypeParams { export class OAuthSession extends AuthSession { activeToken = new AuthToken() crypto: ICryptoHash + reentry = false private static readonly codeVerifierKey = 'looker_oauth_code_verifier' public static readonly returnUrlKey = 'looker_oauth_return_url' @@ -129,8 +130,10 @@ export class OAuthSession extends AuthSession { */ set returnUrl(value: string | null) { if (!value) { + console.log('Removing return url') sessionStorage.removeItem(OAuthSession.returnUrlKey) } else { + console.log(`Saving return url ${value}`) sessionStorage.setItem(OAuthSession.returnUrlKey, value) } } @@ -145,7 +148,9 @@ export class OAuthSession extends AuthSession { async login(_sudoId?: string | number): Promise { if (!this.isAuthenticated()) { - if (!this.returnUrl) { + if (this.reentry) { + // do nothing + } else if (!this.returnUrl) { // OAuth has not been initiated const authUrl = await this.createAuthCodeRequestUrl( 'cors_api', @@ -156,6 +161,7 @@ export class OAuthSession extends AuthSession { // Save the current URL so redirected successful OAuth login can restore it window.location.href = authUrl } else { + this.reentry = true // If return URL is stored, we must be coming back from an OAuth request // so release the stored return url at the start of the redemption this.returnUrl = null @@ -177,7 +183,8 @@ export class OAuthSession extends AuthSession { } await this.redeemAuthCode(code) } - return await this.getToken() + const token = await this.getToken() + return token } return this.activeToken } @@ -253,7 +260,9 @@ export class OAuthSession extends AuthSession { * @returns {Promise} */ async redeemAuthCode(authCode: string, codeVerifier?: string) { - return this.requestToken(this.redeemAuthCodeBody(authCode, codeVerifier)) + const body = this.redeemAuthCodeBody(authCode, codeVerifier) + const token = await this.requestToken(body) + return token } async getToken() { diff --git a/yarn.lock b/yarn.lock index 8895f6438..1437f6feb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3039,6 +3039,11 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== + "@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -3116,6 +3121,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45" integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw== +"@types/lodash@^4.14.178": + version "4.14.178" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" + integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + "@types/mdast@^3.0.0": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" @@ -3229,6 +3239,14 @@ "@types/history" "*" "@types/react" "*" +"@types/react-router@^5.1.18": + version "5.1.18" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.18.tgz#c8851884b60bc23733500d86c1266e1cfbbd9ef3" + integrity sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-test-renderer@>=16.9.0", "@types/react-test-renderer@^17.0.0": version "17.0.1" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" @@ -4062,6 +4080,11 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -5475,10 +5498,10 @@ css-to-react-native@^3.0.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" -"css-what@>= 5.0.1", css-what@^4.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" - integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== +css-what@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" + integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== css.escape@^1.5.1: version "1.5.1" @@ -7469,12 +7492,20 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -"glob-parent@>= 5.1.2", glob-parent@^3.1.0, glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: - is-glob "^4.0.3" + is-glob "^4.0.1" glob-to-regexp@^0.3.0: version "0.3.0" @@ -8481,7 +8512,7 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" -is-extglob@^2.1.1: +is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= @@ -8513,7 +8544,14 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -11609,6 +11647,11 @@ path-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -14378,10 +14421,20 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -"trim-newlines@>= 3.0.1", trim-newlines@^1.0.0, trim-newlines@^2.0.0, trim-newlines@^3.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.0.2.tgz#d6aaaf6a0df1b4b536d183879a6b939489808c7c" - integrity sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew== +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +trim-newlines@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" + integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= + +trim-newlines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== trim-off-newlines@^1.0.0: version "1.0.1" @@ -14393,10 +14446,10 @@ trim-trailing-lines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== -trim@0.0.1, "trim@>= 0.0.3": - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim/-/trim-1.0.1.tgz#68e78f6178ccab9687a610752f4f5e5a7022ee8c" - integrity sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w== +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= trough@^1.0.0: version "1.0.5" @@ -15380,10 +15433,22 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" -ws@7.4.6, "ws@>= 7.4.6", ws@^6.2.1, ws@^7.0.0, ws@^7.2.3, ws@^7.3.1: - version "8.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" - integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + +ws@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + dependencies: + async-limiter "~1.0.0" + +ws@^7.0.0, ws@^7.2.3, ws@^7.3.1: + version "7.5.7" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" + integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== xml-name-validator@^3.0.0: version "3.0.0" From c646c8d367a7f2f67ef0fd3a6b3b47c1017fcf42 Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Fri, 11 Feb 2022 16:05:49 +0000 Subject: [PATCH 03/13] fix: type and diff scene related issues --- packages/api-explorer/src/ApiExplorer.tsx | 6 +- .../ExploreType/ExploreType.spec.tsx | 23 --- .../components/ExploreType/ExploreType.tsx | 11 +- .../src/components/Header/Header.spec.tsx | 39 ++--- .../ApiSpecSelector.spec.tsx | 15 +- .../SelectorContainer.spec.tsx | 33 +---- .../src/components/SideNav/SideNav.spec.tsx | 57 ++------ .../src/components/SideNav/SideNav.tsx | 4 +- packages/api-explorer/src/reducers/index.ts | 26 ---- .../src/reducers/spec/actions.spec.ts | 36 ----- .../api-explorer/src/reducers/spec/actions.ts | 62 -------- .../api-explorer/src/reducers/spec/index.ts | 28 ---- .../src/reducers/spec/reducer.spec.ts | 44 ------ .../api-explorer/src/reducers/spec/reducer.ts | 69 --------- .../src/reducers/spec/utils.spec.ts | 138 ------------------ .../api-explorer/src/reducers/spec/utils.ts | 123 ---------------- .../src/scenes/DiffScene/DiffScene.tsx | 117 +++++---------- .../api-explorer/src/state/specs/slice.ts | 2 - packages/api-explorer/src/test-data/index.ts | 9 +- packages/api-explorer/src/test-data/specs.ts | 12 +- .../api-explorer/src/test-utils/redux.tsx | 5 +- packages/api-explorer/src/utils/index.ts | 8 +- packages/api-explorer/src/utils/path.ts | 18 ++- packages/run-it/src/RunIt.spec.tsx | 9 +- .../RequestForm/RequestForm.spec.tsx | 9 -- .../src/testUtils/testUtils.ts | 2 +- .../sdk-codegen/src/testUtils/testUtils.ts | 2 +- packages/sdk-node/src/testUtils/testUtils.ts | 2 +- packages/sdk-rtl/src/oauthSession.ts | 2 - yarn.lock | 93 +++--------- 30 files changed, 134 insertions(+), 870 deletions(-) delete mode 100644 packages/api-explorer/src/reducers/index.ts delete mode 100644 packages/api-explorer/src/reducers/spec/actions.spec.ts delete mode 100644 packages/api-explorer/src/reducers/spec/actions.ts delete mode 100644 packages/api-explorer/src/reducers/spec/index.ts delete mode 100644 packages/api-explorer/src/reducers/spec/reducer.spec.ts delete mode 100644 packages/api-explorer/src/reducers/spec/reducer.ts delete mode 100644 packages/api-explorer/src/reducers/spec/utils.spec.ts delete mode 100644 packages/api-explorer/src/reducers/spec/utils.ts diff --git a/packages/api-explorer/src/ApiExplorer.tsx b/packages/api-explorer/src/ApiExplorer.tsx index ee9dba72e..8b3d10aa3 100644 --- a/packages/api-explorer/src/ApiExplorer.tsx +++ b/packages/api-explorer/src/ApiExplorer.tsx @@ -65,10 +65,10 @@ import { useLodesStoreState, useSpecActions, useSpecStoreState, - selectCurrentSpec, selectSpecs, + selectCurrentSpec, } from './state' -import { specKeyFromPath } from './utils/path' +import { getSpecKey } from './utils' export interface ApiExplorerProps { adaptor: IApixAdaptor @@ -111,7 +111,7 @@ export const ApiExplorer: FC = ({ initSettingsAction() initLodesAction({ examplesLodeUrl, declarationsLodeUrl }) - const specKey = specKeyFromPath(location.pathname) + const specKey = getSpecKey(location) initSpecsAction({ specKey }) return () => unregisterEnvAdaptor() }, []) diff --git a/packages/api-explorer/src/components/ExploreType/ExploreType.spec.tsx b/packages/api-explorer/src/components/ExploreType/ExploreType.spec.tsx index 8c9ed1732..c9e2d8779 100644 --- a/packages/api-explorer/src/components/ExploreType/ExploreType.spec.tsx +++ b/packages/api-explorer/src/components/ExploreType/ExploreType.spec.tsx @@ -100,28 +100,5 @@ describe('ExploreType', () => { `http://localhost/${specKey}/types/ColorCollection/${targetType.jsonName}` ) }) - test('recognizes anything', () => { - const specKey = 'anything' - const path = `/${specKey}/methods/foo` - renderWithRouter(, [path]) - - const actual = screen.getByText(targetType.jsonName) - expect(actual).toBeInTheDocument() - expect(actual).toHaveProperty( - 'href', - `http://localhost/${specKey}/types/ColorCollection/${targetType.jsonName}` - ) - }) - test('ignores oauth path', () => { - const specKey = 'oauth' - const path = `/${specKey}/methods/foo` - renderWithRouter(, [path]) - const actual = screen.getByText(targetType.jsonName) - expect(actual).toBeInTheDocument() - expect(actual).toHaveProperty( - 'href', - `http://types/ColorCollection/${targetType.jsonName}` - ) - }) }) }) diff --git a/packages/api-explorer/src/components/ExploreType/ExploreType.tsx b/packages/api-explorer/src/components/ExploreType/ExploreType.tsx index e5a468165..90c87997a 100644 --- a/packages/api-explorer/src/components/ExploreType/ExploreType.tsx +++ b/packages/api-explorer/src/components/ExploreType/ExploreType.tsx @@ -29,9 +29,9 @@ import React from 'react' import { Code, Tree, TreeItem } from '@looker/components' import type { IType, ApiModel } from '@looker/sdk-codegen' import { TypeOfType, typeOfType } from '@looker/sdk-codegen' -import { useLocation } from 'react-router' +import { useRouteMatch } from 'react-router-dom' + import { Link } from '../Link' -import { getSpecKey } from '../../reducers' import { buildPath } from '../../utils' import { ExploreProperty, @@ -48,7 +48,8 @@ interface ExploreTypeLinkProps { } export const ExploreTypeLink: FC = ({ type, api }) => { - const location = useLocation() + const match = useRouteMatch<{ specKey: string }>('/:specKey') + const specKey = match?.params.specKey const picked = pickType(type) const name = picked.name const prefix = typeLinkPrefix(type) @@ -56,12 +57,10 @@ export const ExploreTypeLink: FC = ({ type, api }) => { const typed = typeOfType(picked) if (typed === TypeOfType.Intrinsic) return {type.jsonName} - - const specKey = getSpecKey(location) return ( <> {prefix} - + {name} {suffix} diff --git a/packages/api-explorer/src/components/Header/Header.spec.tsx b/packages/api-explorer/src/components/Header/Header.spec.tsx index a5452b86d..48268c435 100644 --- a/packages/api-explorer/src/components/Header/Header.spec.tsx +++ b/packages/api-explorer/src/components/Header/Header.spec.tsx @@ -28,46 +28,35 @@ import { act, screen, waitFor } from '@testing-library/react' import { codeGenerators } from '@looker/sdk-codegen' import userEvent from '@testing-library/user-event' -import { specs, specState } from '../../test-data' +import { getLoadedSpecs, specs } from '../../test-data' import { renderWithRouterAndReduxProvider } from '../../test-utils' import { defaultSettingsState } from '../../state' import { Header } from './Header' describe('Header', () => { - const specDispatch = jest.fn() const hasNavigation = true const toggleNavigation = () => !hasNavigation - + const spec = getLoadedSpecs()['4.0'] beforeAll(() => { window.HTMLElement.prototype.scrollIntoView = jest.fn() }) test('it renders a title', () => { renderWithRouterAndReduxProvider( -
+
) expect(screen.getByText('API Explorer').closest('a')).toHaveAttribute( 'href', - `/${specState.spec.key}` + `/${spec.key}` ) }) test('it renders a spec selector with the correct default value and options', async () => { renderWithRouterAndReduxProvider( -
+
) const selector = screen.getByLabelText('spec selector') - expect(selector).toHaveValue(`${specState.spec.key}`) + expect(selector).toHaveValue(`${spec.key}`) await act(async () => { await userEvent.click(selector) await waitFor(() => { @@ -80,12 +69,7 @@ describe('Header', () => { test('it renders an sdk language selector with the correct value and options', async () => { renderWithRouterAndReduxProvider( -
+
) const selector = screen.getByLabelText('sdk language selector') expect(selector).toHaveValue(defaultSettingsState.sdkLanguage) @@ -101,12 +85,7 @@ describe('Header', () => { test('it renders an icon button for the differ', () => { renderWithRouterAndReduxProvider( -
+
) expect( screen @@ -114,6 +93,6 @@ describe('Header', () => { name: 'Compare Specifications', }) .closest('a') - ).toHaveAttribute('href', `/diff/${specState.spec.key}/`) + ).toHaveAttribute('href', `/diff/${spec.key}/`) }) }) diff --git a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx index d879c2d80..b3086e35b 100644 --- a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx @@ -28,7 +28,7 @@ import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as reactRedux from 'react-redux' -import { specs, getLoadedSpecState } from '../../test-data' +import { getLoadedSpecs, specs } from '../../test-data' import { renderWithReduxProvider, renderWithRouterAndReduxProvider, @@ -36,8 +36,6 @@ import { import { useSpecActions } from '../../state' import { ApiSpecSelector } from './ApiSpecSelector' -const specState = getLoadedSpecState() - jest.mock('react-router-dom', () => { const ReactRouterDOM = jest.requireActual('react-router-dom') return { @@ -57,15 +55,16 @@ jest.mock('../../state/specs', () => ({ describe('ApiSpecSelector', () => { Element.prototype.scrollIntoView = jest.fn() + const spec = getLoadedSpecs()['4.0'] - test('the current spec is selected by default', () => { - renderWithReduxProvider() + test('the base spec is selected by default', () => { + renderWithReduxProvider() const selector = screen.getByRole('textbox') - expect(selector).toHaveValue(`${specState.spec.key}`) + expect(selector).toHaveValue(`${spec.key}`) }) test('it lists all available specs', async () => { - renderWithReduxProvider() + renderWithReduxProvider() userEvent.click(screen.getByRole('textbox')) await waitFor(() => { expect(screen.getAllByRole('option')).toHaveLength( @@ -79,7 +78,7 @@ describe('ApiSpecSelector', () => { jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch) const { setCurrentSpecAction } = useSpecActions() - renderWithRouterAndReduxProvider() + renderWithRouterAndReduxProvider() userEvent.click(screen.getByRole('textbox')) await waitFor(() => { expect(screen.getAllByRole('option')).toHaveLength( diff --git a/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.spec.tsx b/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.spec.tsx index 6f4a723f2..af81c1447 100644 --- a/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.spec.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.spec.tsx @@ -28,28 +28,21 @@ import { act, screen, waitFor } from '@testing-library/react' import { codeGenerators } from '@looker/sdk-codegen' import userEvent from '@testing-library/user-event' -import { specs, specState } from '../../test-data' +import { getLoadedSpecs, specs } from '../../test-data' import { renderWithRouterAndReduxProvider } from '../../test-utils' import { defaultSettingsState } from '../../state' import { SelectorContainer } from './SelectorContainer' describe('SelectorContainer', () => { - const specDispatch = jest.fn() - + const spec = getLoadedSpecs()['4.0'] beforeAll(() => { window.HTMLElement.prototype.scrollIntoView = jest.fn() }) test('it renders a spec selector with the correct default value and options', async () => { - renderWithRouterAndReduxProvider( - - ) + renderWithRouterAndReduxProvider() const selector = screen.getByLabelText('spec selector') - expect(selector).toHaveValue(`${specState.spec.key}`) + expect(selector).toHaveValue(`${spec.key}`) await act(async () => { await userEvent.click(selector) await waitFor(() => { @@ -61,13 +54,7 @@ describe('SelectorContainer', () => { }) test('it renders an sdk language selector with the correct value and options', async () => { - renderWithRouterAndReduxProvider( - - ) + renderWithRouterAndReduxProvider() const selector = screen.getByLabelText('sdk language selector') expect(selector).toHaveValue(defaultSettingsState.sdkLanguage) await act(async () => { @@ -81,19 +68,13 @@ describe('SelectorContainer', () => { }) test('it renders an icon button for the differ', () => { - renderWithRouterAndReduxProvider( - - ) + renderWithRouterAndReduxProvider() expect( screen .getByRole('button', { name: 'Compare Specifications', }) .closest('a') - ).toHaveAttribute('href', `/diff/${specState.spec.key}/`) + ).toHaveAttribute('href', `/diff/${spec.key}/`) }) }) diff --git a/packages/api-explorer/src/components/SideNav/SideNav.spec.tsx b/packages/api-explorer/src/components/SideNav/SideNav.spec.tsx index de7feff88..7c59a9cbf 100644 --- a/packages/api-explorer/src/components/SideNav/SideNav.spec.tsx +++ b/packages/api-explorer/src/components/SideNav/SideNav.spec.tsx @@ -28,15 +28,15 @@ import { criteriaToSet } from '@looker/sdk-codegen' import userEvent from '@testing-library/user-event' import { screen, waitFor } from '@testing-library/react' -import { getLoadedSpecs, specState } from '../../test-data' +import { getLoadedSpecs } from '../../test-data' import { renderWithRouterAndReduxProvider } from '../../test-utils' import { defaultSettingsState } from '../../state' import { SideNav } from './SideNav' import { countMethods, countTypes } from './searchUtils' +const spec = getLoadedSpecs()['4.0'] + describe('SideNav', () => { - const specs = getLoadedSpecs() - const specDispatch = jest.fn() const allTagsPattern = /^(Auth|ApiAuth)$/ const allTypesPattern = /^(WriteDashboard|WriteQuery)$/ @@ -55,13 +55,7 @@ describe('SideNav', () => { }) test('it renders search, methods tab and types tab', () => { - renderWithRouterAndReduxProvider( - - ) + renderWithRouterAndReduxProvider() const search = screen.getByLabelText('Search') expect(search).toHaveProperty('placeholder', 'Search') const tabs = screen.getAllByRole('tab', { @@ -69,23 +63,14 @@ describe('SideNav', () => { }) expect(tabs).toHaveLength(2) expect(tabs[0]).toHaveTextContent( - `Methods (${countMethods(specState.spec.api!.tags)})` + `Methods (${countMethods(spec.api!.tags)})` ) - expect(tabs[1]).toHaveTextContent( - `Types (${countTypes(specState.spec.api!.types)})` - ) + expect(tabs[1]).toHaveTextContent(`Types (${countTypes(spec.api!.types)})`) }) test('Methods tab is the default active tab', () => { - renderWithRouterAndReduxProvider( - , - ['/3.1/methods'] - ) + renderWithRouterAndReduxProvider(, ['/3.1/methods']) expect(screen.getAllByText(allTagsPattern)).toHaveLength(2) expect( screen.queryAllByRole('link', { name: allTypesPattern }) @@ -97,47 +82,29 @@ describe('SideNav', () => { }) test('url determines active tab', () => { - renderWithRouterAndReduxProvider( - , - ['/3.1/types'] - ) + renderWithRouterAndReduxProvider(, ['/3.1/types']) // eslint-disable-next-line jest-dom/prefer-in-document expect(screen.queryAllByText(allTagsPattern)).toHaveLength(2) }) }) describe('Search', () => { - const specs = getLoadedSpecs() - const specDispatch = jest.fn() - test('it filters methods and types on input', async () => { - renderWithRouterAndReduxProvider( - - ) + renderWithRouterAndReduxProvider() const searchPattern = 'embedsso' const input = screen.getByLabelText('Search') - jest.spyOn(specState.spec.api!, 'search') + jest.spyOn(spec.api!, 'search') /** Pasting to avoid triggering search multiple times */ await userEvent.paste(input, searchPattern) await waitFor(() => { - expect(specState.spec.api!.search).toHaveBeenCalledWith( + expect(spec.api!.search).toHaveBeenCalledWith( searchPattern, criteriaToSet(defaultSettingsState.searchCriteria) ) const methods = screen.getByRole('tab', { name: 'Methods (1)' }) userEvent.click(methods) expect( - screen.getByText( - specState.spec.api!.tags.Auth.create_sso_embed_url.summary - ) + screen.getByText(spec.api!.tags.Auth.create_sso_embed_url.summary) ).toBeInTheDocument() const types = screen.getByRole('tab', { name: 'Types (1)' }) userEvent.click(types) diff --git a/packages/api-explorer/src/components/SideNav/SideNav.tsx b/packages/api-explorer/src/components/SideNav/SideNav.tsx index aa27abd7e..6df6b5aed 100644 --- a/packages/api-explorer/src/components/SideNav/SideNav.tsx +++ b/packages/api-explorer/src/components/SideNav/SideNav.tsx @@ -24,7 +24,7 @@ */ -import type { FC, Dispatch } from 'react' +import type { FC } from 'react' import React, { useEffect, useState } from 'react' import { useHistory, useLocation } from 'react-router-dom' import { @@ -37,7 +37,6 @@ import { } from '@looker/components' import type { SpecItem, - SpecList, ISearchResult, ApiModel, TagList, @@ -46,7 +45,6 @@ import type { import { criteriaToSet, tagTypes } from '@looker/sdk-codegen' import { useSelector } from 'react-redux' -import type { SpecAction } from '../../reducers' import { useWindowSize } from '../../utils' import { HEADER_REM } from '../Header' import { selectSearchCriteria, useSettingActions } from '../../state' diff --git a/packages/api-explorer/src/reducers/index.ts b/packages/api-explorer/src/reducers/index.ts deleted file mode 100644 index 781057506..000000000 --- a/packages/api-explorer/src/reducers/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - - MIT License - - Copyright (c) 2021 Looker Data Sciences, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ -export * from './spec' diff --git a/packages/api-explorer/src/reducers/spec/actions.spec.ts b/packages/api-explorer/src/reducers/spec/actions.spec.ts deleted file mode 100644 index af39cb430..000000000 --- a/packages/api-explorer/src/reducers/spec/actions.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - - MIT License - - Copyright (c) 2021 Looker Data Sciences, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ -import { selectSpec } from './actions' - -describe('Spec reducer actions', () => { - test('returns a SELECT_SPEC action object with provided values', () => { - const action = selectSpec('3.1') - expect(action).toEqual({ - type: 'SELECT_SPEC', - payload: '3.1', - }) - }) -}) diff --git a/packages/api-explorer/src/reducers/spec/actions.ts b/packages/api-explorer/src/reducers/spec/actions.ts deleted file mode 100644 index c6db318a3..000000000 --- a/packages/api-explorer/src/reducers/spec/actions.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - - MIT License - - Copyright (c) 2021 Looker Data Sciences, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ - -import type { ApiModel } from '@looker/sdk-codegen' - -export enum Actions { - SELECT_SPEC = 'SELECT_SPEC', - UPDATE_SPEC_API = 'UPDATE_SPEC_API', -} - -export interface ApiUpdatePayload { - specKey: string - api: ApiModel -} - -export interface UpdateSpecApiAction { - type: Actions.UPDATE_SPEC_API - payload: ApiUpdatePayload -} - -export interface SelectSpecAction { - type: Actions.SELECT_SPEC - payload: string -} - -export type SpecAction = SelectSpecAction | UpdateSpecApiAction - -export const selectSpec = (specKey: string): SelectSpecAction => ({ - type: Actions.SELECT_SPEC, - payload: specKey, -}) - -export const updateSpecApi = ( - specKey: string, - api: ApiModel -): UpdateSpecApiAction => ({ - type: Actions.UPDATE_SPEC_API, - payload: { specKey, api }, -}) diff --git a/packages/api-explorer/src/reducers/spec/index.ts b/packages/api-explorer/src/reducers/spec/index.ts deleted file mode 100644 index 1a9461cac..000000000 --- a/packages/api-explorer/src/reducers/spec/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - - MIT License - - Copyright (c) 2021 Looker Data Sciences, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ -export * from './utils' -export * from './actions' -export * from './reducer' diff --git a/packages/api-explorer/src/reducers/spec/reducer.spec.ts b/packages/api-explorer/src/reducers/spec/reducer.spec.ts deleted file mode 100644 index 903c2f436..000000000 --- a/packages/api-explorer/src/reducers/spec/reducer.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - - MIT License - - Copyright (c) 2021 Looker Data Sciences, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ -import { ApiModel } from '@looker/sdk-codegen' - -import { specs } from '../../test-data' -import type { SelectSpecAction } from '.' -import { specReducer, initDefaultSpecState, Actions } from '.' - -describe('Spec Reducer', () => { - test('it selects a spec', () => { - const action: SelectSpecAction = { - type: Actions.SELECT_SPEC, - payload: '4.0', - } - - const state = specReducer(initDefaultSpecState(specs, location), action) - expect(state.spec.api).toBeInstanceOf(ApiModel) - expect(state.spec.key).toEqual('4.0') - expect(state.spec.status).toEqual(specs['4.0'].status) - }) -}) diff --git a/packages/api-explorer/src/reducers/spec/reducer.ts b/packages/api-explorer/src/reducers/spec/reducer.ts deleted file mode 100644 index c08f082e5..000000000 --- a/packages/api-explorer/src/reducers/spec/reducer.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - - MIT License - - Copyright (c) 2021 Looker Data Sciences, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ - -import type { SpecItem, SpecList } from '@looker/sdk-codegen' -import { ApiModel } from '@looker/sdk-codegen' -import type { ApiUpdatePayload } from './actions' -import type { SpecAction } from '.' -import { Actions } from '.' - -export interface SpecState { - specList: SpecList - spec: SpecItem -} - -export const specReducer = ( - state: SpecState, - action: SpecAction -): SpecState => { - const { type, payload } = action - switch (type) { - case Actions.UPDATE_SPEC_API: { - const specList = { ...state.specList } - const { specKey, api } = payload as ApiUpdatePayload - let spec = specList[specKey] - if (spec) { - spec = { ...spec, api } - specList[specKey] = spec - } - return { specList, spec } - } - case 'SELECT_SPEC': { - const newState = { ...state } - const spec = newState.specList[payload as string] - // Does extension API Explorer needs this? - if (spec) { - if (!spec.api && spec.specContent) { - spec.api = ApiModel.fromJson(spec.specContent) - } - return { ...state, spec } - } - return state - } - default: - return state - } -} diff --git a/packages/api-explorer/src/reducers/spec/utils.spec.ts b/packages/api-explorer/src/reducers/spec/utils.spec.ts deleted file mode 100644 index e83341865..000000000 --- a/packages/api-explorer/src/reducers/spec/utils.spec.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - - MIT License - - Copyright (c) 2021 Looker Data Sciences, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ -import type { SpecList } from '@looker/sdk-codegen' -import omit from 'lodash/omit' - -import { specs } from '../../test-data' -import { getDefaultSpecKey, initDefaultSpecState, getSpecKey } from './utils' - -describe('Spec reducer utils', () => { - const specList: SpecList = { - defaultKey: { - key: 'defaultKey', - version: '4.0', - status: 'experimental', - isDefault: true, - }, - deprecatedKey: { - key: 'deprecatedKey', - version: '3.0', - status: 'deprecated', - isDefault: false, - }, - currentKey: { - key: 'currentKey', - version: '3.1', - status: 'current', - isDefault: false, - }, - stableKey: { - key: 'stableKey', - version: '3.1', - status: 'stable', - isDefault: false, - }, - } - - describe('getSpecKey', () => { - const saveLocation = window.location - - afterAll(() => { - window.location = saveLocation - }) - - test('recognizes spec version in url', () => { - window.history.pushState({}, '', '/4.0/') - const specKey = getSpecKey(window.location) - expect(specKey).toBeDefined() - expect(specKey).toEqual('4.0') - }) - - test('ignores oauth as a spec key', () => { - window.history.pushState({}, '', '/oauth/') - const specKey = getSpecKey(window.location) - expect(specKey).toEqual('') - }) - - test('gets default spec key when specs are provided and url has no path', () => { - window.history.pushState({}, '', '') - const specKey = getSpecKey(window.location, specList) - expect(specKey).toBeDefined() - expect(specKey).toEqual('defaultKey') - }) - - test('gets default spec key when specs are provided and url has oauth path', () => { - window.history.pushState({}, '', '/oauth/') - const specKey = getSpecKey(window.location, specList) - expect(specKey).toBeDefined() - expect(specKey).toEqual('defaultKey') - }) - }) - - describe('getDefaultSpecKey', () => { - test('it throws if no specs are provided', () => { - expect(() => { - getDefaultSpecKey({} as SpecList) - }).toThrow('No specs found.') - }) - - test('it returns specKey for default spec if found', () => { - expect(getDefaultSpecKey(specList)).toEqual('defaultKey') - }) - - test('it returns specKey for spec marked as current if none are marked as default', () => { - const specItems = omit(specList, 'defaultKey') - expect(getDefaultSpecKey(specItems)).toEqual('currentKey') - }) - - test('it returns first spec if no spec is marked as default or current', () => { - const specItems = omit(specList, ['defaultKey', 'currentKey']) - expect(getDefaultSpecKey(specItems)).toEqual('deprecatedKey') - }) - }) - - describe('initDefaultSpecState', () => { - const saveLocation = window.location - - afterAll(() => { - window.location = saveLocation - }) - - test('it fetches spec using key defined in url', () => { - window.history.pushState({}, '', '/4.0/') - const specState = initDefaultSpecState(specs, window.location) - expect(specState).toBeDefined() - expect(specState.spec.key).toEqual('4.0') - }) - - test('it gets default spec if url does not specify a key', () => { - window.history.pushState({}, '', '/') - const specState = initDefaultSpecState(specs, window.location) - expect(specState).toBeDefined() - expect(specState.spec.key).toEqual('4.0') - }) - }) -}) diff --git a/packages/api-explorer/src/reducers/spec/utils.ts b/packages/api-explorer/src/reducers/spec/utils.ts deleted file mode 100644 index 8c67a665e..000000000 --- a/packages/api-explorer/src/reducers/spec/utils.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - - MIT License - - Copyright (c) 2021 Looker Data Sciences, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ - -import type { SpecList } from '@looker/sdk-codegen' -import type { Location as HLocation } from 'history' -import { OAuthSession } from '@looker/sdk-rtl' -import { diffPath, oAuthPath } from '../../utils' -import type { SpecState } from './reducer' - -export type AbstractLocation = HLocation | Location - -/** - * Given a collection of specs, it returns the spec marked as default, the one - * marked as current or the first one, in that order. - * @param specs A collection of specs - * @returns A spec - */ -export const getDefaultSpecKey = (specs: SpecList): string => { - const items = Object.entries(specs) - - if (items.length === 0) { - throw Error('No specs found.') - } - - let specKey = '' - items.forEach(([key, item]) => { - if (item.isDefault) { - specKey = key - } - }) - - if (!specKey) { - items.forEach(([key, item]) => { - if (item.status === 'current') { - specKey = key - } - }) - } - - if (!specKey) { - specKey = Object.keys(specs)[0] - } - - if (!specKey) { - throw Error('No specs found.') - } - - return specKey -} - -/** - * Determine the API specification key from URL pattern or default spec - * @param location service to examine - * @param specs to use to find the default spec key - */ -export const getSpecKey = (location: AbstractLocation, specs?: SpecList) => { - let pathName = location.pathname - if (pathName === `/${oAuthPath}`) { - const returnUrl = sessionStorage.getItem(OAuthSession.returnUrlKey) - if (returnUrl) { - pathName = returnUrl - } - } - const pathNodes = pathName.split('/') - let specKey = '' - if ( - pathNodes.length > 1 && - pathNodes[1] && - pathNodes[1] !== oAuthPath && - pathNodes[1] !== diffPath - ) { - specKey = pathNodes[1] - } else if (specs) { - specKey = getDefaultSpecKey(specs) - } - return specKey -} - -/** - * Creates a default state object with the spec matching the specKey defined - * in the url or the default criteria in getDefaultSpecKey - * @param specList A collection of specs - * @param location Standalone or extension location - * @returns An object to be used as default state - */ -export const initDefaultSpecState = ( - specList: SpecList, - location: AbstractLocation -): SpecState => { - const specKey = getSpecKey(location, specList) - // Handle bad spec in the URL. Fall back to 4.0 - let spec = specList[specKey] - if (!spec) { - spec = specList['4.0'] - } - return { - specList, - spec, - } -} diff --git a/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx b/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx index 59a09b9fa..d5ea1b0ea 100644 --- a/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx +++ b/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx @@ -26,7 +26,7 @@ import type { FC } from 'react' import React, { useState, useEffect } from 'react' -import type { ApiModel, DiffRow, SpecList, SpecItem } from '@looker/sdk-codegen' +import type { ApiModel, DiffRow, SpecList } from '@looker/sdk-codegen' import { useHistory, useRouteMatch } from 'react-router-dom' import { Box, @@ -38,35 +38,14 @@ import { SelectMulti, } from '@looker/components' import { SyncAlt } from '@styled-icons/material/SyncAlt' +import { useSelector } from 'react-redux' -import { getDefaultSpecKey } from '../../reducers/spec/utils' -import { diffPath, fallbackFetch, funFetch } from '../../utils' import { ApixSection } from '../../components' +import { selectCurrentSpec } from '../../state' +import { diffPath, getApixAdaptor } from '../../utils' import { diffSpecs, standardDiffToggles } from './diffUtils' import { DocDiff } from './DocDiff' -/** - * Pick the left key, or default spec - * @param specs to pick from - * @param leftKey spec key that may or may not have a value - */ -const pickLeft = (specs: SpecList, leftKey: string) => { - if (leftKey) return leftKey - return getDefaultSpecKey(specs) -} - -/** - * Pick the right key, or "other" spec - * @param specs to pick from - * @param rightKey spec key from url path - * @param leftKey spec key from url path - */ -const pickRight = (specs: SpecList, rightKey: string, leftKey: string) => { - if (rightKey) return rightKey - if (!leftKey) leftKey = getDefaultSpecKey(specs) - return Object.keys(specs).find((k) => k !== leftKey) || '' -} - const diffToggles = [ { label: 'Missing', @@ -104,7 +83,10 @@ const validateParam = (specs: SpecList, specKey = '') => { } export const DiffScene: FC = ({ specs, toggleNavigation }) => { + const adaptor = getApixAdaptor() const history = useHistory() + const spec = useSelector(selectCurrentSpec) + const currentSpecKey = spec.key const match = useRouteMatch<{ l: string; r: string }>(`/${diffPath}/:l?/:r?`) const l = validateParam(specs, match?.params.l) const r = validateParam(specs, match?.params.r) @@ -114,10 +96,10 @@ export const DiffScene: FC = ({ specs, toggleNavigation }) => { label: `${key} (${spec.status})`, })) - const [leftKey, setLeftKey] = useState(pickLeft(specs, l)) - const [rightKey, setRightKey] = useState(pickRight(specs, r, leftKey)) + const [leftKey, setLeftKey] = useState(l || currentSpecKey) + const [rightKey, setRightKey] = useState(r || '') const [leftApi, setLeftApi] = useState(specs[leftKey].api!) - const [rightApi, setRightApi] = useState( + const [rightApi, setRightApi] = useState(() => rightKey ? specs[rightKey].api! : specs[leftKey].api! ) const [toggles, setToggles] = useState(standardDiffToggles) @@ -126,73 +108,46 @@ export const DiffScene: FC = ({ specs, toggleNavigation }) => { toggleNavigation(false) }, []) - const computeDelta = (left: string, right: string, toggles: string[]) => { - if (left && right && specs[left].api && specs[right].api) { - return diffSpecs(specs[left].api!, specs[right].api!, toggles) - } - return [] + const [delta, setDelta] = useState([]) + + const handleLeftChange = (newLeft: string) => { + setLeftKey(newLeft) + } + const handleRightChange = (newRight: string) => { + setRightKey(newRight) } - const [delta, setDelta] = useState( - computeDelta(leftKey, rightKey, toggles) - ) - const loadSpec = async ( - specs: SpecList, - key: string, - setter: (spec: SpecItem) => void - ) => { - const spec = specs[key] - if (!spec.api) { - try { - const newSpec = { ...spec } - const api = await fallbackFetch(newSpec, funFetch) - if (api) { - spec.api = api - setter(spec) - } - } catch (error) { - console.error(error) - } - } + const handleSwitch = () => { + const currLeftKey = leftKey + setLeftKey(rightKey) + setRightKey(currLeftKey) } - const compareKeys = async (left: string, right: string) => { - if (left !== leftKey || right !== rightKey) { - history.push(`/${diffPath}/${left}/${right}`) - } - if (!specs[left].api) { - await loadSpec(specs, leftKey, (spec) => setLeftApi(spec.api!)) - } - if (!specs[right].api) { - await loadSpec(specs, rightKey, (spec) => setRightApi(spec.api!)) + useEffect(() => { + adaptor.fetchSpec(specs[leftKey]).then((spec) => setLeftApi(spec.api!)) + }, [leftKey]) + + useEffect(() => { + if (rightKey in specs) { + adaptor.fetchSpec(specs[rightKey]).then((spec) => setRightApi(spec.api!)) } - setDelta([...computeDelta(left, right, toggles)]) - } + }, [rightKey]) - const handleLeftChange = (newLeft: string) => { - setLeftKey(newLeft) - } + useEffect(() => { + if (leftApi && rightApi) { + setDelta([...diffSpecs(leftApi, rightApi, toggles)]) + } + }, [leftApi, rightApi, toggles]) const handleTogglesChange = (values?: string[]) => { const newToggles = values || [] setToggles(newToggles) - setDelta([...computeDelta(leftKey, rightKey, newToggles)]) } useEffect(() => { - compareKeys(leftKey, rightKey) + history.push(`/${diffPath}/${leftKey}/${rightKey}`) }, [leftKey, rightKey]) - const handleRightChange = (newRight: string) => { - setRightKey(newRight) - } - - const handleSwitch = () => { - const currLeftKey = leftKey - setLeftKey(rightKey) - setRightKey(currLeftKey) - } - return ( @@ -211,6 +166,7 @@ export const DiffScene: FC = ({ specs, toggleNavigation }) => { } mt="medium" onClick={handleSwitch} @@ -221,6 +177,7 @@ export const DiffScene: FC = ({ specs, toggleNavigation }) => { mt="xxsmall" id="compare" name="Right Version" + placeholder="Select a comparison..." value={rightKey} options={options} onChange={handleRightChange} diff --git a/packages/api-explorer/src/state/specs/slice.ts b/packages/api-explorer/src/state/specs/slice.ts index cff789cdc..6717c176f 100644 --- a/packages/api-explorer/src/state/specs/slice.ts +++ b/packages/api-explorer/src/state/specs/slice.ts @@ -52,7 +52,6 @@ export interface InitSpecsAction { export interface InitSpecsSuccessPayload { specs: SpecList currentSpecKey: string - // api: ApiModel } export interface SetCurrentSpecAction { @@ -78,7 +77,6 @@ export const specsSlice = createSlice({ ) { state.specs = action.payload.specs state.currentSpecKey = action.payload.currentSpecKey - // state.specs[action.payload.currentSpecKey].api = action.payload.api state.working = false state.description = undefined }, diff --git a/packages/api-explorer/src/test-data/index.ts b/packages/api-explorer/src/test-data/index.ts index 0fdf9e13a..e355186cf 100644 --- a/packages/api-explorer/src/test-data/index.ts +++ b/packages/api-explorer/src/test-data/index.ts @@ -23,13 +23,6 @@ SOFTWARE. */ -export { - api, - api40, - specs, - specState, - getLoadedSpecs, - getLoadedSpecState, -} from './specs' +export * from './specs' export { examples } from './examples' export * from './declarations' diff --git a/packages/api-explorer/src/test-data/specs.ts b/packages/api-explorer/src/test-data/specs.ts index 960e42a4f..2748492c0 100644 --- a/packages/api-explorer/src/test-data/specs.ts +++ b/packages/api-explorer/src/test-data/specs.ts @@ -25,8 +25,8 @@ */ import type { SpecList } from '@looker/sdk-codegen' import { ApiModel } from '@looker/sdk-codegen' - -import { initDefaultSpecState } from '../reducers' +import type { SpecState } from '@looker/api-explorer' +import { defaultSpecsState } from '@looker/api-explorer' export const specs: SpecList = { '3.1': { @@ -47,8 +47,6 @@ export const specs: SpecList = { }, } -export const specState = initDefaultSpecState(specs, window.location) - export const api = ApiModel.fromJson(specs['3.1'].specContent) export const api40 = ApiModel.fromJson(specs['4.0'].specContent) @@ -59,5 +57,7 @@ export const getLoadedSpecs = () => { return loadedSpecs } -export const getLoadedSpecState = () => - initDefaultSpecState(getLoadedSpecs(), window.location) +export const specState: SpecState = { + ...defaultSpecsState, + specs: getLoadedSpecs(), +} diff --git a/packages/api-explorer/src/test-utils/redux.tsx b/packages/api-explorer/src/test-utils/redux.tsx index 18db62e33..f0f6023a8 100644 --- a/packages/api-explorer/src/test-utils/redux.tsx +++ b/packages/api-explorer/src/test-utils/redux.tsx @@ -43,7 +43,7 @@ import { defaultSpecsState, specsSlice, } from '../state' -import { specs } from '../test-data' +import { specState } from '../test-data' import { renderWithRouter } from './router' export const withReduxProvider = ( @@ -90,8 +90,7 @@ export const createTestStore = (overrides?: DeepPartial) => ...overrides?.lodes, } as LodesState, specs: { - ...defaultSpecsState, - specs, + ...specState, ...overrides?.specs, } as SpecState, }, diff --git a/packages/api-explorer/src/utils/index.ts b/packages/api-explorer/src/utils/index.ts index 6be75641b..31475165d 100644 --- a/packages/api-explorer/src/utils/index.ts +++ b/packages/api-explorer/src/utils/index.ts @@ -24,13 +24,7 @@ */ export { highlightHTML } from './highlight' -export { - buildPath, - buildMethodPath, - buildTypePath, - diffPath, - oAuthPath, -} from './path' +export * from './path' export { getLoded } from './lodeUtils' export { useWindowSize } from './useWindowSize' export * from './apixAdaptor' diff --git a/packages/api-explorer/src/utils/path.ts b/packages/api-explorer/src/utils/path.ts index c852f3ce9..5a38dd667 100644 --- a/packages/api-explorer/src/utils/path.ts +++ b/packages/api-explorer/src/utils/path.ts @@ -26,6 +26,7 @@ import type { ApiModel, IMethod, IType } from '@looker/sdk-codegen' import { firstMethodRef } from '@looker/sdk-codegen' +import type { Location as HLocation } from 'history' /** * Builds a path matching the route used by MethodScene @@ -105,7 +106,18 @@ export const buildPath = ( return path } -export const specKeyFromPath = (path: string) => { - const match = path.match(/^\/(\w+.\w+).*$/) - return match ? match[1] : null +/** + * Determine API specification keys from URL pattern + * @param location service to examine + */ +export const getSpecKey = (location: HLocation | Location): string | null => { + const pathname = location.pathname + let match + if (pathname.startsWith(`/${diffPath}`)) { + const pattern = new RegExp(`(?:/${diffPath})/(?\\w+.\\w+)`) + match = pathname.match(pattern) + } else { + match = pathname.match(/\/(?\w+\.\w+).*/) + } + return match?.groups?.specKey || null } diff --git a/packages/run-it/src/RunIt.spec.tsx b/packages/run-it/src/RunIt.spec.tsx index 81a2cb966..331f63faf 100644 --- a/packages/run-it/src/RunIt.spec.tsx +++ b/packages/run-it/src/RunIt.spec.tsx @@ -33,7 +33,7 @@ import type { ApiModel, IMethod } from '@looker/sdk-codegen' import { BrowserAdaptor, OAuthConfigProvider } from '@looker/extension-utils' import { RunIt } from './RunIt' import { api, testTextResponse } from './test-data' -import { initRunItSdk, runItNoSet } from './utils' +import { initRunItSdk } from './utils' import { RunItProvider } from './RunItProvider' describe('RunIt', () => { @@ -47,12 +47,7 @@ describe('RunIt', () => { ) => { renderWithTheme( - + ) } diff --git a/packages/run-it/src/components/RequestForm/RequestForm.spec.tsx b/packages/run-it/src/components/RequestForm/RequestForm.spec.tsx index dfe82ef80..2711faa09 100644 --- a/packages/run-it/src/components/RequestForm/RequestForm.spec.tsx +++ b/packages/run-it/src/components/RequestForm/RequestForm.spec.tsx @@ -52,7 +52,6 @@ describe('RequestForm', () => { const message = 'Invalid message' renderWithTheme( { test('it creates a form with a simple item, submit button, and config button if not an extension', () => { renderWithTheme( { test('it creates a form with a simple item, submit button, and config button if running as an extension', () => { renderWithTheme( { const name = 'boolean_item' renderWithTheme( { const name = 'date_item' renderWithTheme( { const name = 'number_item' renderWithTheme( { test('interacting with a text simple item changes the request content', async () => { renderWithTheme( { ] renderWithTheme( { const handleSubmit = jest.fn((e) => e.preventDefault()) renderWithTheme( { const apiTestModel = specFromFile(`${testPath}openApiRef.json`) return { apiTestModel, - baseUrl: api_server_url, + baseUrl, config, configContents, dataFile, diff --git a/packages/sdk-codegen/src/testUtils/testUtils.ts b/packages/sdk-codegen/src/testUtils/testUtils.ts index 8703aaa6e..82f62b164 100644 --- a/packages/sdk-codegen/src/testUtils/testUtils.ts +++ b/packages/sdk-codegen/src/testUtils/testUtils.ts @@ -91,7 +91,7 @@ export const TestConfig = (rootPath = ''): ITestConfig => { const apiTestModel = specFromFile(`${testPath}openApiRef.json`) return { apiTestModel, - baseUrl: api_server_url, + baseUrl, config, configContents, dataFile, diff --git a/packages/sdk-node/src/testUtils/testUtils.ts b/packages/sdk-node/src/testUtils/testUtils.ts index 0e3daa866..40f6ad92d 100644 --- a/packages/sdk-node/src/testUtils/testUtils.ts +++ b/packages/sdk-node/src/testUtils/testUtils.ts @@ -83,7 +83,7 @@ export const TestConfig = (rootPath = ''): ITestConfig => { if (!fs.existsSync(testIni)) testIni = '' return { - baseUrl: api_server_url, + baseUrl, dataFile, localIni, rootPath, diff --git a/packages/sdk-rtl/src/oauthSession.ts b/packages/sdk-rtl/src/oauthSession.ts index c54243808..0bff45821 100644 --- a/packages/sdk-rtl/src/oauthSession.ts +++ b/packages/sdk-rtl/src/oauthSession.ts @@ -130,10 +130,8 @@ export class OAuthSession extends AuthSession { */ set returnUrl(value: string | null) { if (!value) { - console.log('Removing return url') sessionStorage.removeItem(OAuthSession.returnUrlKey) } else { - console.log(`Saving return url ${value}`) sessionStorage.setItem(OAuthSession.returnUrlKey, value) } } diff --git a/yarn.lock b/yarn.lock index eb77f5576..414d70218 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4101,11 +4101,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -5519,10 +5514,10 @@ css-to-react-native@^3.0.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" -css-what@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" - integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== +"css-what@>= 5.0.1", css-what@^4.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.0.1.tgz#3be33be55b9f302f710ba3a9c3abc1e2a63fc7eb" + integrity sha512-z93ZGFLNc6yaoXAmVhqoSIb+BduplteCt1fepvwhBUQK6MNE4g6fgjpuZKJKp0esUe+vXWlIkwZZjNWoOKw0ZA== css.escape@^1.5.1: version "1.5.1" @@ -7513,20 +7508,12 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== +"glob-parent@>= 5.1.2", glob-parent@^3.1.0, glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: - is-glob "^4.0.1" + is-glob "^4.0.3" glob-to-regexp@^0.3.0: version "0.3.0" @@ -8533,7 +8520,7 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" -is-extglob@^2.1.0, is-extglob@^2.1.1: +is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= @@ -8565,14 +8552,7 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -11668,11 +11648,6 @@ path-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -14442,20 +14417,10 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - -trim-newlines@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" - integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= - -trim-newlines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" - integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +"trim-newlines@>= 3.0.1", trim-newlines@^1.0.0, trim-newlines@^2.0.0, trim-newlines@^3.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.0.2.tgz#d6aaaf6a0df1b4b536d183879a6b939489808c7c" + integrity sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew== trim-off-newlines@^1.0.0: version "1.0.3" @@ -14467,10 +14432,10 @@ trim-trailing-lines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== -trim@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" - integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= +trim@0.0.1, "trim@>= 0.0.3": + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-1.0.1.tgz#68e78f6178ccab9687a610752f4f5e5a7022ee8c" + integrity sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w== trough@^1.0.0: version "1.0.5" @@ -15454,22 +15419,10 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" -ws@7.4.6: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== - -ws@^6.2.1: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" - integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== - dependencies: - async-limiter "~1.0.0" - -ws@^7.0.0, ws@^7.2.3, ws@^7.3.1: - version "7.5.7" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" - integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== +ws@7.4.6, "ws@>= 7.4.6", ws@^6.2.1, ws@^7.0.0, ws@^7.2.3, ws@^7.3.1: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== xml-name-validator@^3.0.0: version "3.0.0" From 5971391a0288d352deed94c53a3bda8e66255bb8 Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Fri, 11 Feb 2022 16:18:37 +0000 Subject: [PATCH 04/13] fix: incorrect import --- packages/api-explorer/src/test-data/specs.ts | 4 ++-- packages/run-it/src/components/ConfigForm/utils.ts | 3 ++- packages/sdk-rtl/src/oauthSession.ts | 7 ++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/api-explorer/src/test-data/specs.ts b/packages/api-explorer/src/test-data/specs.ts index 2748492c0..18e1f2b97 100644 --- a/packages/api-explorer/src/test-data/specs.ts +++ b/packages/api-explorer/src/test-data/specs.ts @@ -25,8 +25,8 @@ */ import type { SpecList } from '@looker/sdk-codegen' import { ApiModel } from '@looker/sdk-codegen' -import type { SpecState } from '@looker/api-explorer' -import { defaultSpecsState } from '@looker/api-explorer' +import type { SpecState } from '../state' +import { defaultSpecsState } from '../state' export const specs: SpecList = { '3.1': { diff --git a/packages/run-it/src/components/ConfigForm/utils.ts b/packages/run-it/src/components/ConfigForm/utils.ts index 0481231c1..aa828c4d1 100644 --- a/packages/run-it/src/components/ConfigForm/utils.ts +++ b/packages/run-it/src/components/ConfigForm/utils.ts @@ -25,7 +25,8 @@ */ import type { ILookerVersions } from '@looker/sdk-codegen' import { BrowserTransport, DefaultSettings } from '@looker/sdk-rtl' -import type { RunItValues } from '@looker/run-it' + +import type { RunItValues } from '../..' export const RunItConfigKey = 'RunItConfig' export const RunItFormKey = 'RunItForm' diff --git a/packages/sdk-rtl/src/oauthSession.ts b/packages/sdk-rtl/src/oauthSession.ts index 0bff45821..86593fe63 100644 --- a/packages/sdk-rtl/src/oauthSession.ts +++ b/packages/sdk-rtl/src/oauthSession.ts @@ -181,8 +181,7 @@ export class OAuthSession extends AuthSession { } await this.redeemAuthCode(code) } - const token = await this.getToken() - return token + return await this.getToken() } return this.activeToken } @@ -258,9 +257,7 @@ export class OAuthSession extends AuthSession { * @returns {Promise} */ async redeemAuthCode(authCode: string, codeVerifier?: string) { - const body = this.redeemAuthCodeBody(authCode, codeVerifier) - const token = await this.requestToken(body) - return token + return this.requestToken(this.redeemAuthCodeBody(authCode, codeVerifier)) } async getToken() { From b100be60031c848ae336c53e5e51fa94df8a5270 Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Fri, 11 Feb 2022 16:36:55 +0000 Subject: [PATCH 05/13] unexport incorrectly exported saga --- packages/api-explorer/src/state/specs/sagas.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/api-explorer/src/state/specs/sagas.ts b/packages/api-explorer/src/state/specs/sagas.ts index 10b497997..2eb610c13 100644 --- a/packages/api-explorer/src/state/specs/sagas.ts +++ b/packages/api-explorer/src/state/specs/sagas.ts @@ -52,9 +52,7 @@ function* initSaga(action: PayloadAction) { } } -export function* setCurrentSpecSaga( - action: PayloadAction -) { +function* setCurrentSpecSaga(action: PayloadAction) { const { setCurrentSpecSuccessAction, setCurrentSpecFailureAction } = specActions const adaptor = getApixAdaptor() From 80baeefac230f440091e0784d0f0888df5e3a3cb Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Fri, 11 Feb 2022 16:45:10 +0000 Subject: [PATCH 06/13] yarn dedupe.. maybe CI passes now --- yarn.lock | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/yarn.lock b/yarn.lock index 414d70218..7b1cf9806 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3055,12 +3055,7 @@ dependencies: "@types/unist" "*" -"@types/history@*": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@^4.7.11": +"@types/history@*", "@types/history@^4.7.11": version "4.7.11" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== @@ -3137,12 +3132,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/lodash@^4.14.157", "@types/lodash@^4.14.162", "@types/lodash@^4.14.175": - version "4.14.175" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45" - integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw== - -"@types/lodash@^4.14.178": +"@types/lodash@^4.14.157", "@types/lodash@^4.14.162", "@types/lodash@^4.14.175", "@types/lodash@^4.14.178": version "4.14.178" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== @@ -3252,15 +3242,7 @@ "@types/react" "*" "@types/react-router" "*" -"@types/react-router@*", "@types/react-router@^5.1.11": - version "5.1.12" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.12.tgz#0f300e09468e7aed86e18241c90238c18c377e51" - integrity sha512-0bhXQwHYfMeJlCh7mGhc0VJTRm0Gk+Z8T00aiP4702mDUuLs9SMhnd2DitpjWFjdOecx2UXtICK14H9iMnziGA== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router@^5.1.18": +"@types/react-router@*", "@types/react-router@^5.1.11", "@types/react-router@^5.1.18": version "5.1.18" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.18.tgz#c8851884b60bc23733500d86c1266e1cfbbd9ef3" integrity sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g== From 86fdce711f22caa734de2dbe283fdef9c5569f59 Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Fri, 11 Feb 2022 19:15:04 +0000 Subject: [PATCH 07/13] fix package configuration --- examples/access-token-server/package.json | 6 +++--- packages/api-explorer/package.json | 8 ++++---- packages/code-editor/package.json | 2 +- packages/extension-api-explorer/package.json | 4 ++-- packages/extension-playground/package.json | 4 ++-- packages/extension-sdk-react/package.json | 4 ++-- packages/extension-sdk/package.json | 2 +- packages/extension-utils/package.json | 2 +- packages/hackathon/package.json | 4 ++-- packages/run-it/package.json | 6 +++--- packages/sdk-codegen-scripts/package.json | 8 ++++---- packages/sdk-codegen/package.json | 4 ++-- packages/sdk-node/package.json | 4 ++-- packages/sdk-rtl/package.json | 2 +- packages/sdk/package.json | 2 +- packages/wholly-sheet/package.json | 6 +++--- yarn.lock | 21 -------------------- 17 files changed, 34 insertions(+), 55 deletions(-) diff --git a/examples/access-token-server/package.json b/examples/access-token-server/package.json index e80f3f86b..6313e1649 100644 --- a/examples/access-token-server/package.json +++ b/examples/access-token-server/package.json @@ -11,9 +11,9 @@ "create-status-json": "ts-node script/create_status_json.ts" }, "dependencies": { - "@looker/sdk": "^21.14.0", + "@looker/sdk": "^22.0.0", "@looker/sdk-rtl": "^21.0.20", - "@looker/sdk-node": "^21.14.0", + "@looker/sdk-node": "^22.0.0", "body-parser": "^1.19.0", "crypto-js": "^4.0.0", "dotenv": "^8.2.0", @@ -35,4 +35,4 @@ "ts-node": "^10.2.1", "typescript": "^4.4.3" } -} \ No newline at end of file +} diff --git a/packages/api-explorer/package.json b/packages/api-explorer/package.json index 05ec9080a..8b076f314 100644 --- a/packages/api-explorer/package.json +++ b/packages/api-explorer/package.json @@ -34,7 +34,7 @@ "devDependencies": { "@looker/components-test-utils": "^1.5.5", "@looker/sdk-codegen-scripts": "^21.2.1", - "@looker/sdk-node": "^21.20.1", + "@looker/sdk-node": "^22.0.0", "@styled-icons/styled-icon": "^10.6.3", "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.2.2", @@ -75,9 +75,9 @@ "@looker/icons": "^1.5.3", "@looker/redux": "0.0.0", "@looker/run-it": "^0.9.26", - "@looker/sdk": "^21.20.1", - "@looker/sdk-codegen": "^21.3.1", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk": "^22.0.0", + "@looker/sdk-codegen": "^21.3.2", + "@looker/sdk-rtl": "^21.3.2", "@reduxjs/toolkit": "^1.6.2", "@styled-icons/material": "^10.28.0", "@styled-icons/material-outlined": "^10.28.0", diff --git a/packages/code-editor/package.json b/packages/code-editor/package.json index 326bb6e0d..9194680fc 100644 --- a/packages/code-editor/package.json +++ b/packages/code-editor/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@looker/components-test-utils": "^1.5.5", - "@looker/sdk-codegen": "^21.3.1", + "@looker/sdk-codegen": "^21.3.2", "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.2.2", "@testing-library/user-event": "^12.6.0", diff --git a/packages/extension-api-explorer/package.json b/packages/extension-api-explorer/package.json index 8f7319b86..3aa605661 100644 --- a/packages/extension-api-explorer/package.json +++ b/packages/extension-api-explorer/package.json @@ -22,8 +22,8 @@ "@looker/extension-utils": "^0.1.3", "@looker/icons": "^1.5.3", "@looker/run-it": "^0.9.26", - "@looker/sdk": "^21.20.1", - "@looker/sdk-codegen": "^21.3.1", + "@looker/sdk": "^22.0.0", + "@looker/sdk-codegen": "^21.3.2", "@styled-icons/material": "^10.28.0", "@styled-icons/material-outlined": "^10.28.0", "@styled-icons/material-rounded": "^10.28.0", diff --git a/packages/extension-playground/package.json b/packages/extension-playground/package.json index d1b934d6b..28bdcc556 100644 --- a/packages/extension-playground/package.json +++ b/packages/extension-playground/package.json @@ -16,8 +16,8 @@ "dependencies": { "@looker/extension-sdk": "^21.4.1", "@looker/extension-sdk-react": "^21.4.1", - "@looker/sdk": "^21.4.1", - "@looker/sdk-codegen": "^21.3.1", + "@looker/sdk": "^22.0.0", + "@looker/sdk-codegen": "^21.3.2", "@looker/components": "^2.8.1", "@looker/icons": "^1.5.3", "@styled-icons/material": "^10.28.0", diff --git a/packages/extension-sdk-react/package.json b/packages/extension-sdk-react/package.json index d18c4473a..3f1e592a7 100644 --- a/packages/extension-sdk-react/package.json +++ b/packages/extension-sdk-react/package.json @@ -44,8 +44,8 @@ }, "dependencies": { "@looker/extension-sdk": "^21.20.1", - "@looker/sdk": "^21.20.1", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk": "^22.0.0", + "@looker/sdk-rtl": "^21.3.2", "history": "^4.9.0", "lodash": "^4.17.20" }, diff --git a/packages/extension-sdk/package.json b/packages/extension-sdk/package.json index 3beb0816e..dcdbec06f 100644 --- a/packages/extension-sdk/package.json +++ b/packages/extension-sdk/package.json @@ -41,7 +41,7 @@ "dependencies": { "@looker/chatty": "^2.3.0", "@looker/sdk": "^22.0.0", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk-rtl": "^21.3.2", "deepmerge": "^4.2.2", "readable-stream": "^3.4.0", "request": "^2.88.0", diff --git a/packages/extension-utils/package.json b/packages/extension-utils/package.json index b1d4a7b22..0c15145b8 100644 --- a/packages/extension-utils/package.json +++ b/packages/extension-utils/package.json @@ -28,7 +28,7 @@ "@looker/components": "^2.8.1", "@looker/extension-sdk": "^21.20.1", "@looker/extension-sdk-react": "^21.20.1", - "@looker/sdk-rtl": "^21.2.0", + "@looker/sdk-rtl": "^21.3.2", "react": "^16.13.1" }, "devDependencies": { diff --git a/packages/hackathon/package.json b/packages/hackathon/package.json index 76ffb9129..b5cb00051 100644 --- a/packages/hackathon/package.json +++ b/packages/hackathon/package.json @@ -40,8 +40,8 @@ "@looker/extension-sdk-react": "^21.20.1", "@looker/extension-utils": "^0.1.3", "@looker/icons": "^1.5.3", - "@looker/sdk": "^21.20.1", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk": "^22.0.0", + "@looker/sdk-rtl": "^21.3.2", "@looker/wholly-sheet": "^0.5.23", "@styled-icons/material": "^10.28.0", "@styled-icons/material-outlined": "^10.28.0", diff --git a/packages/run-it/package.json b/packages/run-it/package.json index d2c583bfa..2dd705068 100644 --- a/packages/run-it/package.json +++ b/packages/run-it/package.json @@ -57,10 +57,10 @@ "@looker/design-tokens": "^2.7.1", "@looker/extension-utils": "^0.1.3", "@looker/icons": "^1.5.3", - "@looker/sdk": "^21.20.1", - "@looker/sdk-codegen": "^21.3.1", + "@looker/sdk": "^22.0.0", + "@looker/sdk-codegen": "^21.3.2", "@looker/sdk-codegen-utils": "^21.0.11", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk-rtl": "^21.3.2", "@styled-icons/material": "^10.28.0", "@styled-icons/material-outlined": "^10.28.0", "@styled-icons/material-rounded": "^10.28.0", diff --git a/packages/sdk-codegen-scripts/package.json b/packages/sdk-codegen-scripts/package.json index 6bb87dddc..f23ac9553 100644 --- a/packages/sdk-codegen-scripts/package.json +++ b/packages/sdk-codegen-scripts/package.json @@ -27,11 +27,11 @@ "watch:cjs": "yarn lerna exec --scope @looker/sdk-codegen-scripts --stream 'BABEL_ENV=build_cjs babel src --root-mode upward --out-dir lib --source-maps --extensions .ts,.tsx --no-comments --watch'" }, "dependencies": { - "@looker/sdk": "^21.20.1", - "@looker/sdk-codegen": "^21.3.1", + "@looker/sdk": "^22.0.0", + "@looker/sdk-codegen": "^21.3.2", "@looker/sdk-codegen-utils": "^21.0.11", - "@looker/sdk-node": "^21.20.1", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk-node": "^22.0.0", + "@looker/sdk-rtl": "^21.3.2", "config": "^3.3.1", "cross-env": "^7.0.2" }, diff --git a/packages/sdk-codegen/package.json b/packages/sdk-codegen/package.json index df1352776..c865128c0 100644 --- a/packages/sdk-codegen/package.json +++ b/packages/sdk-codegen/package.json @@ -1,6 +1,6 @@ { "name": "@looker/sdk-codegen", - "version": "22.0.0", + "version": "21.3.2", "description": "Looker SDK Codegen core", "main": "lib/index.js", "module": "lib/esm/index.js", @@ -36,7 +36,7 @@ }, "dependencies": { "@looker/sdk-codegen-utils": "^21.0.11", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk-rtl": "^21.3.2", "blueimp-md5": "^2.13.0", "openapi3-ts": "^1.3.0" }, diff --git a/packages/sdk-node/package.json b/packages/sdk-node/package.json index 4ccb15bdf..c3afa9c2b 100644 --- a/packages/sdk-node/package.json +++ b/packages/sdk-node/package.json @@ -1,6 +1,6 @@ { "name": "@looker/sdk-node", - "version": "21.20.1", + "version": "22.0.0", "description": "Looker SDK Runtime for Node Library", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -39,7 +39,7 @@ }, "dependencies": { "@looker/sdk": "^22.0.0", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk-rtl": "^21.3.2", "ini": "^1.3.8", "readable-stream": "^3.4.0", "request": "^2.88.0", diff --git a/packages/sdk-rtl/package.json b/packages/sdk-rtl/package.json index 2640d7027..746a20d43 100644 --- a/packages/sdk-rtl/package.json +++ b/packages/sdk-rtl/package.json @@ -1,6 +1,6 @@ { "name": "@looker/sdk-rtl", - "version": "21.3.1", + "version": "21.3.2", "description": "Looker SDK Runtime Library", "main": "lib/index.js", "module": "lib/esm/index.js", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 47f575523..f7e73dc16 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -41,7 +41,7 @@ "@types/request-promise-native": "^1.0.17" }, "dependencies": { - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk-rtl": "^21.3.2", "ini": "^1.3.8", "readable-stream": "^3.4.0", "request": "^2.88.0", diff --git a/packages/wholly-sheet/package.json b/packages/wholly-sheet/package.json index 3391bd5ce..5aae69733 100644 --- a/packages/wholly-sheet/package.json +++ b/packages/wholly-sheet/package.json @@ -31,13 +31,13 @@ "watch": "yarn lerna exec --scope @looker/wholly-sheet --stream 'BABEL_ENV=build babel src --root-mode upward --out-dir lib/esm --source-maps --extensions .ts,.tsx --no-comments --watch'" }, "dependencies": { - "@looker/sdk": "^21.20.1", - "@looker/sdk-rtl": "^21.3.1", + "@looker/sdk": "^22.0.0", + "@looker/sdk-rtl": "^21.3.2", "lodash": "^4.17.20", "uuid": "^8.3.1" }, "devDependencies": { - "@looker/sdk-node": "^21.20.1", + "@looker/sdk-node": "^22.0.0", "@types/uuid": "^8.3.0", "google-auth-library": "^6.1.0" } diff --git a/yarn.lock b/yarn.lock index 7b1cf9806..51f93faa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2358,27 +2358,6 @@ dependencies: "@styled-icons/styled-icon" "^10.6.3" -"@looker/sdk-codegen@^21.3.1": - version "21.3.1" - resolved "https://registry.yarnpkg.com/@looker/sdk-codegen/-/sdk-codegen-21.3.1.tgz#8af443062d6948600327c5e52615751951cc906c" - integrity sha512-kUHs6+8DrLpMWYrJ2ACrwnjczb7HyMrvESUZuk0RqrXc8si48CcuQq5/RI+Ver+7SnzVtct4hJZa+fpSF789TA== - dependencies: - "@looker/sdk-codegen-utils" "^21.0.11" - "@looker/sdk-rtl" "^21.3.1" - blueimp-md5 "^2.13.0" - openapi3-ts "^1.3.0" - -"@looker/sdk@^21.20.1", "@looker/sdk@^21.4.1": - version "21.20.1" - resolved "https://registry.yarnpkg.com/@looker/sdk/-/sdk-21.20.1.tgz#3d257309c2fe5ad1246e01837ea71aeeb7f4df22" - integrity sha512-t54yXAMeJAIeq7uUc1Oa3SdFsE3zvQaL9S28ebXy9TrljbnHG4FsVMacfq0XQGYPDzYUIoEySShWcBOsFZ+eyw== - dependencies: - "@looker/sdk-rtl" "^21.3.1" - ini "^1.3.8" - readable-stream "^3.4.0" - request "^2.88.0" - request-promise-native "^1.0.8" - "@mdx-js/util@1.6.22": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" From dd703636956d909ec3d420e62d1214261a0fa4ac Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Fri, 11 Feb 2022 19:41:02 +0000 Subject: [PATCH 08/13] fix: Diff links --- .../DiffScene/DocDiff/DiffItem.spec.tsx | 24 +++++++------------ .../src/scenes/DiffScene/DocDiff/DiffItem.tsx | 23 +++++------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx index 0b690364f..10eebf5b2 100644 --- a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx +++ b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx @@ -28,8 +28,7 @@ import { Router } from 'react-router' import type { MemoryHistory } from 'history' import { createMemoryHistory } from 'history' import { renderWithTheme } from '@looker/components-test-utils' -import userEvent from '@testing-library/user-event' -import { act, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import { api } from '../../../test-data' import { DiffMethodLink } from './DiffItem' @@ -37,11 +36,9 @@ describe('DiffMethodLink', () => { const method = api.methods.create_dashboard const specKey = '4.0' let history: MemoryHistory - let pushSpy: jasmine.Spy beforeEach(() => { history = createMemoryHistory() - pushSpy = spyOn(history, 'push') }) test('it renders method and navigates on click', () => { @@ -50,14 +47,12 @@ describe('DiffMethodLink', () => { ) - const s = `${method.name} for ${specKey}` - expect(screen.getByText(s)).toBeInTheDocument() - userEvent.click(screen.getByText(s)) - act(() => { - expect(pushSpy).toHaveBeenCalledWith( - `/${specKey}/methods/${method.schema.tags[0]}/${method.name}` - ) - }) + const link = screen.getByRole('link') + expect(link).toHaveTextContent(`${method.name} for ${specKey}`) + expect(link).toHaveAttribute( + 'href', + `/${specKey}/methods/${method.schema.tags[0]}/${method.name}` + ) }) test('it renders missing method and does not navigate on click', () => { @@ -68,9 +63,6 @@ describe('DiffMethodLink', () => { ) const s = `Missing in ${specKey}` expect(screen.getByText(s)).toBeInTheDocument() - userEvent.click(screen.getByText(s)) - act(() => { - expect(pushSpy).not.toHaveBeenCalled() - }) + expect(screen.queryByRole('link')).not.toBeInTheDocument() }) }) diff --git a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx index 6198887ea..a9f2a1501 100644 --- a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx +++ b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx @@ -23,12 +23,11 @@ SOFTWARE. */ -import type { FC, BaseSyntheticEvent } from 'react' +import type { FC } from 'react' import React, { useState, useEffect } from 'react' import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer' -import { useHistory } from 'react-router' import styled from 'styled-components' -import { Accordion2, Box, Card, Grid, Heading } from '@looker/components' +import { Accordion2, Box, Card, Grid, Heading, Link } from '@looker/components' import type { DiffRow } from '@looker/sdk-codegen/src' import type { ApiModel, IMethod } from '@looker/sdk-codegen' import { useSelector } from 'react-redux' @@ -42,7 +41,7 @@ interface DiffMethodLinkProps { specKey: string } -const DiffLink = styled(Heading)` +const DiffLink = styled(Link)` color: ${({ theme }) => theme.colors.ui5}; cursor: pointer; display: block; @@ -60,22 +59,12 @@ export const DiffMethodLink: FC = ({ method, specKey, }) => { - const history = useHistory() if (!method) return {`Missing in ${specKey}`} - const handleClick = (e: BaseSyntheticEvent) => { - e.stopPropagation() - const tag = method.schema.tags[0] - const path = `${buildMethodPath(specKey, tag, method.name)}` - history.push(path) - } + const tag = method.schema.tags[0] + const path = `${buildMethodPath(specKey, tag, method.name)}` - return ( - {`${method.name} for ${specKey}`} - ) + return {`${method.name} for ${specKey}`} } interface DiffItemProps { From 69a82b35ba56871fae3dad9baeef42216d8db493 Mon Sep 17 00:00:00 2001 From: Bryn Ryans Date: Fri, 11 Feb 2022 23:41:55 +0000 Subject: [PATCH 09/13] fix: stop diff links from reloading --- .../src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx | 7 ++++--- .../src/scenes/DiffScene/DocDiff/DiffItem.tsx | 12 +++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx index 10eebf5b2..8ba670eaa 100644 --- a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx +++ b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx @@ -28,7 +28,7 @@ import { Router } from 'react-router' import type { MemoryHistory } from 'history' import { createMemoryHistory } from 'history' import { renderWithTheme } from '@looker/components-test-utils' -import { screen } from '@testing-library/react' +import { screen, fireEvent } from '@testing-library/react' import { api } from '../../../test-data' import { DiffMethodLink } from './DiffItem' @@ -42,6 +42,7 @@ describe('DiffMethodLink', () => { }) test('it renders method and navigates on click', () => { + const pushSpy = jest.spyOn(history, 'push') renderWithTheme( @@ -49,8 +50,8 @@ describe('DiffMethodLink', () => { ) const link = screen.getByRole('link') expect(link).toHaveTextContent(`${method.name} for ${specKey}`) - expect(link).toHaveAttribute( - 'href', + fireEvent.click(link) + expect(pushSpy).toHaveBeenCalledWith( `/${specKey}/methods/${method.schema.tags[0]}/${method.name}` ) }) diff --git a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx index a9f2a1501..aac37734c 100644 --- a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx +++ b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx @@ -25,6 +25,7 @@ */ import type { FC } from 'react' import React, { useState, useEffect } from 'react' +import { useHistory } from 'react-router' import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer' import styled from 'styled-components' import { Accordion2, Box, Card, Grid, Heading, Link } from '@looker/components' @@ -59,12 +60,21 @@ export const DiffMethodLink: FC = ({ method, specKey, }) => { + const history = useHistory() + if (!method) return {`Missing in ${specKey}`} const tag = method.schema.tags[0] const path = `${buildMethodPath(specKey, tag, method.name)}` - return {`${method.name} for ${specKey}`} + return ( + { + history.push(path) + }} + >{`${method.name} for ${specKey}`} + ) } interface DiffItemProps { From 42c7879be09e21bb55ef0e990b2e9d521f5cea0e Mon Sep 17 00:00:00 2001 From: John Kaster Date: Sat, 12 Feb 2022 00:19:44 +0000 Subject: [PATCH 10/13] changes - fix package refs - including 3.1 spec in APIX --- examples/access-token-server/package.json | 4 +- examples/access-token-server/yarn.lock | 32 +++--- .../src/ExtensionApiExplorer.tsx | 6 +- .../sdk-codegen/src/specConverter.spec.ts | 97 +++++++++++++++++++ packages/sdk-codegen/src/specConverter.ts | 27 ++++-- 5 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 packages/sdk-codegen/src/specConverter.spec.ts diff --git a/examples/access-token-server/package.json b/examples/access-token-server/package.json index 6313e1649..05666a6b2 100644 --- a/examples/access-token-server/package.json +++ b/examples/access-token-server/package.json @@ -11,9 +11,9 @@ "create-status-json": "ts-node script/create_status_json.ts" }, "dependencies": { - "@looker/sdk": "^22.0.0", + "@looker/sdk": "^21.20.1", "@looker/sdk-rtl": "^21.0.20", - "@looker/sdk-node": "^22.0.0", + "@looker/sdk-node": "^21.20.1", "body-parser": "^1.19.0", "crypto-js": "^4.0.0", "dotenv": "^8.2.0", diff --git a/examples/access-token-server/yarn.lock b/examples/access-token-server/yarn.lock index 92739267b..1b2c6ddff 100644 --- a/examples/access-token-server/yarn.lock +++ b/examples/access-token-server/yarn.lock @@ -502,13 +502,13 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@looker/sdk-node@^21.14.0": - version "21.14.0" - resolved "https://registry.yarnpkg.com/@looker/sdk-node/-/sdk-node-21.14.0.tgz#2a7a6d8995f85832cef87165bc99e51041e8d75a" - integrity sha512-JoN7o/WdbMiz3IYRkNlcD0zuGGH4jv73ngz4IrljX7uAKDZmxRaZwQnPvFpN6CF5BDXRCLhfMaA0yCwBYV1xig== +"@looker/sdk-node@^22.0.0": + version "21.20.1" + resolved "https://registry.yarnpkg.com/@looker/sdk-node/-/sdk-node-21.20.1.tgz#4cef072397ad32ec8d0628bae36c461c8e091d3b" + integrity sha512-0xi2uSfGslB8KdXAaG2kVWv9suzgg5yiBOqzpKQaOeLRZY2rZAx8nn3FDNKzk15ZZAG/zwXNGmYblJny24AMLQ== dependencies: - "@looker/sdk" "^21.14.0" - "@looker/sdk-rtl" "^21.0.20" + "@looker/sdk" "^21.20.1" + "@looker/sdk-rtl" "^21.3.1" ini "^1.3.8" readable-stream "^3.4.0" request "^2.88.0" @@ -524,12 +524,22 @@ request "^2.88.0" request-promise-native "^1.0.8" -"@looker/sdk@^21.14.0": - version "21.14.0" - resolved "https://registry.yarnpkg.com/@looker/sdk/-/sdk-21.14.0.tgz#56d6da1eb1ea65e145e25291f09160cc283f9e4f" - integrity sha512-0/oh+95HYNG9VoqzKRim6z7bPic9ZgvlnDoBpQ1OZDLuufAWImZPiIwn2qBZESpCEgq1Sj3f2zdD+oqrLfCLIQ== +"@looker/sdk-rtl@^21.3.1": + version "21.3.1" + resolved "https://registry.yarnpkg.com/@looker/sdk-rtl/-/sdk-rtl-21.3.1.tgz#ff860249959ccf12a9f48d3c79267113e61724d1" + integrity sha512-i4SKNTtpNgNz7hLYMJ2W8xOrgQI81gw674q8pgDw1dFWuUA3yq4nTvuR9CFnStYCIoY/1ar2WcTkWp0YSlFzTw== dependencies: - "@looker/sdk-rtl" "^21.0.20" + ini "^1.3.8" + readable-stream "^3.4.0" + request "^2.88.0" + request-promise-native "^1.0.8" + +"@looker/sdk@^21.20.1", "@looker/sdk@^22.0.0": + version "21.20.1" + resolved "https://registry.yarnpkg.com/@looker/sdk/-/sdk-21.20.1.tgz#3d257309c2fe5ad1246e01837ea71aeeb7f4df22" + integrity sha512-t54yXAMeJAIeq7uUc1Oa3SdFsE3zvQaL9S28ebXy9TrljbnHG4FsVMacfq0XQGYPDzYUIoEySShWcBOsFZ+eyw== + dependencies: + "@looker/sdk-rtl" "^21.3.1" ini "^1.3.8" readable-stream "^3.4.0" request "^2.88.0" diff --git a/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx b/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx index 1526956d1..40534550b 100644 --- a/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx +++ b/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx @@ -68,11 +68,7 @@ export const ExtensionApiExplorer: FC = () => { return ( - + ) diff --git a/packages/sdk-codegen/src/specConverter.spec.ts b/packages/sdk-codegen/src/specConverter.spec.ts new file mode 100644 index 000000000..8d9c4818a --- /dev/null +++ b/packages/sdk-codegen/src/specConverter.spec.ts @@ -0,0 +1,97 @@ +/* + + MIT License + + Copyright (c) 2021 Looker Data Sciences, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ + +import type { IApiVersion, IApiVersionElement } from './specConverter' +import { getSpecsFromVersions, include31 } from './specConverter' + +const payload = `{ + "looker_release_version":"22.3.0", + "current_version":{"version":"4.0","full_version":"4.0.22.3","status":"current","swagger_url":"https://localhost:19999/api/4.0/swagger.json"}, + "supported_versions":[ + {"version":"2.99","full_version":"2.99.0","status":"internal_test","swagger_url":"https://localhost:19999/api/2.99/swagger.json"}, + {"version":"3.0","full_version":"3.0.0","status":"legacy","swagger_url":"https://localhost:19999/api/3.0/swagger.json"}, + {"version":"3.1","full_version":"3.1.0","status":"legacy","swagger_url":"https://localhost:19999/api/3.1/swagger.json"}, + {"version":"4.0","full_version":"4.0.22.3","status":"current","swagger_url":"https://localhost:19999/api/4.0/swagger.json"}, + {"version":"4.0","full_version":"4.0.22.3","status":"undocumented","swagger_url":"https://localhost:19999/api/4.0/undoc.json"} + ], + "api_server_url":"https://localhost:19999", + "web_server_url":"https://localhost:9999" +}` +describe('specConverter', () => { + describe('includeVersion', () => { + it('skips 3.0', () => { + const v: IApiVersionElement = { + status: 'legacy', + version: '3.0', + full_version: '3.0.22.4', + swagger_url: 'swagger', + } + expect(include31(v)).toEqual(false) + }) + it('skips 3.0.1', () => { + const v: IApiVersionElement = { + status: 'legacy', + version: '3.0.1', + full_version: '3.0.22.4', + swagger_url: 'swagger', + } + expect(include31(v)).toEqual(false) + }) + it('includes 3.1', () => { + const v: IApiVersionElement = { + status: 'legacy', + version: '3.1', + full_version: '3.1.22.4', + swagger_url: 'swagger', + } + expect(include31(v)).toEqual(true) + }) + it('includes 3.1.22', () => { + const v: IApiVersionElement = { + status: 'legacy', + version: '3.1.22', + full_version: '3.1.22.4', + swagger_url: 'swagger', + } + expect(include31(v)).toEqual(true) + }) + it('includes 4.0', () => { + const v: IApiVersionElement = { + status: 'stable', + version: '4.0', + full_version: '4.0.22.4', + swagger_url: 'swagger', + } + expect(include31(v)).toEqual(true) + }) + }) + + it('getSpecsFromVersions', async () => { + const versions: IApiVersion = JSON.parse(payload) + const specs = await getSpecsFromVersions(versions) + expect(Object.keys(specs)).toEqual(['3.1', '4.0', '4.0u']) + }) +}) diff --git a/packages/sdk-codegen/src/specConverter.ts b/packages/sdk-codegen/src/specConverter.ts index bf0e3ce1d..84d87ef03 100644 --- a/packages/sdk-codegen/src/specConverter.ts +++ b/packages/sdk-codegen/src/specConverter.ts @@ -129,15 +129,34 @@ export interface ISpecItem { * Callback for fetching and compiling specification to ApiModel */ export type SpecFetcher = (spec: SpecItem) => Promise +export type IncludeVersion = (version: IApiVersionElement) => boolean + +/** + * Should this specification version be included? + * @param ver to check + */ +export const include31 = (ver: IApiVersionElement) => { + return ( + (ver.status && + ver.version && + ver.swagger_url && + ver.status !== 'internal_test' && + ver.status !== 'deprecated' && + ver.status !== 'legacy') || + (ver.version || '') >= '3.1' // unfortunately, need to hard-code this for API Explorer's spec selector + ) +} /** * Return all public API specifications from an ApiVersion payload * @param versions payload from a Looker server * @param fetcher fetches and compiles spec to ApiModel + * @param include test for specification version inclusion */ export const getSpecsFromVersions = async ( versions: IApiVersion, - fetcher: SpecFetcher | undefined = undefined + fetcher: SpecFetcher | undefined = undefined, + include: IncludeVersion = include31 ): Promise => { const items = {} @@ -165,11 +184,7 @@ export const getSpecsFromVersions = async ( for (const v of versions.supported_versions) { // Tell TypeScript these are all defined because IApiVersion definition is lax if (v.status && v.version && v.swagger_url) { - if ( - v.status !== 'internal_test' && - v.status !== 'deprecated' && - v.status !== 'legacy' - ) { + if (include(v)) { const spec: SpecItem = { key: uniqueId(v), status: v.status, From fc8ab5bf2aea6afa828e058dfacb16bd4c9216a9 Mon Sep 17 00:00:00 2001 From: Bryn Ryans Date: Sat, 12 Feb 2022 02:17:00 +0000 Subject: [PATCH 11/13] fix: DiffScene back/forward browser button behaviour DiffScene did not handle changes in route when back or forward button pressed. This change has the react router driving the changes to state. If diff changes, history is updated. History updates state. When back/forward button pressed, history is update and again history updates state. --- .../src/scenes/DiffScene/DiffScene.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx b/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx index d5ea1b0ea..5cbe52e5e 100644 --- a/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx +++ b/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx @@ -104,6 +104,18 @@ export const DiffScene: FC = ({ specs, toggleNavigation }) => { ) const [toggles, setToggles] = useState(standardDiffToggles) + useEffect(() => { + if (r !== rightKey) { + setRightKey(r) + } + }, [r, rightKey]) + + useEffect(() => { + if (l !== leftKey) { + setLeftKey(l) + } + }, [l, leftKey]) + useEffect(() => { toggleNavigation(false) }, []) @@ -111,16 +123,14 @@ export const DiffScene: FC = ({ specs, toggleNavigation }) => { const [delta, setDelta] = useState([]) const handleLeftChange = (newLeft: string) => { - setLeftKey(newLeft) + history.push(`/${diffPath}/${newLeft}/${rightKey}`) } const handleRightChange = (newRight: string) => { - setRightKey(newRight) + history.push(`/${diffPath}/${leftKey}/${newRight}`) } const handleSwitch = () => { - const currLeftKey = leftKey - setLeftKey(rightKey) - setRightKey(currLeftKey) + history.push(`/${diffPath}/${rightKey}/${leftKey}`) } useEffect(() => { @@ -144,10 +154,6 @@ export const DiffScene: FC = ({ specs, toggleNavigation }) => { setToggles(newToggles) } - useEffect(() => { - history.push(`/${diffPath}/${leftKey}/${rightKey}`) - }, [leftKey, rightKey]) - return ( From 3fd376049d924a3cec27233e8f106358447de0a2 Mon Sep 17 00:00:00 2001 From: Bryn Ryans Date: Sat, 12 Feb 2022 18:08:42 +0000 Subject: [PATCH 12/13] fix: route from diff page Setting of current spec has been hoisted to ApiExplorer. ApiExplorer monitors the current route and sets the current spec if route is different from current spec. Added a check in the rendering to make sure that the current spec is set correctly before it allows the router component to render. Renders the loading component instead which I think is okay but may need to be revisited. ApiSpecSelector has been modifief to ONLY change the route. This means that if a component needs to switch spec all it needs to do is modify the route (diff component showing detail for example). --- packages/api-explorer/src/ApiExplorer.tsx | 18 ++++++++++-- .../ApiSpecSelector.spec.tsx | 28 ++++++++----------- .../SelectorContainer/ApiSpecSelector.tsx | 4 +-- .../src/scenes/DiffScene/DocDiff/DocDiff.tsx | 4 +-- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/api-explorer/src/ApiExplorer.tsx b/packages/api-explorer/src/ApiExplorer.tsx index 8b3d10aa3..3609ede98 100644 --- a/packages/api-explorer/src/ApiExplorer.tsx +++ b/packages/api-explorer/src/ApiExplorer.tsx @@ -68,7 +68,7 @@ import { selectSpecs, selectCurrentSpec, } from './state' -import { getSpecKey } from './utils' +import { getSpecKey, diffPath } from './utils' export interface ApiExplorerProps { adaptor: IApixAdaptor @@ -92,7 +92,7 @@ export const ApiExplorer: FC = ({ const spec = useSelector(selectCurrentSpec) const { initLodesAction } = useLodeActions() const { initSettingsAction } = useSettingActions() - const { initSpecsAction } = useSpecActions() + const { initSpecsAction, setCurrentSpecAction } = useSpecActions() const location = useLocation() const [hasNavigation, setHasNavigation] = useState(true) @@ -116,6 +116,13 @@ export const ApiExplorer: FC = ({ return () => unregisterEnvAdaptor() }, []) + useEffect(() => { + const maybeSpec = location.pathname?.split('/')[1] + if (spec && maybeSpec && maybeSpec !== diffPath && maybeSpec !== spec.key) { + setCurrentSpecAction({ currentSpecKey: maybeSpec }) + } + }, [location.pathname, spec]) + useEffect(() => { if (headless) { window.addEventListener('message', hasNavigationToggle) @@ -129,13 +136,18 @@ export const ApiExplorer: FC = ({ const themeOverrides = adaptor.themeOverrides() + let neededSpec = location.pathname?.split('/')[1] + if (neededSpec === diffPath) { + neededSpec = spec.key + } + return ( <> - {working ? ( + {working || neededSpec !== spec.key ? ( ) : ( diff --git a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx index b3086e35b..5fe012753 100644 --- a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.spec.tsx @@ -26,14 +26,12 @@ import React from 'react' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import * as reactRedux from 'react-redux' - +import { useHistory } from 'react-router-dom' import { getLoadedSpecs, specs } from '../../test-data' import { renderWithReduxProvider, renderWithRouterAndReduxProvider, } from '../../test-utils' -import { useSpecActions } from '../../state' import { ApiSpecSelector } from './ApiSpecSelector' jest.mock('react-router-dom', () => { @@ -43,15 +41,16 @@ jest.mock('react-router-dom', () => { useLocation: () => ({ pathname: '/4.0/methods/Dashboard/dashboard', }), + useHistory: jest.fn().mockReturnValue({ push: jest.fn() }), } }) -jest.mock('../../state/specs', () => ({ - ...(jest.requireActual('../../state/specs') as Record), - useSpecActions: jest - .fn() - .mockReturnValue({ setCurrentSpecAction: jest.fn() }), -})) +// jest.mock('../../state/specs', () => ({ +// ...(jest.requireActual('../../state/specs') as Record), +// useSpecActions: jest +// .fn() +// .mockReturnValue({ setCurrentSpecAction: jest.fn() }), +// })) describe('ApiSpecSelector', () => { Element.prototype.scrollIntoView = jest.fn() @@ -73,11 +72,8 @@ describe('ApiSpecSelector', () => { }) }) - test('fetches selected spec', async () => { - const mockDispatch = jest.fn() - jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch) - const { setCurrentSpecAction } = useSpecActions() - + test('requests selected spec', async () => { + const { push } = useHistory() renderWithRouterAndReduxProvider() userEvent.click(screen.getByRole('textbox')) await waitFor(() => { @@ -87,8 +83,6 @@ describe('ApiSpecSelector', () => { }) const button = screen.getByText('3.1') userEvent.click(button) - expect(setCurrentSpecAction).toHaveBeenCalledWith({ - currentSpecKey: '3.1', - }) + expect(push).toHaveBeenCalledWith('/3.1/methods/Dashboard/dashboard') }) }) diff --git a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx index 8818edab9..428e2feaf 100644 --- a/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx @@ -31,7 +31,7 @@ import { useHistory, useLocation } from 'react-router-dom' import type { SpecItem } from '@looker/sdk-codegen' import { useSelector } from 'react-redux' -import { selectSpecs, useSpecActions } from '../../state' +import { selectSpecs } from '../../state' interface ApiSpecSelectorProps { spec: SpecItem @@ -41,7 +41,6 @@ export const ApiSpecSelector: FC = ({ spec }) => { const history = useHistory() const location = useLocation() const specs = useSelector(selectSpecs) - const { setCurrentSpecAction } = useSpecActions() const options = Object.entries(specs).map(([key, spec]) => ({ value: key, label: key, @@ -49,7 +48,6 @@ export const ApiSpecSelector: FC = ({ spec }) => { })) const handleChange = (specKey: string) => { - setCurrentSpecAction({ currentSpecKey: specKey }) const matchPath = location.pathname.replace(`/${spec.key}`, `/${specKey}`) history.push(matchPath) } diff --git a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DocDiff.tsx b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DocDiff.tsx index 8d3bb8c76..2ef834a3d 100644 --- a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DocDiff.tsx +++ b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DocDiff.tsx @@ -80,9 +80,9 @@ export const DocDiff: FC = ({ {`${delta.length} differences between ${leftKey} and ${rightKey}`} - {pageItemData.map((item) => ( + {pageItemData.map((item, index) => ( Date: Sat, 12 Feb 2022 18:35:03 +0000 Subject: [PATCH 13/13] fix: initial load issue --- packages/api-explorer/src/ApiExplorer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api-explorer/src/ApiExplorer.tsx b/packages/api-explorer/src/ApiExplorer.tsx index 3609ede98..769296455 100644 --- a/packages/api-explorer/src/ApiExplorer.tsx +++ b/packages/api-explorer/src/ApiExplorer.tsx @@ -137,8 +137,8 @@ export const ApiExplorer: FC = ({ const themeOverrides = adaptor.themeOverrides() let neededSpec = location.pathname?.split('/')[1] - if (neededSpec === diffPath) { - neededSpec = spec.key + if (!neededSpec || neededSpec === diffPath) { + neededSpec = spec?.key } return ( @@ -147,7 +147,7 @@ export const ApiExplorer: FC = ({ loadGoogleFonts={themeOverrides.loadGoogleFonts} themeCustomizations={themeOverrides.themeCustomizations} > - {working || neededSpec !== spec.key ? ( + {working || !neededSpec || neededSpec !== spec.key ? ( ) : (