mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:31:48 +00:00
add storybooks for easier local dev on vscode UI (#50772)
Running `cd client/cody && pnpm run storybook` will open storybooks for the VS Code extension. There is a light shim in place to support rendering the VS Code extension UI with roughly the right theme colors. ### Storybook for `<App />` (VS Code main webview UI)   ### Storybook for login component showing invalid state  ## Test plan n/a; dev only --------- Co-authored-by: Naman Kumar <naman@outlook.in>
This commit is contained in:
parent
0b32522c2d
commit
838a029b99
@ -41,7 +41,8 @@
|
||||
"release": "ts-node ./scripts/release.ts",
|
||||
"watch": "concurrently \"pnpm watch:esbuild\" \"pnpm watch:webview\"",
|
||||
"watch:esbuild": "pnpm esbuild --sourcemap --watch",
|
||||
"watch:webview": "vite build --mode development --watch"
|
||||
"watch:webview": "vite build --mode development --watch",
|
||||
"storybook": "STORIES_GLOB='client/cody/webviews/**/*.story.tsx' pnpm --filter @sourcegraph/storybook run start"
|
||||
},
|
||||
"main": "./dist/extension.js",
|
||||
"activationEvents": [
|
||||
|
||||
35
client/cody/webviews/App.story.tsx
Normal file
35
client/cody/webviews/App.story.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { ComponentMeta, ComponentStoryObj } from '@storybook/react'
|
||||
|
||||
import { App } from './App'
|
||||
import { VSCodeStoryDecorator } from './storybook/VSCodeStoryDecorator'
|
||||
import { VSCodeWrapper } from './utils/VSCodeApi'
|
||||
|
||||
const meta: ComponentMeta<typeof App> = {
|
||||
title: 'cody/App',
|
||||
component: App,
|
||||
|
||||
decorators: [VSCodeStoryDecorator],
|
||||
|
||||
parameters: {
|
||||
component: App,
|
||||
chromatic: {
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Simple: ComponentStoryObj<typeof App> = {
|
||||
render: () => <App vscodeAPI={dummyVSCodeAPI} />,
|
||||
}
|
||||
|
||||
const dummyVSCodeAPI: VSCodeWrapper = {
|
||||
onMessage: cb => {
|
||||
// Send initial message so that the component is fully rendered.
|
||||
cb({ type: 'config', config: { debug: true, hasAccessToken: true, serverEndpoint: 'https://example.com' } })
|
||||
return () => {}
|
||||
},
|
||||
postMessage: () => {},
|
||||
}
|
||||
@ -15,9 +15,9 @@ import { NavBar, View } from './NavBar'
|
||||
import { Recipes } from './Recipes'
|
||||
import { Settings } from './Settings'
|
||||
import { UserHistory } from './UserHistory'
|
||||
import { vscodeAPI } from './utils/VSCodeApi'
|
||||
import type { VSCodeWrapper } from './utils/VSCodeApi'
|
||||
|
||||
export function App(): React.ReactElement {
|
||||
export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vscodeAPI }) => {
|
||||
const [config, setConfig] = useState<Pick<Configuration, 'debug' | 'serverEndpoint'> | null>(null)
|
||||
const [debugLog, setDebugLog] = useState(['No data yet'])
|
||||
const [view, setView] = useState<View | undefined>()
|
||||
@ -74,20 +74,23 @@ export function App(): React.ReactElement {
|
||||
|
||||
vscodeAPI.postMessage({ command: 'initialized' })
|
||||
// The dependencies array is empty to execute the callback only on component mount.
|
||||
}, [debugLog])
|
||||
}, [debugLog, vscodeAPI])
|
||||
|
||||
const onLogin = useCallback((token: string, endpoint: string) => {
|
||||
if (!token || !endpoint) {
|
||||
return
|
||||
}
|
||||
setIsValidLogin(undefined)
|
||||
vscodeAPI.postMessage({ command: 'settings', serverEndpoint: endpoint, accessToken: token })
|
||||
}, [])
|
||||
const onLogin = useCallback(
|
||||
(token: string, endpoint: string) => {
|
||||
if (!token || !endpoint) {
|
||||
return
|
||||
}
|
||||
setIsValidLogin(undefined)
|
||||
vscodeAPI.postMessage({ command: 'settings', serverEndpoint: endpoint, accessToken: token })
|
||||
},
|
||||
[vscodeAPI]
|
||||
)
|
||||
|
||||
const onLogout = useCallback(() => {
|
||||
vscodeAPI.postMessage({ command: 'removeToken' })
|
||||
setView('login')
|
||||
}, [setView])
|
||||
}, [vscodeAPI])
|
||||
|
||||
const onResetClick = useCallback(() => {
|
||||
setView('chat')
|
||||
@ -96,7 +99,7 @@ export function App(): React.ReactElement {
|
||||
setMessageInProgress(null)
|
||||
setTranscript([])
|
||||
vscodeAPI.postMessage({ command: 'reset' })
|
||||
}, [setView, setMessageInProgress, setTranscript, setDebugLog])
|
||||
}, [vscodeAPI])
|
||||
|
||||
if (!view) {
|
||||
return <LoadingPage />
|
||||
@ -123,9 +126,10 @@ export function App(): React.ReactElement {
|
||||
userHistory={userHistory}
|
||||
setUserHistory={setUserHistory}
|
||||
setInputHistory={setInputHistory}
|
||||
vscodeAPI={vscodeAPI}
|
||||
/>
|
||||
)}
|
||||
{view === 'recipes' && <Recipes />}
|
||||
{view === 'recipes' && <Recipes vscodeAPI={vscodeAPI} />}
|
||||
{view === 'settings' && (
|
||||
<Settings setView={setView} onLogout={onLogout} serverEndpoint={config?.serverEndpoint} />
|
||||
)}
|
||||
@ -138,6 +142,7 @@ export function App(): React.ReactElement {
|
||||
setFormInput={setFormInput}
|
||||
inputHistory={inputHistory}
|
||||
setInputHistory={setInputHistory}
|
||||
vscodeAPI={vscodeAPI}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -9,7 +9,7 @@ import { Chat as ChatUI, ChatUISubmitButtonProps, ChatUITextAreaProps } from '@s
|
||||
import { SubmitSvg } from '@sourcegraph/cody-ui/src/utils/icons'
|
||||
|
||||
import { FileLink } from './FileLink'
|
||||
import { vscodeAPI } from './utils/VSCodeApi'
|
||||
import { VSCodeWrapper } from './utils/VSCodeApi'
|
||||
|
||||
import styles from './Chat.module.css'
|
||||
|
||||
@ -21,6 +21,7 @@ interface ChatboxProps {
|
||||
setFormInput: (input: string) => void
|
||||
inputHistory: string[]
|
||||
setInputHistory: (history: string[]) => void
|
||||
vscodeAPI: VSCodeWrapper
|
||||
}
|
||||
|
||||
export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>> = ({
|
||||
@ -31,10 +32,14 @@ export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>
|
||||
setFormInput,
|
||||
inputHistory,
|
||||
setInputHistory,
|
||||
vscodeAPI,
|
||||
}) => {
|
||||
const onSubmit = useCallback((text: string) => {
|
||||
vscodeAPI.postMessage({ command: 'submit', text })
|
||||
}, [])
|
||||
const onSubmit = useCallback(
|
||||
(text: string) => {
|
||||
vscodeAPI.postMessage({ command: 'submit', text })
|
||||
},
|
||||
[vscodeAPI]
|
||||
)
|
||||
|
||||
return (
|
||||
<ChatUI
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import './Debug.css'
|
||||
import styles from './Debug.module.css'
|
||||
|
||||
interface DebugProps {
|
||||
debugLog: string[]
|
||||
@ -7,9 +7,10 @@ interface DebugProps {
|
||||
export const Debug: React.FunctionComponent<React.PropsWithChildren<DebugProps>> = ({ debugLog }) => (
|
||||
<div className="inner-container">
|
||||
<div className="non-transcript-container">
|
||||
<div className="debug-container" data-tab-target="debug">
|
||||
<div className={styles.debugContainer} data-tab-target="debug">
|
||||
{debugLog?.map((log, i) => (
|
||||
<div key={`log-${i}`} className="debug-message">
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={i} className={styles.debugMessage}>
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react'
|
||||
|
||||
import { FileLinkProps } from '@sourcegraph/cody-ui/src/chat/ContextFiles'
|
||||
|
||||
import { vscodeAPI } from './utils/VSCodeApi'
|
||||
import { getVSCodeAPI } from './utils/VSCodeApi'
|
||||
|
||||
import styles from './FileLink.module.css'
|
||||
|
||||
@ -11,7 +11,7 @@ export const FileLink: React.FunctionComponent<FileLinkProps> = ({ path }) => (
|
||||
className={styles.linkButton}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
vscodeAPI.postMessage({ command: 'openFile', filePath: path })
|
||||
getVSCodeAPI().postMessage({ command: 'openFile', filePath: path })
|
||||
}}
|
||||
>
|
||||
{path}
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import { VSCodeTag } from '@vscode/webview-ui-toolkit/react'
|
||||
|
||||
import './Header.css'
|
||||
|
||||
import { CodyColoredSvg } from '@sourcegraph/cody-ui/src/utils/icons'
|
||||
|
||||
import styles from './Header.module.css'
|
||||
|
||||
export const Header: React.FunctionComponent = () => (
|
||||
<div className="header-container">
|
||||
<div className="header-container-left">
|
||||
<div className="header-logo">
|
||||
<div className={styles.headerContainer}>
|
||||
<div className={styles.headerContainerLeft}>
|
||||
<div className={styles.headerLogo}>
|
||||
<CodyColoredSvg />
|
||||
</div>
|
||||
<div className="header-title">
|
||||
<span className="header-cody">Cody</span>
|
||||
<VSCodeTag className="tag-beta">experimental</VSCodeTag>
|
||||
<div className={styles.headerTitle}>
|
||||
<span className={styles.headerCody}>Cody</span>
|
||||
<VSCodeTag className={styles.tagBeta}>experimental</VSCodeTag>
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-container-right" />
|
||||
<div className={styles.headerContainerRight} />
|
||||
</div>
|
||||
)
|
||||
|
||||
27
client/cody/webviews/Login.story.tsx
Normal file
27
client/cody/webviews/Login.story.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { ComponentMeta, ComponentStoryObj } from '@storybook/react'
|
||||
|
||||
import { Login } from './Login'
|
||||
import { VSCodeStoryDecorator } from './storybook/VSCodeStoryDecorator'
|
||||
|
||||
const meta: ComponentMeta<typeof Login> = {
|
||||
title: 'cody/Login',
|
||||
component: Login,
|
||||
decorators: [VSCodeStoryDecorator],
|
||||
parameters: {
|
||||
component: Login,
|
||||
chromatic: {
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Simple: ComponentStoryObj<typeof Login> = {
|
||||
render: () => <Login onLogin={() => {}} isValidLogin={true} />,
|
||||
}
|
||||
|
||||
export const InvalidLogin: ComponentStoryObj<typeof Login> = {
|
||||
render: () => <Login onLogin={() => {}} isValidLogin={false} />,
|
||||
}
|
||||
@ -14,7 +14,7 @@
|
||||
.tab-btn {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: var(--vscode-tab-inactiveBackground);
|
||||
background-color: transparent;
|
||||
color: var(--vscode-tab-inactiveForeground);
|
||||
border: none;
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
import './NavBar.css'
|
||||
import React from 'react'
|
||||
|
||||
import styles from './NavBar.module.css'
|
||||
|
||||
export type View = 'chat' | 'recipes' | 'login' | 'settings' | 'debug' | 'history'
|
||||
|
||||
@ -27,16 +29,16 @@ export const NavBar: React.FunctionComponent<React.PropsWithChildren<NavBarProps
|
||||
onResetClick,
|
||||
showResetButton,
|
||||
}) => (
|
||||
<div className="tab-menu-container">
|
||||
<div className="tab-menu-group">
|
||||
<div className={styles.tabMenuContainer}>
|
||||
<div className={styles.tabMenuGroup}>
|
||||
{navBarItems.map(({ title, tab }) => (
|
||||
<button key={title} onClick={() => setView(tab)} className="tab-btn" type="button">
|
||||
<p className={view === tab ? 'tab-btn-selected' : ''}>{title}</p>
|
||||
<button key={title} onClick={() => setView(tab)} className={styles.tabBtn} type="button">
|
||||
<p className={view === tab ? styles.tabBtnSelected : ''}>{title}</p>
|
||||
</button>
|
||||
))}
|
||||
{devMode && (
|
||||
<button onClick={() => setView('debug')} className="tab-btn" type="button">
|
||||
<p className={view === 'debug' ? 'tab-btn-selected' : ''}>Debug</p>
|
||||
<button onClick={() => setView('debug')} className={styles.tabBtn} type="button">
|
||||
<p className={view === 'debug' ? styles.tabBtnSelected : ''}>Debug</p>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -44,7 +46,7 @@ export const NavBar: React.FunctionComponent<React.PropsWithChildren<NavBarProps
|
||||
{showResetButton && (
|
||||
<button
|
||||
onClick={() => onResetClick()}
|
||||
className="tab-btn"
|
||||
className={styles.tabBtn}
|
||||
type="button"
|
||||
title="Start a new conversation"
|
||||
>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
|
||||
|
||||
import { vscodeAPI } from './utils/VSCodeApi'
|
||||
import { VSCodeWrapper } from './utils/VSCodeApi'
|
||||
|
||||
import './Recipes.css'
|
||||
import styles from './Recipes.module.css'
|
||||
|
||||
export const recipesList = {
|
||||
'explain-code-detailed': 'Explain selected code (detailed)',
|
||||
@ -16,7 +16,7 @@ export const recipesList = {
|
||||
fixup: 'Fixup code from inline instructions',
|
||||
}
|
||||
|
||||
export function Recipes(): React.ReactElement {
|
||||
export const Recipes: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vscodeAPI }) => {
|
||||
const onRecipeClick = (recipeID: string): void => {
|
||||
vscodeAPI.postMessage({ command: 'executeRecipe', recipe: recipeID })
|
||||
}
|
||||
@ -24,11 +24,11 @@ export function Recipes(): React.ReactElement {
|
||||
return (
|
||||
<div className="inner-container">
|
||||
<div className="non-transcript-container">
|
||||
<div className="recipes">
|
||||
<div className={styles.recipes}>
|
||||
{Object.entries(recipesList).map(([key, value]) => (
|
||||
<VSCodeButton
|
||||
key={key}
|
||||
className="recipe-button"
|
||||
className={styles.recipeButton}
|
||||
type="button"
|
||||
onClick={() => onRecipeClick(key)}
|
||||
>
|
||||
|
||||
@ -11,7 +11,7 @@ import { ChatHistory, ChatMessage } from '@sourcegraph/cody-shared/src/chat/tran
|
||||
import { ContextFiles } from '@sourcegraph/cody-ui/src/chat/ContextFiles'
|
||||
|
||||
import { FileLink } from './FileLink'
|
||||
import { vscodeAPI } from './utils/VSCodeApi'
|
||||
import { VSCodeWrapper } from './utils/VSCodeApi'
|
||||
|
||||
import styles from './Chat.module.css'
|
||||
|
||||
@ -19,12 +19,14 @@ interface HistoryProps {
|
||||
userHistory: ChatHistory | null
|
||||
setUserHistory: (history: ChatHistory | null) => void
|
||||
setInputHistory: (inputHistory: string[] | []) => void
|
||||
vscodeAPI: VSCodeWrapper
|
||||
}
|
||||
|
||||
export const UserHistory: React.FunctionComponent<React.PropsWithChildren<HistoryProps>> = ({
|
||||
userHistory,
|
||||
setUserHistory,
|
||||
setInputHistory,
|
||||
vscodeAPI,
|
||||
}) => {
|
||||
const [chatHistory, setChatHistory] = useState('')
|
||||
|
||||
@ -35,7 +37,7 @@ export const UserHistory: React.FunctionComponent<React.PropsWithChildren<Histor
|
||||
setUserHistory(null)
|
||||
setInputHistory([])
|
||||
}
|
||||
}, [setInputHistory, setUserHistory, userHistory])
|
||||
}, [setInputHistory, setUserHistory, userHistory, vscodeAPI])
|
||||
|
||||
return (
|
||||
<div className={styles.innerContainer}>
|
||||
|
||||
@ -6,8 +6,10 @@ import { App } from './App'
|
||||
|
||||
import './index.css'
|
||||
|
||||
import { getVSCodeAPI } from './utils/VSCodeApi'
|
||||
|
||||
ReactDOM.createRoot(document.querySelector('#root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<App vscodeAPI={getVSCodeAPI()} />
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* CSS styles for storybooks to define variables and otherwise resemble the VS Code.
|
||||
*/
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 2rem auto;
|
||||
border: solid 1px #ffffff33;
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
.container a {
|
||||
color: var(--vscode-link-color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define any --vscode-* CSS variables needed. These differ per theme and don't need to be
|
||||
* an exact match for any particular VS Code theme. They just need to be good enough.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--vscode-editor-foreground: var(--foreground);
|
||||
--vscode-editor-background: #151c28;
|
||||
--vscode-inputValidation-errorBorder: #ff0000;
|
||||
--vscode-inputValidation-errorBackground: #ff000033;
|
||||
--vscode-editor-font-size: 13px;
|
||||
--vscode-link-color: #7777ff;
|
||||
--vscode-tab-inactiveBackground: #00000033;
|
||||
--vscode-tab-inactiveForeground: #ffffff77;
|
||||
--vscode-tab-activeForeground: #ffffff;
|
||||
--vscode-sideBarSectionHeader-border: #ffffff33;
|
||||
--vscode-sideBarTitle-foreground: #ffffffcc;
|
||||
}
|
||||
8
client/cody/webviews/storybook/VSCodeStoryDecorator.tsx
Normal file
8
client/cody/webviews/storybook/VSCodeStoryDecorator.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { DecoratorFn } from '@storybook/react'
|
||||
|
||||
import styles from './VSCodeStoryDecorator.module.css'
|
||||
|
||||
/**
|
||||
* A decorator for storybooks that makes them look like they're running in VS Code.
|
||||
*/
|
||||
export const VSCodeStoryDecorator: DecoratorFn = story => <div className={styles.container}>{story()}</div>
|
||||
@ -8,20 +8,26 @@ interface VSCodeApi {
|
||||
postMessage: (message: unknown) => void
|
||||
}
|
||||
|
||||
class VSCodeWrapper {
|
||||
private readonly vscodeApi: VSCodeApi = acquireVsCodeApi()
|
||||
|
||||
public postMessage(message: WebviewMessage): void {
|
||||
this.vscodeApi.postMessage(message)
|
||||
}
|
||||
|
||||
public onMessage(callback: (message: ExtensionMessage) => void): () => void {
|
||||
const listener = (event: MessageEvent<ExtensionMessage>): void => {
|
||||
callback(event.data)
|
||||
}
|
||||
window.addEventListener('message', listener)
|
||||
return () => window.removeEventListener('message', listener)
|
||||
}
|
||||
export interface VSCodeWrapper {
|
||||
postMessage(message: WebviewMessage): void
|
||||
onMessage(callback: (message: ExtensionMessage) => void): () => void
|
||||
}
|
||||
|
||||
export const vscodeAPI: VSCodeWrapper = new VSCodeWrapper()
|
||||
let api: VSCodeWrapper
|
||||
|
||||
export function getVSCodeAPI(): VSCodeWrapper {
|
||||
if (!api) {
|
||||
const vsCodeApi = acquireVsCodeApi()
|
||||
api = {
|
||||
postMessage: message => vsCodeApi.postMessage(message),
|
||||
onMessage: callback => {
|
||||
const listener = (event: MessageEvent<ExtensionMessage>): void => {
|
||||
callback(event.data)
|
||||
}
|
||||
window.addEventListener('message', listener)
|
||||
return () => window.removeEventListener('message', listener)
|
||||
},
|
||||
}
|
||||
}
|
||||
return api
|
||||
}
|
||||
|
||||
@ -43,9 +43,21 @@ const getStoriesGlob = (): string[] => {
|
||||
// Due to an issue with constant recompiling (https://github.com/storybookjs/storybook/issues/14342)
|
||||
// we need to make the globs more specific (`(web|shared..)` also doesn't work). Once the above issue
|
||||
// is fixed, this can be removed and watched for `client/**/*.story.tsx` again.
|
||||
const directoriesWithStories = ['branded', 'browser', 'jetbrains/webview', 'shared', 'web', 'wildcard', 'cody-ui']
|
||||
const directoriesWithStories = [
|
||||
'branded',
|
||||
'browser',
|
||||
'jetbrains/webview',
|
||||
'shared',
|
||||
'web',
|
||||
'wildcard',
|
||||
'cody-ui',
|
||||
'cody',
|
||||
]
|
||||
const storiesGlobs = directoriesWithStories.map(packageDirectory =>
|
||||
path.resolve(ROOT_PATH, `client/${packageDirectory}/src/**/*.story.tsx`)
|
||||
path.resolve(
|
||||
ROOT_PATH,
|
||||
`client/${packageDirectory}/${packageDirectory === 'cody' ? 'webviews' : 'src'}/**/*.story.tsx`
|
||||
)
|
||||
)
|
||||
|
||||
return [...storiesGlobs]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user