diff --git a/.github/workflows/apix-ci.yml b/.github/workflows/apix-ci.yml index 174a614a9..c624dcaa4 100644 --- a/.github/workflows/apix-ci.yml +++ b/.github/workflows/apix-ci.yml @@ -6,6 +6,7 @@ on: - packages/run-it/** - packages/api-explorer/** - packages/extension-api-explorer/** + - packages/extension-utils/** push: branches: @@ -15,6 +16,7 @@ on: - packages/run-it/** - packages/api-explorer/** - packages/extension-api-explorer/** + - packages/extension-utils/** workflow_dispatch: diff --git a/.github/workflows/tssdk-ci.yml b/.github/workflows/tssdk-ci.yml index 47cb0a5bd..b71912508 100644 --- a/.github/workflows/tssdk-ci.yml +++ b/.github/workflows/tssdk-ci.yml @@ -7,6 +7,7 @@ on: - packages/sdk-node/** - packages/extension-sdk/** - packages/extension-sdk-react/** + - packages/extension-utils/** push: branches: @@ -17,6 +18,7 @@ on: - packages/sdk-node/** - packages/extension-sdk/** - packages/extension-sdk-react/** + - packages/extension-utils/** workflow_dispatch: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1ca5a8cdf..5423c6898 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -5,6 +5,7 @@ "packages/extension-api-explorer": "21.18.1", "packages/extension-sdk": "21.18.1", "packages/extension-sdk-react": "21.18.1", + "packages/extension-utils": "0.1.0", "packages/hackathon": "21.18.1", "packages/run-it": "0.9.22", "packages/sdk": "21.18.1", diff --git a/examples/python/cloud-function-user-provision/README.md b/examples/python/cloud-function-user-provision/README.md index 56ede8da8..e42a65c30 100644 --- a/examples/python/cloud-function-user-provision/README.md +++ b/examples/python/cloud-function-user-provision/README.md @@ -2,13 +2,13 @@ This repository contains a [Google Cloud Function](https://cloud.google.com/functions) that leverages Looker Python SDK. The repository can be used as a starter template to build serverless microservices that interact with Looker through the following workflow: -1. Send a POST request to trigger an HTTP-based Cloud Function +1. Trigger an HTTP-based Cloud Function 2. Initialize the Looker Python SDK 3. Call Looker SDK methods and build custom logic to manage users, content, queries, etc. -In this repository, the `main.py` file takes an email address as an input and checks if this email has been registered with an existing Looker user. If an exisiting user is found, an email to reset the password will be sent to the user. Otherwise, a new user will be created, and a setup email will be sent. +In this repository, the `main.py` file takes an email address as an input and checks if this email has been registered with an existing Looker user. If a current user is found, an email to reset the password will be sent to the user. Otherwise, a new user will be created, and a setup email will be sent. -For more use cases and Python examples, check out [Looker's Python SDK examples](https://github.com/looker-open-source/sdk-codegen/tree/main/examples/python). +Check out [Looker's Python SDK examples](https://github.com/looker-open-source/sdk-codegen/tree/main/examples/python) for more code examples. ## Demo @@ -16,23 +16,24 @@ For more use cases and Python examples, check out [Looker's Python SDK examples] Demo

- ## Setup -The following steps assume deployment using Google Cloud UI Console. Check out ["Your First Function: Python"](https://cloud.google.com/functions/docs/first-python) for steps to deploy using the `gcloud` command line tool +The following steps assume deployment using Google Cloud UI Console. Check out ["Your First Function: Python"](https://cloud.google.com/functions/docs/first-python) for steps to deploy using the `gcloud` command-line tool 1. Obtain a [Looker API3 Key](https://docs.looker.com/admin-options/settings/users#api3_keys) 2. Follow the steps [provided here](https://cloud.google.com/functions/docs/quickstart-python) to create a new Google Cloud Function -3. Configure runtime environment variables using the Cloud Function UI: Edit > Configuration > Runtime, build, connections and security settings > Runtime environment variables. Alternatively, environtment variables can be configured through the `os` module or a `.ini`. Check out [Configuring Looker Python SDK](https://github.com/looker-open-source/sdk-codegen/tree/main/python#configuring-the-sdk) to learn more +3. If using Google Sheet: Grant "Viewer" permission to the email address associated with the "Runtime service account" in Cloud Functions. The recommendation is to use the [Default App Engine Service Account](https://cloud.google.com/appengine/docs/standard/python/service-account) and share its email (`YOUR_PROJECT_ID@appspot.gserviceaccount.com`) to the Google Sheet. + +4. Configure runtime environment variables using the Cloud Function UI: Edit > Configuration > Runtime, build, connections and security settings > Runtime environment variables. Alternatively, environment variables can be configured through the `os` module or a `.ini` file. Check [Configuring Looker Python SDK](https://github.com/looker-open-source/sdk-codegen/tree/main/python#configuring-the-sdk) for more information

Setting environmental variables in Cloud Function UI

-4. Copy and paste the contents of `main.py` in this repository into `main.py` file once inside Cloud Function's inline editor. Change the "Entry point" in the top right to `main`. `main.py` is executed once the function is triggered +5. Copy and paste the contents of `main.py` in this repository into the `main.py` file once inside Cloud Function's inline editor. Change the "Entry point" in the top right to the main function. `main.py` is executed once the function is triggered -5. Copy and paste the contents of `requirements.txt` in this repository to the `requirements.txt` file once inside Cloud Function's inline editor. This file is used to install neccessary libraries to execute the function +6. Copy and paste the contents of `requirements.txt` in this repository to the `requirements.txt` file once inside Cloud Function's inline editor. This file is used to install necessary libraries to execute the function -6. Deploy and test the function. Check out [this article](https://cloud.google.com/functions/docs/quickstart-python#test_the_function) for instruction +7. Deploy and test the function. Check out [this article](https://cloud.google.com/functions/docs/quickstart-python#test_the_function) for instruction diff --git a/examples/python/cloud-function-user-provision/main.py b/examples/python/cloud-function-user-provision/main.py index 9f78da559..87060c2ce 100644 --- a/examples/python/cloud-function-user-provision/main.py +++ b/examples/python/cloud-function-user-provision/main.py @@ -1,16 +1,29 @@ -"""This Cloud Function leverages Looker Python SDK to manage user provision. The -`main` function is used as the entry point to the code. It takes an email address -as an input through a POST request, then checks if this email has been associated -with an existing Looker user. If an exisiting user is found, then an email to -reset password will be sent. Otherwise, a new user will be created, and a setup email -will be sent. +"""This Cloud Function leverages Looker Python SDK to manage user provision. +It takes an email address as an input, then checks if this email has been +associated with an existing Looker user. If a current user is found, then an +email to reset the password will be sent. Otherwise, a new user will be created, +and a setup email will be sent. + +The `main` function is triggered through an HTTP request. Two example approaches +are provided below: + main(request): take a POST request in form of {"email":"test@test.com"}, + and read the email value from the request body + main_gsheet(request): take a GET request and read the email value from a cell + inside an existing Google sheet. HTTP Cloud Functions: https://cloud.google.com/functions/docs/writing/http#sample_usage""" +# If not using Google Sheet, removing Google modules here and in `requirements.txt` +from googleapiclient.discovery import build +import google.auth + import looker_sdk sdk = looker_sdk.init40() +# [START main(request)] def main(request): + """Take email from JSON body of a POST request, and use the email value + as an input for looker_user_provision() function""" try: request_json = request.get_json() email = request_json["email"] @@ -18,7 +31,46 @@ def main(request): return result except: return 'Please provide JSON in the format of {"email":"test@test.com"}' +# [END main(request)] + +# [START main_gsheet(request)] +def main_gsheet(request): + """Take email from a cell inside an existing Google Sheet""" + try: + email = get_email_from_sheet() + result = looker_user_provision(email=email) + return result + except: + return 'An error occurred.' + +def get_email_from_sheet(): + """ Authenticate to an existing Google Sheet using the default runtime + service account and extract the email address from a cell inside the sheet. + + Refer to Google Sheet API Python Quickstart for details: + https://developers.google.com/sheets/api/quickstart/python + """ + # Get the key of an existing Google Sheet from the URL. + # Example: https://docs.google.com/spreadsheets/d/[KEY HERE]/edit#gid=111 + SAMPLE_SPREADSHEET_ID = "foo" + + # Google Sheet Range: https://developers.google.com/sheets/api/samples/reading + SAMPLE_RANGE_NAME = "Sheet1!A:A" + + creds, _proj_id = google.auth.default() + service = build("sheets", "v4", credentials=creds) + sheet = service.spreadsheets() + result = sheet.values().get(spreadsheetId=SAMPLE_SPREADSHEET_ID, + range=SAMPLE_RANGE_NAME).execute() + # `values` will be a list of lists (i.e.: [['email1'], ['email2']]) + # and we can access value 'email' using index + values = result.get('values', []) + email = values[0][0] + return email +# [END main_gsheet(request)] + +# [START looker_user_provision] def looker_user_provision(email): user_id = search_users_by_email(email=email) if user_id is not None: @@ -49,7 +101,7 @@ def create_users(email): models_dir_validated=False ) ) - + # Create email credentials for the new user sdk.create_user_credentials_email( user_id=new_user.id, @@ -57,7 +109,8 @@ def create_users(email): email=email, forced_password_reset_at_next_login=False )) - + # Send a welcome/setup email sdk.send_user_credentials_email_password_reset(user_id=new_user["id"]) +# [END looker_user_provision] diff --git a/examples/python/cloud-function-user-provision/requirements.txt b/examples/python/cloud-function-user-provision/requirements.txt index 69c05299c..49a941dba 100644 --- a/examples/python/cloud-function-user-provision/requirements.txt +++ b/examples/python/cloud-function-user-provision/requirements.txt @@ -1,4 +1,6 @@ # Function dependencies, for example: # package>=version looker_sdk -requests +google-api-python-client==1.7.9 +google-auth-httplib2==0.0.3 +google-auth-oauthlib==0.4.0 diff --git a/packages/api-explorer/package.json b/packages/api-explorer/package.json index d3df0b6f2..73d5c5569 100644 --- a/packages/api-explorer/package.json +++ b/packages/api-explorer/package.json @@ -4,7 +4,7 @@ "description": "Looker API Explorer", "main": "lib/index.js", "module": "lib/esm/index.js", - "sideEffects": "false", + "sideEffects": false, "typings": "lib/index.d.ts", "license": "MIT", "author": "Looker", @@ -67,6 +67,7 @@ "webpack-merge": "^5.7.3" }, "dependencies": { + "@looker/extension-utils": "0.1.0", "@looker/code-editor": "^0.1.13", "@looker/components": "^2.8.1", "@looker/components-date": "^2.4.1", diff --git a/packages/api-explorer/src/ApiExplorer.tsx b/packages/api-explorer/src/ApiExplorer.tsx index 9ae68753f..d30a48dcd 100644 --- a/packages/api-explorer/src/ApiExplorer.tsx +++ b/packages/api-explorer/src/ApiExplorer.tsx @@ -44,8 +44,12 @@ import { funFetch, fallbackFetch, OAuthScene } from '@looker/run-it' import { FirstPage } from '@styled-icons/material/FirstPage' import { LastPage } from '@styled-icons/material/LastPage' -import type { IApixEnvAdaptor } from './utils' -import { oAuthPath, registerEnvAdaptor, unregisterEnvAdaptor } from './utils' +import type { IEnvironmentAdaptor } from '@looker/extension-utils' +import { + registerEnvAdaptor, + unregisterEnvAdaptor, +} from '@looker/extension-utils' +import { oAuthPath } from './utils' import { Header, SideNav, @@ -63,9 +67,10 @@ import { useLodeActions, useLodesStoreState, } from './state' + export interface ApiExplorerProps { specs: SpecList - envAdaptor: IApixEnvAdaptor + adaptor: IEnvironmentAdaptor setVersionsUrl: RunItSetter examplesLodeUrl?: string declarationsLodeUrl?: string @@ -76,7 +81,7 @@ const BodyOverride = createGlobalStyle` html { height: 100%; overflow: hidden; } export const ApiExplorer: FC = ({ specs, - envAdaptor, + adaptor, setVersionsUrl, examplesLodeUrl = 'https://raw.githubusercontent.com/looker-open-source/sdk-codegen/main/examplesIndex.json', declarationsLodeUrl = `${apixFilesHost}/declarationsIndex.json`, @@ -105,7 +110,7 @@ export const ApiExplorer: FC = ({ }, []) useEffect(() => { - registerEnvAdaptor(envAdaptor) + registerEnvAdaptor(adaptor) initSettingsAction() initLodesAction({ examplesLodeUrl, declarationsLodeUrl }) @@ -143,7 +148,7 @@ export const ApiExplorer: FC = ({ } }, [spec, location]) - const themeOverrides = envAdaptor.themeOverrides() + const themeOverrides = adaptor.themeOverrides() return ( <> @@ -154,7 +159,7 @@ export const ApiExplorer: FC = ({ {!initialized ? ( ) : ( - + {!headless && (
= ({ specKey={spec.key} specs={specs} toggleNavigation={toggleNavigation} - envAdaptor={envAdaptor} + adaptor={adaptor} setVersionsUrl={setVersionsUrl} /> )} diff --git a/packages/api-explorer/src/StandaloneApiExplorer.tsx b/packages/api-explorer/src/StandaloneApiExplorer.tsx index 3a88c9900..7f1d6607c 100644 --- a/packages/api-explorer/src/StandaloneApiExplorer.tsx +++ b/packages/api-explorer/src/StandaloneApiExplorer.tsx @@ -37,10 +37,10 @@ import { import type { IAPIMethods } from '@looker/sdk-rtl' import type { SpecList } from '@looker/sdk-codegen' import { Provider } from 'react-redux' +import { BrowserAdaptor } from '@looker/extension-utils' import { ApiExplorer } from './ApiExplorer' import { store } from './state' -import { StandaloneEnvAdaptor } from './utils' import { Loader } from './components' export interface StandaloneApiExplorerProps { @@ -48,10 +48,10 @@ export interface StandaloneApiExplorerProps { versionsUrl: string } -const standaloneEnvAdaptor = new StandaloneEnvAdaptor() +const browserAdaptor = new BrowserAdaptor() const loadVersions = async (current: string) => { - const data = await standaloneEnvAdaptor.localStorageGetItem(RunItConfigKey) + 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) @@ -99,12 +99,12 @@ export const StandaloneApiExplorer: FC = ({ {specs ? ( ) : ( - + )} diff --git a/packages/api-explorer/src/components/DocMarkdown/DocMarkdown.tsx b/packages/api-explorer/src/components/DocMarkdown/DocMarkdown.tsx index 119e8109e..71a83b5a7 100644 --- a/packages/api-explorer/src/components/DocMarkdown/DocMarkdown.tsx +++ b/packages/api-explorer/src/components/DocMarkdown/DocMarkdown.tsx @@ -29,7 +29,7 @@ import React from 'react' import { useHistory } from 'react-router-dom' import { Markdown } from '@looker/code-editor' import { useSelector } from 'react-redux' -import { getEnvAdaptor } from '../../utils' +import { getEnvAdaptor } from '@looker/extension-utils' import { selectSearchPattern } from '../../state' import { transformURL } from './utils' @@ -48,8 +48,8 @@ export const DocMarkdown: FC = ({ source, specKey }) => { } else if (url.startsWith(`/${specKey}`)) { history.push(url) } else if (url.startsWith('https://')) { - const envAdaptor = getEnvAdaptor() - envAdaptor.openBrowserWindow(url) + const adaptor = getEnvAdaptor() + adaptor.openBrowserWindow(url) } } return ( diff --git a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx index 1d494ea88..9991e4ce8 100644 --- a/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.spec.tsx @@ -29,11 +29,9 @@ import userEvent from '@testing-library/user-event' import { codeGenerators } from '@looker/sdk-codegen' import * as reactRedux from 'react-redux' +import { registerTestEnvAdaptor } from '@looker/extension-utils' import { defaultSettingsState, settingsSlice } from '../../state' -import { - registerTestEnvAdaptor, - renderWithReduxProvider, -} from '../../test-utils' +import { renderWithReduxProvider } from '../../test-utils' import { SdkLanguageSelector } from './SdkLanguageSelector' describe('SdkLanguageSelector', () => { diff --git a/packages/api-explorer/src/components/common/Loader.tsx b/packages/api-explorer/src/components/common/Loader.tsx index 596aefa58..d52cbb364 100644 --- a/packages/api-explorer/src/components/common/Loader.tsx +++ b/packages/api-explorer/src/components/common/Loader.tsx @@ -32,7 +32,7 @@ import { Heading, ProgressCircular, } from '@looker/components' -import type { ThemeOverrides } from '../../utils' +import type { ThemeOverrides } from '@looker/extension-utils' export interface LoaderProps { themeOverrides: ThemeOverrides diff --git a/packages/api-explorer/src/routes/AppRouter.tsx b/packages/api-explorer/src/routes/AppRouter.tsx index 54084807f..13b1afd7a 100644 --- a/packages/api-explorer/src/routes/AppRouter.tsx +++ b/packages/api-explorer/src/routes/AppRouter.tsx @@ -23,11 +23,13 @@ SOFTWARE. */ + 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, @@ -37,7 +39,6 @@ import { TypeTagScene, } from '../scenes' import { DiffScene } from '../scenes/DiffScene' -import type { IApixEnvAdaptor } from '../utils' import { diffPath } from '../utils' interface AppRouterProps { @@ -45,7 +46,7 @@ interface AppRouterProps { specKey: string specs: SpecList toggleNavigation: (target?: boolean) => void - envAdaptor: IApixEnvAdaptor + adaptor: IEnvironmentAdaptor setVersionsUrl: RunItSetter } @@ -54,7 +55,7 @@ export const AppRouter: FC = ({ api, specs, toggleNavigation, - envAdaptor, + adaptor, setVersionsUrl, }) => { return ( @@ -72,7 +73,7 @@ export const AppRouter: FC = ({ diff --git a/packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx b/packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx index 790eb569c..aa3b40a88 100644 --- a/packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx +++ b/packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx @@ -42,6 +42,7 @@ 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 { ApixSection, DocActivityType, @@ -57,12 +58,11 @@ import { DocSchema, } from '../../components' import { selectSdkLanguage } from '../../state' -import type { IApixEnvAdaptor } from '../../utils' import { DocOperation, DocRequestBody } from './components' interface MethodSceneProps { api: ApiModel - envAdaptor: IApixEnvAdaptor + adaptor: IEnvironmentAdaptor setVersionsUrl: RunItSetter } @@ -72,14 +72,14 @@ interface MethodSceneParams { specKey: string } -const showRunIt = async (envAdaptor: IApixEnvAdaptor) => { - const data = await envAdaptor.localStorageGetItem(RunItFormKey) +const showRunIt = async (adaptor: IEnvironmentAdaptor) => { + const data = await adaptor.localStorageGetItem(RunItFormKey) return !!data } export const MethodScene: FC = ({ api, - envAdaptor, + adaptor, setVersionsUrl, }) => { const history = useHistory() @@ -110,7 +110,7 @@ export const MethodScene: FC = ({ useEffect(() => { const checkRunIt = async () => { try { - const show = await showRunIt(envAdaptor) + const show = await showRunIt(adaptor) if (show) { setOn() } @@ -119,7 +119,7 @@ export const MethodScene: FC = ({ } } checkRunIt() - }, [envAdaptor, setOn]) + }, [adaptor, setOn]) const runItToggle = ( { }) ) expect(localStorage.setItem).toHaveBeenLastCalledWith( - EnvAdaptorConstants.LOCALSTORAGE_SETTINGS_KEY, + StoreConstants.LOCALSTORAGE_SETTINGS_KEY, JSON.stringify({ sdkLanguage: 'Kotlin' }) ) }) diff --git a/packages/api-explorer/src/state/settings/sagas.ts b/packages/api-explorer/src/state/settings/sagas.ts index 9777f4a3d..c9ea8a6e1 100644 --- a/packages/api-explorer/src/state/settings/sagas.ts +++ b/packages/api-explorer/src/state/settings/sagas.ts @@ -25,7 +25,8 @@ */ import { takeEvery, call, put, select } from 'typed-redux-saga' -import { EnvAdaptorConstants, getEnvAdaptor } from '../../utils' +import { getEnvAdaptor } from '@looker/extension-utils' +import { StoreConstants } from '@looker/run-it' import type { RootState } from '../store' import { settingActions, defaultSettings } from './slice' @@ -33,13 +34,12 @@ import { settingActions, defaultSettings } from './slice' * Serializes state to local storage */ function* serializeToLocalStorageSaga() { - const envAdaptor = getEnvAdaptor() + const adaptor = getEnvAdaptor() const settings = yield* select((state: RootState) => ({ sdkLanguage: state.settings.sdkLanguage, })) - yield* call( - envAdaptor.localStorageSetItem, - EnvAdaptorConstants.LOCALSTORAGE_SETTINGS_KEY, + adaptor.localStorageSetItem( + StoreConstants.LOCALSTORAGE_SETTINGS_KEY, JSON.stringify(settings) ) } @@ -47,11 +47,10 @@ function* serializeToLocalStorageSaga() { /** * Returns default settings overridden with any persisted state in local storage */ -function* deserializeLocalStorage() { - const envAdaptor = getEnvAdaptor() - const settings = yield* call( - envAdaptor.localStorageGetItem, - EnvAdaptorConstants.LOCALSTORAGE_SETTINGS_KEY +async function deserializeLocalStorage() { + const adaptor = getEnvAdaptor() + const settings = await adaptor.localStorageGetItem( + StoreConstants.LOCALSTORAGE_SETTINGS_KEY ) return settings ? { ...defaultSettings, ...JSON.parse(settings) } diff --git a/packages/api-explorer/src/test-utils/index.ts b/packages/api-explorer/src/test-utils/index.ts index 870a334ed..6e421dbf5 100644 --- a/packages/api-explorer/src/test-utils/index.ts +++ b/packages/api-explorer/src/test-utils/index.ts @@ -26,4 +26,3 @@ export * from './lodes' export * from './router' export * from './redux' -export { registerTestEnvAdaptor } from './envAdaptor' diff --git a/packages/api-explorer/src/test-utils/redux.tsx b/packages/api-explorer/src/test-utils/redux.tsx index e85729b66..898a0d0d6 100644 --- a/packages/api-explorer/src/test-utils/redux.tsx +++ b/packages/api-explorer/src/test-utils/redux.tsx @@ -23,6 +23,7 @@ SOFTWARE. */ + import type { ReactElement } from 'react' import React from 'react' import { Provider } from 'react-redux' @@ -31,6 +32,7 @@ import { renderWithTheme } from '@looker/components-test-utils' import type { RenderOptions } from '@testing-library/react' import { createStore } from '@looker/redux' +import { BrowserAdaptor, registerEnvAdaptor } from '@looker/extension-utils' import type { LodesState, RootState, SettingState } from '../state' import { settingsSlice, @@ -39,14 +41,13 @@ import { store as defaultStore, lodesSlice, } from '../state' -import { registerEnvAdaptor, StandaloneEnvAdaptor } from '../utils' import { renderWithRouter } from './router' export const withReduxProvider = ( consumers: ReactElement, store: Store = defaultStore ) => { - registerEnvAdaptor(new StandaloneEnvAdaptor()) + registerEnvAdaptor(new BrowserAdaptor()) return {consumers} } diff --git a/packages/api-explorer/src/utils/index.ts b/packages/api-explorer/src/utils/index.ts index e17aaae2b..e20ee732c 100644 --- a/packages/api-explorer/src/utils/index.ts +++ b/packages/api-explorer/src/utils/index.ts @@ -33,4 +33,3 @@ export { } from './path' export { getLoded } from './lodeUtils' export { useWindowSize } from './useWindowSize' -export * from './envAdaptor' diff --git a/packages/code-editor/package.json b/packages/code-editor/package.json index 686c89b9c..ac406dfd3 100644 --- a/packages/code-editor/package.json +++ b/packages/code-editor/package.json @@ -4,7 +4,7 @@ "description": "A syntax highlighter Viewer and Editor for Looker SDK supported languages.", "main": "lib/index.js", "module": "lib/esm/index.js", - "sideEffects": "false", + "sideEffects": false, "typings": "lib/index.d.ts", "files": [ "lib" diff --git a/packages/extension-api-explorer/package.json b/packages/extension-api-explorer/package.json index 17eadffe5..f3d21de36 100644 --- a/packages/extension-api-explorer/package.json +++ b/packages/extension-api-explorer/package.json @@ -1,7 +1,7 @@ { "name": "@looker/extension-api-explorer", "version": "21.18.1", - "description": "Looker API Explorer extension version ", + "description": "Looker API Explorer extension", "main": "dist/bundle.js", "sideEffects": false, "license": "MIT", @@ -16,6 +16,7 @@ }, "dependencies": { "@looker/api-explorer": "^0.9.22", + "@looker/extension-utils": "0.1.0", "@looker/components": "^2.8.1", "@looker/extension-sdk": "^21.18.1", "@looker/extension-sdk-react": "^21.18.1", diff --git a/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx b/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx index f46226306..47bf05c96 100644 --- a/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx +++ b/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx @@ -35,7 +35,7 @@ import { getSpecsFromVersions } from '@looker/sdk-codegen' import { ApiExplorer, store, Loader } from '@looker/api-explorer' import { getExtensionSDK } from '@looker/extension-sdk' import { Provider } from 'react-redux' -import { ExtensionEnvAdaptor } from './utils' +import { ExtensionAdaptor } from '@looker/extension-utils' class ExtensionConfigurator implements RunItConfigurator { storage: Record = {} @@ -66,7 +66,6 @@ class ExtensionConfigurator implements RunItConfigurator { const configurator = new ExtensionConfigurator() export const ExtensionApiExplorer: FC = () => { - // const match = useRouteMatch<{ specKey: string }>(`/:specKey`) const extensionContext = useContext(ExtensionContext) const [specs, setSpecs] = useState() @@ -88,7 +87,7 @@ export const ExtensionApiExplorer: FC = () => { if (sdk && !specs) loadSpecs().catch((err) => console.error(err)) }, [specs, sdk]) - const extensionEnvAdaptor = new ExtensionEnvAdaptor(getExtensionSDK()) + const extensionAdaptor = new ExtensionAdaptor(getExtensionSDK()) return ( @@ -97,13 +96,13 @@ export const ExtensionApiExplorer: FC = () => { {specs ? ( ) : ( - + )} diff --git a/packages/extension-playground/package.json b/packages/extension-playground/package.json index b627fc68a..af380b3ff 100644 --- a/packages/extension-playground/package.json +++ b/packages/extension-playground/package.json @@ -4,7 +4,7 @@ "description": "Extension Playground", "main": "lib/index.js", "module": "lib/esm/index.js", - "sideEffects": "false", + "sideEffects": false, "typings": "lib/index.d.ts", "license": "MIT", "private": true, diff --git a/packages/extension-sdk-react/package.json b/packages/extension-sdk-react/package.json index 42456307b..9ddcaa455 100644 --- a/packages/extension-sdk-react/package.json +++ b/packages/extension-sdk-react/package.json @@ -4,7 +4,7 @@ "description": "Looker Extension SDK for React", "main": "lib/index.js", "module": "lib/esm/index.js", - "sideEffects": "false", + "sideEffects": false, "typings": "lib/index.d.ts", "files": [ "lib" diff --git a/packages/extension-sdk/package.json b/packages/extension-sdk/package.json index 380109668..f7155f798 100644 --- a/packages/extension-sdk/package.json +++ b/packages/extension-sdk/package.json @@ -4,7 +4,7 @@ "description": "Looker Extension SDK", "main": "lib/index.js", "module": "lib/esm/index.js", - "sideEffects": "false", + "sideEffects": false, "typings": "lib/index.d.ts", "files": [ "lib" diff --git a/packages/extension-utils/CHANGELOG.md b/packages/extension-utils/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/packages/extension-utils/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/packages/extension-utils/LICENSE b/packages/extension-utils/LICENSE new file mode 100644 index 000000000..c67b8ffe7 --- /dev/null +++ b/packages/extension-utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 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. diff --git a/packages/extension-utils/README.md b/packages/extension-utils/README.md new file mode 100644 index 000000000..8dc7d42ec --- /dev/null +++ b/packages/extension-utils/README.md @@ -0,0 +1,79 @@ +# @looker/extension-utils + +## "Dual mode" Looker browser applications + +This package provides interfaces and classes that support building a Looker application that can be both hosted as +a Looker extension and as a browser application while using exactly the same source code. Looker's [API Explorer](https://github.com/looker-open-source/sdk-codegen/tree/main/packages/api-explorer) +is the original version of an application that can run just in the browser, or hosted in Looker as an extension. + +## Installation + +Either + +```shell +yarn add @looker/extension-utils +``` + +or + +```shell +npm install @looker/extension-utils +``` + +## Using environment adaptors + +All source code for the application except for the launch page can be the same. For the launch page, either `ExtensionAdaptor` or `BrowserAdaptor` will be used. + +### BrowserAdaptor + +See [StandAloneApiExplorer.tsx](https://github.com/looker-open-source/sdk-codegen/tree/main/packages/api-explorer/src/StandAloneApiExplorer.tsx) for a reference implementation. + +### ExtensionAdaptor + +See [ExtensionApiExplorer](https://github.com/looker-open-source/sdk-codegen/tree/main/packages/extension-api-explorer/src/ExtensionApiExplorer.tsx) for a reference implementation. + +### Configuring the extension provider + +The following code, extracted from the [Hackathon application's index.tsx](https://github.com/looker-open-source/sdk-codegen/tree/main/packages/hackathon/src/index.tsx), +configures the extension provider so the extension SDK and extension adaptor can be used in the [``](https://github.com/looker-open-source/sdk-codegen/tree/main/packages/hackathon/src/Hackathon.tsx) React component. + +```tsx +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { ExtensionProvider } from '@looker/extension-sdk-react' +import { Provider } from 'react-redux' +import { Hackathon } from './Hackathon' +import store from './data/store' + +window.addEventListener('DOMContentLoaded', (_) => { + const root = document.createElement('div') + document.body.appendChild(root) + ReactDOM.render( + + + + + , + root + ) +}) +``` + +Inside ``, this is the code that sets up the theming and "browser API" services like opening links: + +```tsx + const extSdk = getExtensionSDK() + const adaptor = new ExtensionAdaptor(extSdk) + const themeOverrides = adaptor.themeOverrides() + +// ... + + return ( + + // ... + + ) +``` diff --git a/packages/extension-utils/package.json b/packages/extension-utils/package.json new file mode 100644 index 000000000..90529358d --- /dev/null +++ b/packages/extension-utils/package.json @@ -0,0 +1,35 @@ +{ + "name": "@looker/extension-utils", + "version": "0.1.0", + "description": "Looker Extension Utilities", + "main": "dist/bundle.js", + "module": "lib/esm/index.js", + "sideEffects": false, + "typings": "lib/index.d.ts", + "files": [ + "lib" + ], + "license": "MIT", + "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.18.1", + "@looker/extension-sdk-react": "^21.18.1", + "react": "^16.13.1" + }, + "devDependencies": { + "@types/redux": "^3.6.0", + "webpack-bundle-analyzer": "^4.2.0", + "webpack-cli": "^4.6.0", + "webpack-dev-server": "^3.11.2", + "webpack-merge": "^5.7.3" + } +} diff --git a/packages/api-explorer/src/utils/envAdaptor.ts b/packages/extension-utils/src/adaptorUtils.ts similarity index 56% rename from packages/api-explorer/src/utils/envAdaptor.ts rename to packages/extension-utils/src/adaptorUtils.ts index ccc0b7f07..5a1b4e339 100644 --- a/packages/api-explorer/src/utils/envAdaptor.ts +++ b/packages/extension-utils/src/adaptorUtils.ts @@ -25,12 +25,13 @@ */ import type { ThemeCustomizations } from '@looker/design-tokens' +import { BrowserAdaptor } from './browserAdaptor' /** * NOTE: This interface should describe all methods that require an adaptor when running in standalone vs extension mode - * Examples include: local storage operations, writing to clipboard and various link navigation functions amongst others + * Examples include: local storage operations and various link navigation functions */ -export interface IApixEnvAdaptor { +export interface IEnvironmentAdaptor { /** Method for retrieving a keyed value from local storage */ localStorageGetItem(key: string): Promise /** Method for setting a keyed value in local storage */ @@ -51,12 +52,32 @@ export interface IApixEnvAdaptor { * system is being used (for example an embedded extension). */ export interface ThemeOverrides { + /** Should Google-specific fonts be used for the theme? */ loadGoogleFonts?: boolean + /** Property bag overrides for Looker component theming */ themeCustomizations?: ThemeCustomizations } -export const getThemeOverrides = (useGoogleFonts: boolean): ThemeOverrides => - useGoogleFonts +/** + * Is this an "internal" host that will use internal brancing? + * @param hostname to check + */ +export const hostedInternally = (hostname: string): boolean => + hostname.endsWith('.looker.com') || + hostname.endsWith('.google.com') || + hostname === 'localhost' || + // Include firebase staging dev portal for now. Can be removed + // when dev portal gets its own APIX project. Also includes + // PRs. + (hostname.startsWith('looker-developer-portal') && + hostname.endsWith('.web.app')) + +/** + * Return theme overrides that make apply "internal" or external theming + * @param internalTheming true if "internal" theme should be used + */ +export const getThemeOverrides = (internalTheming: boolean): ThemeOverrides => + internalTheming ? { loadGoogleFonts: true, themeCustomizations: { @@ -70,78 +91,37 @@ export const getThemeOverrides = (useGoogleFonts: boolean): ThemeOverrides => }, } -/** - * An adaptor class for interacting with browser APIs when running in standalone mode - */ -export class StandaloneEnvAdaptor implements IApixEnvAdaptor { - private _themeOverrides: ThemeOverrides - - constructor() { - const { hostname } = location - this._themeOverrides = getThemeOverrides( - hostname.endsWith('.looker.com') || - hostname.endsWith('.google.com') || - hostname === 'localhost' || - // Include firebase staging dev portal for now. Can be removed - // when dev portal gets its own APIX project. Also includes - // PRs. - (hostname.startsWith('looker-developer-portal') && - hostname.endsWith('.web.app')) - ) - } - - async localStorageGetItem(key: string) { - return localStorage.getItem(key) - } - - async localStorageSetItem(key: string, value: string) { - await localStorage.setItem(key, value) - } - - async localStorageRemoveItem(key: string) { - await localStorage.removeItem(key) - } - - themeOverrides() { - return this._themeOverrides - } - - openBrowserWindow(url: string, target?: string) { - window.open(url, target) - } - - logError(_error: Error, _componentStack: string): void { - // noop - error logging for standalone APIX TBD - } -} - -export enum EnvAdaptorConstants { - LOCALSTORAGE_SDK_LANGUAGE_KEY = 'sdkLanguage', - LOCALSTORAGE_SETTINGS_KEY = 'settings', -} - -let envAdaptor: IApixEnvAdaptor | undefined +let extensionAdaptor: IEnvironmentAdaptor | undefined /** - * Register the environment adaptor. The API Explorer will automatically call this. + * Register the environment adaptor. Used when initializing the application + * @param adaptor to register */ -export const registerEnvAdaptor = (adaptor: IApixEnvAdaptor) => { - envAdaptor = adaptor +export const registerEnvAdaptor = (adaptor: IEnvironmentAdaptor) => { + extensionAdaptor = adaptor } /** - * Unregister the envAdaptor. The API Explorer will automatically call this when it is unmounted. + * Unregister the environment adaptor. Extensions should call this when unmounted */ export const unregisterEnvAdaptor = () => { - envAdaptor = undefined + extensionAdaptor = undefined } /** - * Global access to the envAdaptor. An error will be thrown if accessed prematurely. + * Global access to the environment adaptor. An error will be thrown if accessed prematurely. */ export const getEnvAdaptor = () => { - if (!envAdaptor) { + if (!extensionAdaptor) { throw new Error('Environment adaptor not initialized.') } - return envAdaptor + return extensionAdaptor +} + +/** + * Used by some unit tests + * @param adaptor to use for testing + */ +export const registerTestEnvAdaptor = (adaptor?: IEnvironmentAdaptor) => { + registerEnvAdaptor(adaptor || new BrowserAdaptor()) } diff --git a/packages/api-explorer/src/utils/envAdaptor.spec.ts b/packages/extension-utils/src/browserAdaptor.spec.ts similarity index 85% rename from packages/api-explorer/src/utils/envAdaptor.spec.ts rename to packages/extension-utils/src/browserAdaptor.spec.ts index bc409e71b..48541e31f 100644 --- a/packages/api-explorer/src/utils/envAdaptor.spec.ts +++ b/packages/extension-utils/src/browserAdaptor.spec.ts @@ -23,10 +23,12 @@ SOFTWARE. */ -import type { ThemeOverrides } from './envAdaptor' -import { StandaloneEnvAdaptor, getThemeOverrides } from './envAdaptor' -describe('StandaloneEnvAdaptor', () => { +import type { ThemeOverrides } from './adaptorUtils' +import { getThemeOverrides } from './adaptorUtils' +import { BrowserAdaptor } from './browserAdaptor' + +describe('BrowserAdaptor', () => { test.each([ ['www.looker.com', getThemeOverrides(true)], ['www.google.com', getThemeOverrides(true)], @@ -41,9 +43,7 @@ describe('StandaloneEnvAdaptor', () => { ...saveLoc, hostname, } - expect(new StandaloneEnvAdaptor().themeOverrides()).toEqual( - expectedOverrides - ) + expect(new BrowserAdaptor().themeOverrides()).toEqual(expectedOverrides) window.location = saveLoc } ) diff --git a/packages/hackathon/src/App.tsx b/packages/extension-utils/src/browserAdaptor.ts similarity index 52% rename from packages/hackathon/src/App.tsx rename to packages/extension-utils/src/browserAdaptor.ts index b6d9c55f5..c5ecb48bf 100644 --- a/packages/hackathon/src/App.tsx +++ b/packages/extension-utils/src/browserAdaptor.ts @@ -24,23 +24,41 @@ */ -import type { FC } from 'react' -import React from 'react' -import { Provider } from 'react-redux' -import { ExtensionProvider } from '@looker/extension-sdk-react' -import { ComponentsProvider } from '@looker/components' -import { hot } from 'react-hot-loader/root' -import { Hackathon } from './Hackathon' -import store from './data/store' - -export const App: FC = hot(() => { - return ( - - - - - - - - ) -}) +import type { IEnvironmentAdaptor, ThemeOverrides } from './adaptorUtils' +import { getThemeOverrides, hostedInternally } from './adaptorUtils' + +/** + * An adaptor class for interacting with browser APIs when not running in an extension + */ +export class BrowserAdaptor implements IEnvironmentAdaptor { + private _themeOverrides: ThemeOverrides + + constructor() { + const { hostname } = location + this._themeOverrides = getThemeOverrides(hostedInternally(hostname)) + } + + async localStorageGetItem(key: string) { + return localStorage.getItem(key) + } + + async localStorageSetItem(key: string, value: string) { + await localStorage.setItem(key, value) + } + + async localStorageRemoveItem(key: string) { + await localStorage.removeItem(key) + } + + themeOverrides() { + return this._themeOverrides + } + + openBrowserWindow(url: string, target?: string) { + window.open(url, target) + } + + logError(_error: Error, _componentStack: string): void { + // noop - error logging for standalone applications TBD + } +} diff --git a/packages/extension-api-explorer/src/utils.spec.ts b/packages/extension-utils/src/extensionAdaptor.spec.ts similarity index 86% rename from packages/extension-api-explorer/src/utils.spec.ts rename to packages/extension-utils/src/extensionAdaptor.spec.ts index 6802b3a23..49184ba40 100644 --- a/packages/extension-api-explorer/src/utils.spec.ts +++ b/packages/extension-utils/src/extensionAdaptor.spec.ts @@ -23,12 +23,12 @@ SOFTWARE. */ -import type { ThemeOverrides } from '@looker/api-explorer/src/utils' -import { getThemeOverrides } from '@looker/api-explorer/src/utils' import type { ExtensionSDK, LookerHostData } from '@looker/extension-sdk' -import { ExtensionEnvAdaptor } from './utils' +import { ExtensionAdaptor } from './extensionAdaptor' +import type { ThemeOverrides } from '@looker/extension-utils' +import { getThemeOverrides } from '@looker/extension-utils' -describe('ExtensionEnvAdaptor', () => { +describe('ExtensionAdaptor', () => { test.each([ [undefined, getThemeOverrides(false)], ['standard', getThemeOverrides(true)], @@ -38,7 +38,7 @@ describe('ExtensionEnvAdaptor', () => { 'returns correct font overrides', (hostType?: string, expectedOverrides?: ThemeOverrides) => { expect( - new ExtensionEnvAdaptor({ + new ExtensionAdaptor({ lookerHostData: { hostType, } as Readonly, diff --git a/packages/extension-api-explorer/src/utils.ts b/packages/extension-utils/src/extensionAdaptor.ts similarity index 91% rename from packages/extension-api-explorer/src/utils.ts rename to packages/extension-utils/src/extensionAdaptor.ts index 5727e2788..60328d3bf 100644 --- a/packages/extension-api-explorer/src/utils.ts +++ b/packages/extension-utils/src/extensionAdaptor.ts @@ -23,14 +23,15 @@ SOFTWARE. */ -import type { IApixEnvAdaptor, ThemeOverrides } from '@looker/api-explorer' -import { getThemeOverrides } from '@looker/api-explorer' + import type { ExtensionSDK } from '@looker/extension-sdk' +import type { IEnvironmentAdaptor, ThemeOverrides } from './adaptorUtils' +import { getThemeOverrides } from './adaptorUtils' /** * An adaptor class for interacting with browser APIs when running as an extension */ -export class ExtensionEnvAdaptor implements IApixEnvAdaptor { +export class ExtensionAdaptor implements IEnvironmentAdaptor { _themeOverrides: ThemeOverrides constructor(public extensionSdk: ExtensionSDK) { this._themeOverrides = getThemeOverrides( diff --git a/packages/api-explorer/src/test-utils/envAdaptor.ts b/packages/extension-utils/src/index.ts similarity index 81% rename from packages/api-explorer/src/test-utils/envAdaptor.ts rename to packages/extension-utils/src/index.ts index fe1972faf..95dc21291 100644 --- a/packages/api-explorer/src/test-utils/envAdaptor.ts +++ b/packages/extension-utils/src/index.ts @@ -23,9 +23,7 @@ SOFTWARE. */ -import type { IApixEnvAdaptor } from '../utils' -import { registerEnvAdaptor, StandaloneEnvAdaptor } from '../utils' -export const registerTestEnvAdaptor = (envAdaptor?: IApixEnvAdaptor) => { - registerEnvAdaptor(envAdaptor || new StandaloneEnvAdaptor()) -} +export * from './adaptorUtils' +export * from './browserAdaptor' +export * from './extensionAdaptor' diff --git a/packages/extension-utils/tsconfig.build.json b/packages/extension-utils/tsconfig.build.json new file mode 100644 index 000000000..51db2ccb4 --- /dev/null +++ b/packages/extension-utils/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["src/**/*"] +} diff --git a/packages/hackathon/package.json b/packages/hackathon/package.json index 301347567..474071f48 100644 --- a/packages/hackathon/package.json +++ b/packages/hackathon/package.json @@ -34,6 +34,7 @@ "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/extension-utils": "^0.1.0", "@looker/sdk": "^21.18.1", "@looker/sdk-rtl": "^21.1.1", "@looker/code-editor": "^0.1.13", diff --git a/packages/hackathon/src/Hackathon.tsx b/packages/hackathon/src/Hackathon.tsx index ffb1ee79b..3a849c9d6 100644 --- a/packages/hackathon/src/Hackathon.tsx +++ b/packages/hackathon/src/Hackathon.tsx @@ -26,11 +26,20 @@ import type { FC } from 'react' import React, { useEffect } from 'react' import styled from 'styled-components' -import { Page, Layout, Aside, Section, MessageBar } from '@looker/components' +import { + Page, + Layout, + Aside, + Section, + MessageBar, + ComponentsProvider, +} from '@looker/components' +import { hot } from 'react-hot-loader/root' import { useSelector, useDispatch } from 'react-redux' -import { SideNav } from './components/SideNav' -import { Header } from './components/Header' +import { getExtensionSDK } from '@looker/extension-sdk' +import { ExtensionAdaptor } from '@looker/extension-utils' +import { SideNav, Header } from './components' import { AppRouter, getAuthorizedRoutes } from './routes' import { getMessageState } from './data/common/selectors' import { actionClearMessage } from './data/common/actions' @@ -53,7 +62,11 @@ const banner = (currentHackathon: any, hacker?: IHackerProps) => { return result } -export const Hackathon: FC = () => { +export const Hackathon: FC = hot(() => { + const extSdk = getExtensionSDK() + const adaptor = new ExtensionAdaptor(extSdk) + const themeOverrides = adaptor.themeOverrides() + const dispatch = useDispatch() useEffect(() => { dispatch(initHackSessionRequest()) @@ -69,26 +82,31 @@ export const Hackathon: FC = () => { } return ( - - -
- {message && ( - - {message.messageText} - - )} - - -
- -
-
- - + + + +
+ {message && ( + + {message.messageText} + + )} + + +
+ +
+
+ + + ) -} +}) const Background = styled.div` background-color: #ffffff; diff --git a/packages/hackathon/src/components/Loading/Loading.tsx b/packages/hackathon/src/components/Loading/Loading.tsx index 699e310c9..d1d0d97e1 100644 --- a/packages/hackathon/src/components/Loading/Loading.tsx +++ b/packages/hackathon/src/components/Loading/Loading.tsx @@ -29,18 +29,18 @@ import React from 'react' import { Flex, ProgressCircular, Text } from '@looker/components' interface LoadingProps { - loading: boolean + loading?: boolean message?: string } export const Loading: FC = ({ - loading, + loading = true, message = 'Loading ...', }) => ( - + {loading && ( <> - + {message} )} diff --git a/packages/hackathon/src/index.tsx b/packages/hackathon/src/index.tsx index e8979d694..eae343915 100644 --- a/packages/hackathon/src/index.tsx +++ b/packages/hackathon/src/index.tsx @@ -25,10 +25,20 @@ */ import * as React from 'react' import * as ReactDOM from 'react-dom' -import { App } from './App' +import { ExtensionProvider } from '@looker/extension-sdk-react' +import { Provider } from 'react-redux' +import { Hackathon } from './Hackathon' +import store from './data/store' window.addEventListener('DOMContentLoaded', (_) => { const root = document.createElement('div') document.body.appendChild(root) - ReactDOM.render(, root) + ReactDOM.render( + + + + + , + root + ) }) diff --git a/packages/hackathon/src/scenes/AdminScene/AdminScene.tsx b/packages/hackathon/src/scenes/AdminScene/AdminScene.tsx index 7ecc4464b..054445b4d 100644 --- a/packages/hackathon/src/scenes/AdminScene/AdminScene.tsx +++ b/packages/hackathon/src/scenes/AdminScene/AdminScene.tsx @@ -25,7 +25,14 @@ */ import type { FC } from 'react' import React, { useEffect } from 'react' -import { TabList, Tab, TabPanels, TabPanel } from '@looker/components' +import { + TabList, + Tab, + TabPanels, + TabPanel, + Heading, + SpaceVertical, +} from '@looker/components' import { useHistory, useRouteMatch } from 'react-router-dom' import { Routes } from '../../routes/AppRouter' import { getTabInfo } from '../../utils' @@ -59,22 +66,30 @@ export const AdminScene: FC = () => { return ( <> - - General - Configuration - Add Users - - - -
General admin stuff TBD
-
- - - - - - -
+ + Admin + + {/* Tab components incorrectly sets content height causing unnecessary + scrolling. Wrapping with SpaceVertical fixes this. TODO: Remove this hack + once tab components are fixed. */} + + + General + Configuration + Add Users + + + +
General admin stuff TBD
+
+ + + + + + +
+
) } diff --git a/packages/hackathon/src/scenes/HomeScene/HomeScene.tsx b/packages/hackathon/src/scenes/HomeScene/HomeScene.tsx index 40dc2f65d..023ffec56 100644 --- a/packages/hackathon/src/scenes/HomeScene/HomeScene.tsx +++ b/packages/hackathon/src/scenes/HomeScene/HomeScene.tsx @@ -27,8 +27,9 @@ import type { FC } from 'react' import React from 'react' -import { Heading, SpaceVertical } from '@looker/components' +import { Heading, SpaceVertical, Paragraph, Span } from '@looker/components' import type { IHackerProps } from '../../models' +import { ExtMarkdown } from '../../components' import { Agenda } from './components' import { localAgenda } from './agenda' @@ -42,9 +43,18 @@ export const HomeScene: FC = ({ hacker }) => { return ( <> - - Agenda - + + + Agenda + + + + + diff --git a/packages/hackathon/src/scenes/JudgingScene/JudgingScene.tsx b/packages/hackathon/src/scenes/JudgingScene/JudgingScene.tsx index dc846b028..128dde4ad 100644 --- a/packages/hackathon/src/scenes/JudgingScene/JudgingScene.tsx +++ b/packages/hackathon/src/scenes/JudgingScene/JudgingScene.tsx @@ -26,9 +26,11 @@ import type { FC } from 'react' import React from 'react' import { useSelector } from 'react-redux' +import { Space, Heading } from '@looker/components' import { isLoadingState } from '../../data/common/selectors' import { Loading } from '../../components/Loading' import { JudgingList } from './components' + interface JudgingSceneProps {} export const JudgingScene: FC = () => { @@ -36,7 +38,12 @@ export const JudgingScene: FC = () => { return ( <> - + + + Judgings + + {isLoading && } + ) diff --git a/packages/hackathon/src/scenes/ProjectsScene/ProjectsScene.tsx b/packages/hackathon/src/scenes/ProjectsScene/ProjectsScene.tsx index 79a60bc21..e3632c5a9 100644 --- a/packages/hackathon/src/scenes/ProjectsScene/ProjectsScene.tsx +++ b/packages/hackathon/src/scenes/ProjectsScene/ProjectsScene.tsx @@ -28,7 +28,7 @@ import type { FC } from 'react' import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' -import { Button, Space } from '@looker/components' +import { Button, Space, Heading } from '@looker/components' import { Add } from '@styled-icons/material-outlined/Add' import { Create } from '@styled-icons/material-outlined/Create' import { Lock } from '@styled-icons/material-outlined/Lock' @@ -65,7 +65,12 @@ export const ProjectsScene: FC = () => { return ( <> - + + + Projects + + {isLoading && } +