diff --git a/packages/api-explorer/src/ApiExplorer.tsx b/packages/api-explorer/src/ApiExplorer.tsx index dd46842c2..8f5e827a0 100644 --- a/packages/api-explorer/src/ApiExplorer.tsx +++ b/packages/api-explorer/src/ApiExplorer.tsx @@ -44,14 +44,8 @@ import { funFetch, fallbackFetch, OAuthScene } from '@looker/run-it' import { FirstPage } from '@styled-icons/material/FirstPage' import { LastPage } from '@styled-icons/material/LastPage' -import { LodeContext, defaultLodeContextValue } from './context' import type { IApixEnvAdaptor } from './utils' -import { - getLoded, - oAuthPath, - registerEnvAdaptor, - unregisterEnvAdaptor, -} from './utils' +import { oAuthPath, registerEnvAdaptor, unregisterEnvAdaptor } from './utils' import { Header, SideNav, @@ -63,13 +57,17 @@ import { import { specReducer, initDefaultSpecState, updateSpecApi } from './reducers' import { AppRouter } from './routes' import { apixFilesHost } from './utils/lodeUtils' -import { useActions, useSettingsStoreState } from './state' - +import { + useSettingActions, + useSettingStoreState, + useLodeActions, + useLodesStoreState, +} from './state' export interface ApiExplorerProps { specs: SpecList envAdaptor: IApixEnvAdaptor setVersionsUrl: RunItSetter - exampleLodeUrl?: string + examplesLodeUrl?: string declarationsLodeUrl?: string headless?: boolean } @@ -80,12 +78,14 @@ const ApiExplorer: FC = ({ specs, envAdaptor, setVersionsUrl, - exampleLodeUrl = 'https://raw.githubusercontent.com/looker-open-source/sdk-codegen/main/examplesIndex.json', + examplesLodeUrl = 'https://raw.githubusercontent.com/looker-open-source/sdk-codegen/main/examplesIndex.json', declarationsLodeUrl = `${apixFilesHost}/declarationsIndex.json`, headless = false, }) => { - const { initialized } = useSettingsStoreState() - const { initAction } = useActions() + const { initialized } = useSettingStoreState() + useLodesStoreState() + const { initLodesAction } = useLodeActions() + const { initSettingsAction } = useSettingActions() const location = useLocation() const oauthReturn = location.pathname === `/${oAuthPath}` const [specState, specDispatch] = useReducer( @@ -94,8 +94,6 @@ const ApiExplorer: FC = ({ ) const { spec } = specState - const [lode, setLode] = useState(defaultLodeContextValue) - const [hasNavigation, setHasNavigation] = useState(true) const toggleNavigation = (target?: boolean) => setHasNavigation(target || !hasNavigation) @@ -108,7 +106,8 @@ const ApiExplorer: FC = ({ useEffect(() => { registerEnvAdaptor(envAdaptor) - initAction() + initSettingsAction() + initLodesAction({ examplesLodeUrl, declarationsLodeUrl }) return () => unregisterEnvAdaptor() }, []) @@ -144,10 +143,6 @@ const ApiExplorer: FC = ({ } }, [spec, location]) - useEffect(() => { - getLoded(exampleLodeUrl, declarationsLodeUrl).then((resp) => setLode(resp)) - }, [exampleLodeUrl, declarationsLodeUrl]) - const themeOverrides = envAdaptor.themeOverrides() return ( @@ -160,87 +155,85 @@ const ApiExplorer: FC = ({ ) : ( - - - {!headless && ( -
- )} - - - {headless && ( - <> - - {hasNavigation && ( - - API DOCUMENTATION - - )} - : } - label={HEADER_TOGGLE_LABEL} - onClick={() => toggleNavigation()} - /> - + + {!headless && ( +
+ )} + + + {headless && ( + <> + {hasNavigation && ( - <> - - - + + API DOCUMENTATION + )} - - )} - {hasNavigation && ( - - )} - - {oauthReturn && } - {!oauthReturn && spec.api && ( - : } + label={HEADER_TOGGLE_LABEL} + onClick={() => toggleNavigation()} + /> + + {hasNavigation && ( + <> + + + + )} + + )} + {hasNavigation && ( + )} - - - + + {oauthReturn && } + {!oauthReturn && spec.api && ( + + )} + + )} diff --git a/packages/api-explorer/src/components/DocSdkUsage/DocSdkUsage.spec.tsx b/packages/api-explorer/src/components/DocSdkUsage/DocSdkUsage.spec.tsx index 16d3c11c3..ebc828b88 100644 --- a/packages/api-explorer/src/components/DocSdkUsage/DocSdkUsage.spec.tsx +++ b/packages/api-explorer/src/components/DocSdkUsage/DocSdkUsage.spec.tsx @@ -27,7 +27,7 @@ import React from 'react' import { screen } from '@testing-library/react' import { findExampleLanguages } from '@looker/sdk-codegen' -import { renderWithReduxProviderAndLode } from '../../test-utils' +import { renderWithLode } from '../../test-utils' import { api, examples } from '../../test-data' import { DocSdkUsage } from './DocSdkUsage' import { @@ -47,7 +47,7 @@ describe('DocSdkUsage', () => { method.operationId, 1 ) - renderWithReduxProviderAndLode(, examples) + renderWithLode(, examples) expect(screen.getAllByRole('link')).toHaveLength(PER_PAGE_COUNT) expect( diff --git a/packages/api-explorer/src/components/DocSdkUsage/DocSdkUsage.tsx b/packages/api-explorer/src/components/DocSdkUsage/DocSdkUsage.tsx index 781de20e4..1ad1fd199 100644 --- a/packages/api-explorer/src/components/DocSdkUsage/DocSdkUsage.tsx +++ b/packages/api-explorer/src/components/DocSdkUsage/DocSdkUsage.tsx @@ -24,7 +24,7 @@ */ import type { FC } from 'react' -import React, { useContext, useState, useEffect } from 'react' +import React, { useState, useEffect } from 'react' import { Box, Card, @@ -41,8 +41,7 @@ import { CollapserCard } from '@looker/run-it' import { InsertDriveFile } from '@styled-icons/material-outlined/InsertDriveFile' import { useSelector } from 'react-redux' -import { selectSdkLanguage } from '../../state' -import { LodeContext } from '../../context' +import { selectSdkLanguage, selectExamplesLode } from '../../state' import { exampleColumns, EMPTY_STRING, @@ -61,15 +60,16 @@ interface DocSdkUsageProps { * links to the source files */ export const DocSdkUsage: FC = ({ method }) => { - const { examples } = useContext(LodeContext) + const examples = useSelector(selectExamplesLode) const sdkLanguage = useSelector(selectSdkLanguage) - let languages = findExampleLanguages(examples, method.name) const [page, setPage] = useState(1) useEffect(() => { setPage(1) }, [method]) + if (!examples) return <> + let languages = findExampleLanguages(examples, method.name) if (languages.length === 0) return <> languages = sortLanguagesByPreference(languages, sdkLanguage) diff --git a/packages/api-explorer/src/components/DocSource/DocSource.tsx b/packages/api-explorer/src/components/DocSource/DocSource.tsx index 64a607c54..562b97019 100644 --- a/packages/api-explorer/src/components/DocSource/DocSource.tsx +++ b/packages/api-explorer/src/components/DocSource/DocSource.tsx @@ -24,13 +24,14 @@ */ import type { FC } from 'react' -import React, { useContext } from 'react' +import React from 'react' import type { IMethod, IType } from '@looker/sdk-codegen' import { findDeclaration } from '@looker/sdk-codegen' import { Icon, Link, Tooltip } from '@looker/components' import { IdeFileDocument } from '@looker/icons' +import { useSelector } from 'react-redux' -import { LodeContext } from '../../context' +import { selectDeclarationsLode } from '../../state' interface DocSourceProps { method?: IMethod @@ -38,7 +39,7 @@ interface DocSourceProps { } export const DocSource: FC = ({ method, type }) => { - const { declarations } = useContext(LodeContext) + const declarations = useSelector(selectDeclarationsLode) let sourceLink let declaration if (declarations) { diff --git a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx index 051045181..1d494ea88 100644 --- a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx @@ -29,7 +29,7 @@ import userEvent from '@testing-library/user-event' import { codeGenerators } from '@looker/sdk-codegen' import * as reactRedux from 'react-redux' -import { defaultSettingsState, slice as settingsSlice } from '../../state' +import { defaultSettingsState, settingsSlice } from '../../state' import { registerTestEnvAdaptor, renderWithReduxProvider, diff --git a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx index 136f006cb..ba42bb350 100644 --- a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx @@ -30,13 +30,13 @@ import { Select } from '@looker/components' import { useSelector } from 'react-redux' import type { SelectOptionProps } from '@looker/components' -import { useActions, selectSdkLanguage } from '../../state' +import { useSettingActions, selectSdkLanguage } from '../../state' /** * Allows the user to select their preferred SDK language */ export const SdkLanguageSelector: FC = () => { - const { setSdkLanguageAction } = useActions() + const { setSdkLanguageAction } = useSettingActions() const selectedSdkLanguage = useSelector(selectSdkLanguage) const allSdkLanguages: SelectOptionProps[] = codeGenerators.map((gen) => ({ diff --git a/packages/api-explorer/src/components/SideNav/SideNav.tsx b/packages/api-explorer/src/components/SideNav/SideNav.tsx index d60b7165c..a9781b8c5 100644 --- a/packages/api-explorer/src/components/SideNav/SideNav.tsx +++ b/packages/api-explorer/src/components/SideNav/SideNav.tsx @@ -49,7 +49,7 @@ import { useSelector } from 'react-redux' import type { SpecAction } from '../../reducers' import { useWindowSize } from '../../utils' import { HEADER_REM } from '../Header' -import { selectSearchCriteria, useActions } from '../../state' +import { selectSearchCriteria, useSettingActions } from '../../state' import { SideNavMethodTags } from './SideNavMethodTags' import { SideNavTypeTags } from './SideNavTypeTags' import { useDebounce, countMethods, countTypes } from './searchUtils' @@ -100,7 +100,7 @@ export const SideNav: FC = ({ headless = false, spec }) => { } const tabs = useTabs({ defaultIndex, onChange: onTabChange }) const searchCriteria = useSelector(selectSearchCriteria) - const { setSearchPatternAction } = useActions() + const { setSearchPatternAction } = useSettingActions() const [pattern, setSearchPattern] = useState('') const debouncedPattern = useDebounce(pattern, 250) diff --git a/packages/api-explorer/src/state/index.ts b/packages/api-explorer/src/state/index.ts index 584e4bcfd..dcb854d53 100644 --- a/packages/api-explorer/src/state/index.ts +++ b/packages/api-explorer/src/state/index.ts @@ -25,3 +25,4 @@ */ export * from './store' export * from './settings' +export * from './lodes' diff --git a/packages/api-explorer/src/context/index.ts b/packages/api-explorer/src/state/lodes/index.ts similarity index 94% rename from packages/api-explorer/src/context/index.ts rename to packages/api-explorer/src/state/lodes/index.ts index 66b0f791d..a06152c6f 100644 --- a/packages/api-explorer/src/context/index.ts +++ b/packages/api-explorer/src/state/lodes/index.ts @@ -23,4 +23,5 @@ SOFTWARE. */ -export { LodeContext, defaultLodeContextValue } from './lode' +export * from './selectors' +export * from './slice' diff --git a/packages/api-explorer/src/state/lodes/sagas.spec.ts b/packages/api-explorer/src/state/lodes/sagas.spec.ts new file mode 100644 index 000000000..071586686 --- /dev/null +++ b/packages/api-explorer/src/state/lodes/sagas.spec.ts @@ -0,0 +1,92 @@ +/* + + 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 { examples, declarations } from '../../test-data' +import * as lodeUtils from '../../utils/lodeUtils' +import * as sagas from './sagas' +import { lodeActions, lodesSlice } from './slice' + +describe('Lode sagas', () => { + let sagaTester: ReduxSagaTester + + beforeEach(() => { + jest.resetAllMocks() + sagaTester = new ReduxSagaTester({ + initialState: { lodes: { examples, declarations } }, + reducers: { + lodes: lodesSlice.reducer, + }, + }) + sagaTester.start(sagas.saga) + }) + + describe('initSaga', () => { + const { initLodesAction, initLodesSuccessAction, initLodesFailureAction } = + lodeActions + const examplesLodeUrl = 'https://foo.com/examples.json' + const declarationsLodeUrl = 'https://foo.com/declarations.json' + + test('sends initLodesSuccessAction with examples and declarations on success', async () => { + jest + .spyOn(lodeUtils, 'getLoded') + .mockResolvedValueOnce({ examples, declarations }) + + sagaTester.dispatch( + initLodesAction({ examplesLodeUrl, declarationsLodeUrl }) + ) + + await sagaTester.waitFor('lodes/initLodesSuccessAction') + const calledActions = sagaTester.getCalledActions() + expect(calledActions).toHaveLength(2) + expect(calledActions[0]).toEqual( + initLodesAction({ examplesLodeUrl, declarationsLodeUrl }) + ) + expect(calledActions[1]).toEqual( + initLodesSuccessAction({ + examples, + declarations, + }) + ) + }) + + test('sends initLodesFailureAction on error', async () => { + const error = new Error('boom') + jest.spyOn(lodeUtils, 'getLoded').mockRejectedValueOnce(error) + + sagaTester.dispatch( + initLodesAction({ examplesLodeUrl, declarationsLodeUrl }) + ) + + await sagaTester.waitFor('lodes/initLodesFailureAction') + const calledActions = sagaTester.getCalledActions() + expect(calledActions).toHaveLength(2) + expect(calledActions[0]).toEqual( + initLodesAction({ examplesLodeUrl, declarationsLodeUrl }) + ) + expect(calledActions[1]).toEqual(initLodesFailureAction(error)) + }) + }) +}) diff --git a/packages/api-explorer/src/state/lodes/sagas.ts b/packages/api-explorer/src/state/lodes/sagas.ts new file mode 100644 index 000000000..1dc37a908 --- /dev/null +++ b/packages/api-explorer/src/state/lodes/sagas.ts @@ -0,0 +1,51 @@ +/* + + 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 { call, put, takeEvery } from 'typed-redux-saga' +import type { PayloadAction } from '@reduxjs/toolkit' + +import { getLoded } from '../../utils' +import type { InitPayload } from './slice' +import { lodeActions } from './slice' + +function* initSaga(action: PayloadAction) { + const { initLodesSuccessAction, initLodesFailureAction } = lodeActions + try { + const lode = yield* call( + getLoded, + action.payload.examplesLodeUrl, + action.payload.declarationsLodeUrl + ) + yield* put(initLodesSuccessAction(lode)) + } catch (error: any) { + yield* put(initLodesFailureAction(error)) + } +} + +export function* saga() { + const { initLodesAction } = lodeActions + + yield* takeEvery(initLodesAction, initSaga) +} diff --git a/packages/api-explorer/src/state/lodes/selectors.spec.ts b/packages/api-explorer/src/state/lodes/selectors.spec.ts new file mode 100644 index 000000000..b69d1f23e --- /dev/null +++ b/packages/api-explorer/src/state/lodes/selectors.spec.ts @@ -0,0 +1,42 @@ +/* + + 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 { createTestStore } from '../../test-utils' +import { declarations, examples } from '../../test-data' +import { selectDeclarationsLode, selectExamplesLode } from './selectors' + +const testStore = createTestStore({ lodes: { examples, declarations } }) + +describe('Lode selectors', () => { + const state = testStore.getState() + + test('selectExamplesLode selects', () => { + expect(selectExamplesLode(state)).toEqual(examples) + }) + + test('selectDeclarationsLode selects', () => { + expect(selectDeclarationsLode(state)).toEqual(declarations) + }) +}) diff --git a/packages/api-explorer/src/context/lode/index.ts b/packages/api-explorer/src/state/lodes/selectors.ts similarity index 79% rename from packages/api-explorer/src/context/lode/index.ts rename to packages/api-explorer/src/state/lodes/selectors.ts index 596362f9a..5784b182f 100644 --- a/packages/api-explorer/src/context/lode/index.ts +++ b/packages/api-explorer/src/state/lodes/selectors.ts @@ -23,4 +23,12 @@ SOFTWARE. */ -export { defaultLodeContextValue, LodeContext } from './LodeContext' +import type { RootState } from '../store' + +const selectLodeState = (state: RootState) => state.lodes + +export const selectExamplesLode = (state: RootState) => + selectLodeState(state).examples + +export const selectDeclarationsLode = (state: RootState) => + selectLodeState(state).declarations diff --git a/packages/api-explorer/src/state/lodes/slice.ts b/packages/api-explorer/src/state/lodes/slice.ts new file mode 100644 index 000000000..a5b43fae8 --- /dev/null +++ b/packages/api-explorer/src/state/lodes/slice.ts @@ -0,0 +1,69 @@ +/* + + 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 { createSliceHooks } from '@looker/redux' +import type { IDeclarationMine, IExampleMine } from '@looker/sdk-codegen' +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' + +import { saga } from './sagas' + +export interface LodesState { + examples?: IExampleMine + declarations?: IDeclarationMine + error?: Error +} + +export const defaultLodesState: LodesState = { + examples: undefined, + declarations: undefined, +} + +export interface InitPayload { + examplesLodeUrl?: string + declarationsLodeUrl?: string +} + +type InitSuccessAction = LodesState + +export const lodesSlice = createSlice({ + name: 'lodes', + initialState: defaultLodesState, + reducers: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + initLodesAction(_state, _action: PayloadAction) {}, + initLodesSuccessAction(state, action: PayloadAction) { + state.examples = action.payload.examples + state.declarations = action.payload.declarations + }, + initLodesFailureAction(state, action: PayloadAction) { + state.error = action.payload + }, + }, +}) + +export const lodeActions = lodesSlice.actions +export const { useActions: useLodeActions, useStoreState: useLodesStoreState } = + createSliceHooks(lodesSlice, saga) diff --git a/packages/api-explorer/src/state/settings/sagas.spec.ts b/packages/api-explorer/src/state/settings/sagas.spec.ts index 75c1d413a..10b6df3f2 100644 --- a/packages/api-explorer/src/state/settings/sagas.spec.ts +++ b/packages/api-explorer/src/state/settings/sagas.spec.ts @@ -28,7 +28,7 @@ import { registerTestEnvAdaptor } from '../../test-utils' import { EnvAdaptorConstants, getEnvAdaptor } from '../../utils' import * as sagas from './sagas' -import { actions, defaultSettings, slice } from './slice' +import { settingActions, defaultSettings, settingsSlice } from './slice' describe('Settings Sagas', () => { let sagaTester: ReduxSagaTester @@ -39,7 +39,7 @@ describe('Settings Sagas', () => { sagaTester = new ReduxSagaTester({ initialState: { settings: { sdkLanguage: 'Go' } }, reducers: { - settings: slice.reducer, + settings: settingsSlice.reducer, }, }) localStorage.clear() @@ -47,7 +47,7 @@ describe('Settings Sagas', () => { }) describe('setSdkLanguageSaga', () => { - const setSdkLanguageAction = actions.setSdkLanguageAction + const setSdkLanguageAction = settingActions.setSdkLanguageAction test('persists value sdkLanguage in localstorage', async () => { sagaTester.dispatch(setSdkLanguageAction({ sdkLanguage: 'Kotlin' })) @@ -67,51 +67,55 @@ describe('Settings Sagas', () => { }) describe('initSaga', () => { - const { initAction, initSuccessAction, initFailureAction } = actions + const { + initSettingsAction, + initSettingsSuccessAction, + initSettingsFailureAction, + } = settingActions test('sends initSuccess action with defaults on success if no persisted settings are found', async () => { - sagaTester.dispatch(initAction()) - await sagaTester.waitFor('settings/initSuccessAction') + sagaTester.dispatch(initSettingsAction()) + await sagaTester.waitFor('settings/initSettingsSuccessAction') const calledActions = sagaTester.getCalledActions() expect(calledActions).toHaveLength(2) - expect(calledActions[0]).toEqual(initAction()) + expect(calledActions[0]).toEqual(initSettingsAction()) expect(calledActions[1]).toEqual( - initSuccessAction({ + initSettingsSuccessAction({ ...defaultSettings, }) ) }) - test('sends initSuccess action with persisted language on success', async () => { + test('sends initSettingsSuccessAction with persisted language on success', async () => { jest .spyOn(getEnvAdaptor(), 'localStorageGetItem') .mockResolvedValueOnce(JSON.stringify({ sdkLanguage: 'Go' })) - sagaTester.dispatch(initAction()) - await sagaTester.waitFor('settings/initSuccessAction') + sagaTester.dispatch(initSettingsAction()) + await sagaTester.waitFor('settings/initSettingsSuccessAction') const calledActions = sagaTester.getCalledActions() expect(calledActions).toHaveLength(2) - expect(calledActions[0]).toEqual(initAction()) + expect(calledActions[0]).toEqual(initSettingsAction()) expect(calledActions[1]).toEqual( - initSuccessAction({ + initSettingsSuccessAction({ ...defaultSettings, sdkLanguage: 'Go', }) ) }) - test('sends initFailure action on error', async () => { + test('sends initSettingsFailureAction on error', async () => { const error = new Error('boom') jest .spyOn(getEnvAdaptor(), 'localStorageGetItem') .mockRejectedValueOnce(error) - sagaTester.dispatch(initAction()) - await sagaTester.waitFor('settings/initFailureAction') + sagaTester.dispatch(initSettingsAction()) + await sagaTester.waitFor('settings/initSettingsFailureAction') const calledActions = sagaTester.getCalledActions() expect(calledActions).toHaveLength(2) - expect(calledActions[0]).toEqual(initAction()) - expect(calledActions[1]).toEqual(initFailureAction(error)) + expect(calledActions[0]).toEqual(initSettingsAction()) + expect(calledActions[1]).toEqual(initSettingsFailureAction(error)) }) }) }) diff --git a/packages/api-explorer/src/state/settings/sagas.ts b/packages/api-explorer/src/state/settings/sagas.ts index bcf470320..9777f4a3d 100644 --- a/packages/api-explorer/src/state/settings/sagas.ts +++ b/packages/api-explorer/src/state/settings/sagas.ts @@ -27,7 +27,7 @@ import { takeEvery, call, put, select } from 'typed-redux-saga' import { EnvAdaptorConstants, getEnvAdaptor } from '../../utils' import type { RootState } from '../store' -import { actions, defaultSettings } from './slice' +import { settingActions, defaultSettings } from './slice' /** * Serializes state to local storage @@ -62,18 +62,19 @@ function* deserializeLocalStorage() { * Initializes the store with default settings and existing persisted settings */ function* initSaga() { - const { initSuccessAction, initFailureAction } = actions + const { initSettingsSuccessAction, initSettingsFailureAction } = + settingActions try { const settings = yield* call(deserializeLocalStorage) - yield* put(initSuccessAction(settings)) + yield* put(initSettingsSuccessAction(settings)) } catch (error: any) { - yield* put(initFailureAction(error)) + yield* put(initSettingsFailureAction(error)) } } export function* saga() { - const { initAction, setSdkLanguageAction } = actions + const { initSettingsAction, setSdkLanguageAction } = settingActions - yield* takeEvery(initAction, initSaga) + yield* takeEvery(initSettingsAction, initSaga) yield* takeEvery(setSdkLanguageAction, serializeToLocalStorageSaga) } diff --git a/packages/api-explorer/src/state/settings/selectors.spec.ts b/packages/api-explorer/src/state/settings/selectors.spec.ts index fd7c6f5bd..38c81a636 100644 --- a/packages/api-explorer/src/state/settings/selectors.spec.ts +++ b/packages/api-explorer/src/state/settings/selectors.spec.ts @@ -23,13 +23,9 @@ SOFTWARE. */ -import { createTestStore } from '../../test-utils' +import { createTestStore, preloadedState } from '../../test-utils' import { selectSdkLanguage, isInitialized } from './selectors' -const preloadedState = { - settings: { initialized: false, sdkLanguage: 'Python' }, -} - const testStore = createTestStore() describe('Settings selectors', () => { diff --git a/packages/api-explorer/src/state/settings/selectors.ts b/packages/api-explorer/src/state/settings/selectors.ts index 096ca1cd5..7ba09d3d7 100644 --- a/packages/api-explorer/src/state/settings/selectors.ts +++ b/packages/api-explorer/src/state/settings/selectors.ts @@ -25,16 +25,16 @@ */ import type { RootState } from '../store' -const getSettingsState = (state: RootState) => state.settings +const selectSettingsState = (state: RootState) => state.settings export const selectSdkLanguage = (state: RootState) => - getSettingsState(state).sdkLanguage + selectSettingsState(state).sdkLanguage export const selectSearchPattern = (state: RootState) => - getSettingsState(state).searchPattern + selectSettingsState(state).searchPattern export const selectSearchCriteria = (state: RootState) => - getSettingsState(state).searchCriteria + selectSettingsState(state).searchCriteria export const isInitialized = (state: RootState) => - getSettingsState(state).initialized + selectSettingsState(state).initialized diff --git a/packages/api-explorer/src/state/settings/slice.ts b/packages/api-explorer/src/state/settings/slice.ts index 61a095444..c6373d9d0 100644 --- a/packages/api-explorer/src/state/settings/slice.ts +++ b/packages/api-explorer/src/state/settings/slice.ts @@ -58,17 +58,20 @@ type SetSdkLanguageAction = Pick export type InitSuccessPayload = UserDefinedSettings -export const slice = createSlice({ +export const settingsSlice = createSlice({ name: 'settings', initialState: defaultSettingsState, reducers: { // eslint-disable-next-line @typescript-eslint/no-empty-function - initAction() {}, - initSuccessAction(state, action: PayloadAction) { + initSettingsAction() {}, + initSettingsSuccessAction( + state, + action: PayloadAction + ) { state.sdkLanguage = action.payload.sdkLanguage state.initialized = true }, - initFailureAction(state, action: PayloadAction) { + initSettingsFailureAction(state, action: PayloadAction) { state.error = action.payload state.initialized = false }, @@ -84,6 +87,8 @@ export const slice = createSlice({ }, }) -export const actions = slice.actions -export const { useActions, useStoreState: useSettingsStoreState } = - createSliceHooks(slice, saga) +export const settingActions = settingsSlice.actions +export const { + useActions: useSettingActions, + useStoreState: useSettingStoreState, +} = createSliceHooks(settingsSlice, saga) diff --git a/packages/api-explorer/src/state/store.ts b/packages/api-explorer/src/state/store.ts index f930bebef..ed0b437a1 100644 --- a/packages/api-explorer/src/state/store.ts +++ b/packages/api-explorer/src/state/store.ts @@ -26,17 +26,22 @@ import { createStore } from '@looker/redux' import type { SettingState } from './settings' -import { defaultSettingsState, slice } from './settings' +import { defaultSettingsState, settingsSlice } from './settings' +import type { LodesState } from './lodes' +import { lodesSlice, defaultLodesState } from './lodes' export const store = createStore({ preloadedState: { settings: defaultSettingsState, + lodes: defaultLodesState, }, reducer: { - settings: slice.reducer, + settings: settingsSlice.reducer, + lodes: lodesSlice.reducer, }, }) export interface RootState { settings: SettingState + lodes: LodesState } diff --git a/packages/api-explorer/src/context/lode/LodeContext.ts b/packages/api-explorer/src/test-data/declarations.ts similarity index 69% rename from packages/api-explorer/src/context/lode/LodeContext.ts rename to packages/api-explorer/src/test-data/declarations.ts index e21429469..55c4511c3 100644 --- a/packages/api-explorer/src/context/lode/LodeContext.ts +++ b/packages/api-explorer/src/test-data/declarations.ts @@ -23,18 +23,21 @@ SOFTWARE. */ -import { createContext } from 'react' -import type { IDeclarationMine, IExampleMine } from '@looker/sdk-codegen' - -interface LodeContextProps { - examples: IExampleMine - declarations?: IDeclarationMine -} - -export const defaultLodeContextValue: LodeContextProps = { - examples: { commitHash: '', remoteOrigin: '', nuggets: {}, summaries: {} }, +import type { IDeclarationMine } from '@looker/sdk-codegen' + +export const declarations: IDeclarationMine = { + commitHash: '1e9348b797c2f3760d03c1f94c60f18e534e8298', + remoteOrigin: 'https://github.com/looker-open-source/sdk-codegen', + types: { + FullLode: { + line: 40, + sourceFile: 'packages/api-explorer/src/utils/lodeUtils.ts', + }, + }, + methods: { + getLoded: { + line: 45, + sourceFile: 'packages/api-explorer/src/utils/lodeUtils.ts', + }, + }, } - -export const LodeContext = createContext( - defaultLodeContextValue -) diff --git a/packages/api-explorer/src/test-data/index.ts b/packages/api-explorer/src/test-data/index.ts index bb4c8c744..0fdf9e13a 100644 --- a/packages/api-explorer/src/test-data/index.ts +++ b/packages/api-explorer/src/test-data/index.ts @@ -32,3 +32,4 @@ export { getLoadedSpecState, } from './specs' export { examples } from './examples' +export * from './declarations' diff --git a/packages/api-explorer/src/test-utils/index.ts b/packages/api-explorer/src/test-utils/index.ts index e487dbfac..870a334ed 100644 --- a/packages/api-explorer/src/test-utils/index.ts +++ b/packages/api-explorer/src/test-utils/index.ts @@ -23,10 +23,7 @@ SOFTWARE. */ -export { - renderWithLode, - renderWithReduxProviderAndLode, -} from './render_with_lode' +export * from './lodes' export * from './router' export * from './redux' export { registerTestEnvAdaptor } from './envAdaptor' diff --git a/packages/api-explorer/src/test-utils/render_with_lode.tsx b/packages/api-explorer/src/test-utils/lodes.tsx similarity index 63% rename from packages/api-explorer/src/test-utils/render_with_lode.tsx rename to packages/api-explorer/src/test-utils/lodes.tsx index 440364352..9568a12fd 100644 --- a/packages/api-explorer/src/test-utils/render_with_lode.tsx +++ b/packages/api-explorer/src/test-utils/lodes.tsx @@ -23,47 +23,19 @@ SOFTWARE. */ - +import type { IDeclarationMine, IExampleMine } from '@looker/sdk-codegen' import type { ReactElement } from 'react' -import React from 'react' import type { RenderOptions } from '@testing-library/react' -import type { Store } from 'redux' - import { renderWithTheme } from '@looker/components-test-utils' -import type { IDeclarationMine, IExampleMine } from '@looker/sdk-codegen' -import { LodeContext } from '../context' -import type { RootState } from '../state' -import { withReduxProvider } from './redux' -const withLode = ( - consumer: ReactElement, - examples: IExampleMine, - declarations?: IDeclarationMine -) => { - return ( - - {consumer} - - ) -} +import { createTestStore, withReduxProvider } from './redux' export const renderWithLode = ( - component: ReactElement, + consumers: ReactElement, examples: IExampleMine, declarations?: IDeclarationMine, options?: Omit ) => { - return renderWithTheme(withLode(component, examples, declarations), options) + const store = createTestStore({ lodes: { examples, declarations } }) + return renderWithTheme(withReduxProvider(consumers, store), options) } - -export const renderWithReduxProviderAndLode = ( - component: ReactElement, - examples: IExampleMine, - declarations?: IDeclarationMine, - store?: Store, - options?: Omit -) => - renderWithTheme( - withReduxProvider(withLode(component, examples, declarations), store), - options - ) diff --git a/packages/api-explorer/src/test-utils/redux.tsx b/packages/api-explorer/src/test-utils/redux.tsx index 6b8eeac87..e85729b66 100644 --- a/packages/api-explorer/src/test-utils/redux.tsx +++ b/packages/api-explorer/src/test-utils/redux.tsx @@ -31,9 +31,14 @@ import { renderWithTheme } from '@looker/components-test-utils' import type { RenderOptions } from '@testing-library/react' import { createStore } from '@looker/redux' -import type { RootState, SettingState } from '../state' -import { defaultSettingsState, store as defaultStore } from '../state' -import { slice as settingsSlice } from '../state/settings' +import type { LodesState, RootState, SettingState } from '../state' +import { + settingsSlice, + defaultLodesState, + defaultSettingsState, + store as defaultStore, + lodesSlice, +} from '../state' import { registerEnvAdaptor, StandaloneEnvAdaptor } from '../utils' import { renderWithRouter } from './router' @@ -59,8 +64,9 @@ export const renderWithRouterAndReduxProvider = ( ) => renderWithRouter(withReduxProvider(consumers, store), initialEntries, options) -const preloadedState: RootState = { +export const preloadedState: RootState = { settings: defaultSettingsState, + lodes: defaultLodesState, } type DeepPartial = { @@ -74,6 +80,10 @@ export const createTestStore = (overrides?: DeepPartial) => ...preloadedState.settings, ...overrides?.settings, } as SettingState, + lodes: { + ...defaultLodesState, + ...overrides?.lodes, + } as LodesState, }, - reducer: { settings: settingsSlice.reducer }, + reducer: { settings: settingsSlice.reducer, lodes: lodesSlice.reducer }, }) diff --git a/packages/api-explorer/src/utils/lodeUtils.ts b/packages/api-explorer/src/utils/lodeUtils.ts index 3e3ac90e3..4f8e0f703 100644 --- a/packages/api-explorer/src/utils/lodeUtils.ts +++ b/packages/api-explorer/src/utils/lodeUtils.ts @@ -38,17 +38,17 @@ const fetchLode = async (lodeUrl: string) => { } interface FullLode { - examples: IExampleMine + examples?: IExampleMine declarations?: IDeclarationMine } export const getLoded = async ( - examplesLodeUrl: string, + examplesLodeUrl?: string, declarationsLodeUrl?: string ): Promise => { // First try to load from the apix-files server let examples = await fetchLode(`${apixFilesHost}/examplesIndex.json`) - if (!examples) { + if (!examples && examplesLodeUrl) { examples = await fetchLode(examplesLodeUrl) } @@ -57,9 +57,7 @@ export const getLoded = async ( declarations = await fetchLode(declarationsLodeUrl) } - const lode: FullLode = { - examples: { commitHash: '', nuggets: {}, remoteOrigin: '', summaries: {} }, - } + const lode: FullLode = { examples: undefined, declarations: undefined } if (examples) { lode.examples = JSON.parse(examples) }