diff --git a/client/branded/src/global-styles/colors-redesign.scss b/client/branded/src/global-styles/colors-redesign.scss
index f3b7ec4235d..c6ae64b0b56 100644
--- a/client/branded/src/global-styles/colors-redesign.scss
+++ b/client/branded/src/global-styles/colors-redesign.scss
@@ -35,9 +35,7 @@ $theme-colors-redesign: (
merged: $redesign-purple,
);
-:root.theme-redesign,
-// Descendant selector is required for Storybook `addRedesignVariants` helper.
-:root .theme-redesign {
+.theme-redesign {
--primary: #{map-get($theme-colors-redesign, 'primary')};
--primary-3: #{$redesign-indigo};
--brand-secondary: #a305e1;
diff --git a/client/shared/src/util/addRedesignVariants.tsx b/client/shared/src/util/addRedesignVariants.tsx
deleted file mode 100644
index 265b3b02f01..00000000000
--- a/client/shared/src/util/addRedesignVariants.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React, { ReactElement } from 'react'
-
-import { REDESIGN_CLASS_NAME } from './useRedesignToggle'
-
-/**
- *
- * To rely on screenshot tests for redesigned components in Storybook, this wrapper function
- * renders a copy of the story wrapped into a div with redesign class next to the initial story.
- *
- * @example
- * const { add } = storiesOf('wildcard/PageSelector', module).addDecorator(story => (
- *
- * {() => addRedesignVariants({story()}
)}
- *
- * ))
- *
- */
-export const addRedesignVariants = (story: ReactElement): ReactElement => (
- <>
- {story}
-
-
-
Redesign variant
- {story}
-
- >
-)
diff --git a/client/storybook/src/chromatic-story/Chromatic.story.tsx b/client/storybook/src/chromatic-story/Chromatic.story.tsx
new file mode 100644
index 00000000000..144dae651fc
--- /dev/null
+++ b/client/storybook/src/chromatic-story/Chromatic.story.tsx
@@ -0,0 +1,37 @@
+import { PublishedStoreItem } from '@storybook/client-api'
+import { raw } from '@storybook/react'
+import isChromatic from 'chromatic/isChromatic'
+
+import { addStory } from './add-story'
+
+// Execute logic below only in the environment where Chromatic snapshots are captured.
+if (isChromatic()) {
+ // Get an array of all stories which are already added to the `StoryStore`.
+ // Use `raw()` because we don't want to apply any filtering and sorting on the array of stories.
+ const storeItems = raw() as PublishedStoreItem[]
+
+ // Add three more versions of each story to test visual regressions with Chromatic snapshots.
+ // In other environments, these themes can be explored by a user via toolbar toggles.
+ for (const storeItem of storeItems) {
+ // Default theme + Dark mode.
+ addStory({
+ storeItem,
+ isDarkModeEnabled: true,
+ isRedesignEnabled: false,
+ })
+
+ // Redesign theme + Light mode.
+ addStory({
+ storeItem,
+ isDarkModeEnabled: false,
+ isRedesignEnabled: true,
+ })
+
+ // Redesign theme + Dark mode.
+ addStory({
+ storeItem,
+ isDarkModeEnabled: true,
+ isRedesignEnabled: true,
+ })
+ }
+}
diff --git a/client/storybook/src/chromatic-story/add-story.ts b/client/storybook/src/chromatic-story/add-story.ts
new file mode 100644
index 00000000000..3ce2cc095aa
--- /dev/null
+++ b/client/storybook/src/chromatic-story/add-story.ts
@@ -0,0 +1,60 @@
+import { PublishedStoreItem, StoryStore } from '@storybook/client-api'
+import { toId } from '@storybook/csf'
+
+import { createChromaticStory, CreateChromaticStoryOptions } from './create-chromatic-story'
+
+// This global reference is used internally by Storybook:
+// https://github.com/storybookjs/storybook/blob/3ec358f71c6111838092397d13fbe35b627a9a9d/lib/core-client/src/preview/start.ts#L43
+declare global {
+ interface Window {
+ __STORYBOOK_STORY_STORE__: StoryStore
+ }
+}
+
+// See the discussion about `StoryStore` usage in stories:
+// https://github.com/storybookjs/storybook/discussions/12050#discussioncomment-125658
+const storyStore = window.__STORYBOOK_STORY_STORE__
+
+interface AddStoryOptions extends Pick {
+ storeItem: PublishedStoreItem
+}
+
+export const addStory = (options: AddStoryOptions): void => {
+ const {
+ storeItem: { name, kind, storyFn, parameters },
+ isDarkModeEnabled,
+ isRedesignEnabled,
+ } = options
+
+ // Add suffix to the story name based on theme options:
+ // 1. Default + Dark: "Text" -> "Text 🌚"
+ // 2. Redesign + Light: "Text" -> "Text [Redesign]"
+ // 3. Redesign + Dark: "Text" -> "Text [Redesign] 🌚"
+ const storyName = [name, isRedesignEnabled && '[Redesign]', isDarkModeEnabled && '🌚'].filter(Boolean).join(' ')
+
+ /**
+ * Use `storyStore.addStory()` to avoid applying decorators to stories, because `PublishedStoreItem.storyFn` already has decorators applied.
+ * `storiesOf().add()` usage API would result in decorators duplication. It's possible to avoid this issue using `PublishedStoreItem.getOriginal()`,
+ * which returns only story function without any decorators and story context. It means that we should apply them manually and
+ * keep this logic in sync with Storybook internals to have consistent behavior. `storyStore.addStory()` allows to avoid it.
+ */
+ storyStore.addStory(
+ {
+ id: toId(kind, storyName),
+ kind,
+ name: storyName,
+ parameters,
+ loaders: [],
+ storyFn: createChromaticStory({
+ storyFn,
+ isDarkModeEnabled,
+ isRedesignEnabled,
+ }),
+ },
+ {
+ // The default `applyDecorators` implementation accepts `decorators` as a second arg and applies them to the `storyFn`.
+ // Our `storyFn` already has all the decorators applied, so we just return it.
+ applyDecorators: storyFn => storyFn,
+ }
+ )
+}
diff --git a/client/storybook/src/chromatic-story/create-chromatic-story.tsx b/client/storybook/src/chromatic-story/create-chromatic-story.tsx
new file mode 100644
index 00000000000..17d17a605ce
--- /dev/null
+++ b/client/storybook/src/chromatic-story/create-chromatic-story.tsx
@@ -0,0 +1,47 @@
+import { StoryFn } from '@storybook/addons'
+import React, { ReactElement, useEffect } from 'react'
+import { useDarkMode } from 'storybook-dark-mode'
+
+import { useRedesignToggle } from '@sourcegraph/shared/src/util/useRedesignToggle'
+
+import { THEME_DARK_CLASS, THEME_LIGHT_CLASS } from '../themes'
+
+export interface CreateChromaticStoryOptions {
+ storyFn: StoryFn
+ isRedesignEnabled: boolean
+ isDarkModeEnabled: boolean
+}
+
+// Wrap `storyFn` into a decorator which takes care of CSS classes toggling based on received theme options.
+export const createChromaticStory = (options: CreateChromaticStoryOptions): StoryFn => () => {
+ const { storyFn, isRedesignEnabled, isDarkModeEnabled } = options
+ // The `storyFn` is retrieved from the `StoryStore`, so it already has a `StoryContext`.
+ // We can safely change its type to remove required props `StoryContext` props check.
+ const Story = storyFn as React.ComponentType
+
+ const [, setRedesignToggle] = useRedesignToggle()
+ const isDarkModeEnabledInitially = useDarkMode()
+
+ useEffect(() => {
+ setRedesignToggle(isRedesignEnabled)
+
+ // 'storybook-dark-mode' doesn't expose any API to toggle dark/light theme programmatically, so we do it manually.
+ document.body.classList.toggle(THEME_DARK_CLASS, isDarkModeEnabled)
+ document.body.classList.toggle(THEME_LIGHT_CLASS, !isDarkModeEnabled)
+
+ return () => {
+ // Do not enable redesign theme if it was disabled before this story was opened.
+ if (isRedesignEnabled) {
+ setRedesignToggle(!isRedesignEnabled)
+ }
+
+ // Always toggle dark mode back to the previous value because otherwise, it might be out of sync with the toolbar toggle.
+ document.body.classList.toggle(THEME_DARK_CLASS, isDarkModeEnabledInitially)
+ document.body.classList.toggle(THEME_LIGHT_CLASS, !isDarkModeEnabledInitially)
+ }
+ // We need to execute `useEffect` callback once to take snapshot in Chromatic, so we can omit dependencies here.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ return
+}
diff --git a/client/storybook/src/main.ts b/client/storybook/src/main.ts
index 63b277aa9f9..6f7e7ea430e 100644
--- a/client/storybook/src/main.ts
+++ b/client/storybook/src/main.ts
@@ -7,6 +7,10 @@ import { Configuration, DefinePlugin, ProgressPlugin, RuleSetUseItem, RuleSetUse
const rootPath = path.resolve(__dirname, '../../../')
const monacoEditorPaths = [path.resolve(rootPath, 'node_modules', 'monaco-editor')]
+
+// Stories in this file are guarded by the `isChromatic()` check. It will result in noop in all other environments.
+const chromaticStoriesGlob = path.resolve(rootPath, 'client/storybook/src/chromatic-story/Chromatic.story.tsx')
+
// 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.
@@ -32,7 +36,7 @@ const getCSSLoaders = (...loaders: RuleSetUseItem[]): RuleSetUse => [
]
const config = {
- stories: storiesGlobs,
+ stories: [...storiesGlobs, chromaticStoriesGlob],
addons: [
'@storybook/addon-knobs',
'@storybook/addon-actions',
diff --git a/client/storybook/src/preview.ts b/client/storybook/src/preview.ts
index afcf05797db..ca233eabe8d 100644
--- a/client/storybook/src/preview.ts
+++ b/client/storybook/src/preview.ts
@@ -9,7 +9,7 @@ import { withDesign } from 'storybook-addon-designs'
import { setLinkComponent, AnchorLink } from '@sourcegraph/shared/src/components/Link'
import { getIsRedesignEnabled, REDESIGN_CLASS_NAME } from '@sourcegraph/shared/src/util/useRedesignToggle'
-import * as themes from './themes'
+import { themeDark, themeLight, THEME_DARK_CLASS, THEME_LIGHT_CLASS } from './themes'
const withConsoleDecorator: DecoratorFunction = (storyFn, context): ReactElement =>
withConsole()(storyFn)(context)
@@ -19,10 +19,10 @@ export const decorators = [withDesign, withConsoleDecorator]
export const parameters = {
darkMode: {
stylePreview: true,
- darkClass: 'theme-dark',
- lightClass: 'theme-light',
- light: themes.light,
- dark: themes.dark,
+ lightClass: THEME_LIGHT_CLASS,
+ darkClass: THEME_DARK_CLASS,
+ light: themeLight,
+ dark: themeDark,
},
}
diff --git a/client/storybook/src/redesign-toggle-toolbar/RedesignToggleStorybook.tsx b/client/storybook/src/redesign-toggle-toolbar/RedesignToggleStorybook.tsx
index 970004c248e..900c014c0e0 100644
--- a/client/storybook/src/redesign-toggle-toolbar/RedesignToggleStorybook.tsx
+++ b/client/storybook/src/redesign-toggle-toolbar/RedesignToggleStorybook.tsx
@@ -1,10 +1,12 @@
+import { addons } from '@storybook/addons'
import { Icons, IconButton } from '@storybook/components'
-import React, { ReactElement } from 'react'
+import { SET_STORIES } from '@storybook/core-events'
+import React, { ReactElement, useEffect } from 'react'
import { useRedesignToggle, REDESIGN_CLASS_NAME } from '@sourcegraph/shared/src/util/useRedesignToggle'
const toggleRedesignClass = (element: HTMLElement, isRedesignEnabled: boolean): void => {
- element.classList.toggle(REDESIGN_CLASS_NAME, !isRedesignEnabled)
+ element.classList.toggle(REDESIGN_CLASS_NAME, isRedesignEnabled)
}
const updatePreview = (isRedesignEnabled: boolean): void => {
@@ -29,10 +31,26 @@ const updateManager = (isRedesignEnabled: boolean): void => {
export const RedesignToggleStorybook = (): ReactElement => {
const [isRedesignEnabled, setIsRedesignEnabled] = useRedesignToggle()
+ useEffect(() => {
+ const handleIsRedesignEnabledChange = (): void => {
+ updatePreview(isRedesignEnabled)
+ updateManager(isRedesignEnabled)
+ }
+
+ handleIsRedesignEnabledChange()
+
+ const channel = addons.getChannel()
+ // Preview iframe is not available on toolbar mount.
+ // Wait for the SET_STORIES event, after which the iframe is accessible, and ensure that the redesign-theme class is in place.
+ channel.on(SET_STORIES, handleIsRedesignEnabledChange)
+
+ return () => {
+ channel.removeListener(SET_STORIES, handleIsRedesignEnabledChange)
+ }
+ }, [isRedesignEnabled])
+
const handleRedesignToggle = (): void => {
setIsRedesignEnabled(!isRedesignEnabled)
- updatePreview(isRedesignEnabled)
- updateManager(isRedesignEnabled)
}
return (
diff --git a/client/storybook/src/themes.ts b/client/storybook/src/themes.ts
index d76e37be71d..4d2f1c016d1 100644
--- a/client/storybook/src/themes.ts
+++ b/client/storybook/src/themes.ts
@@ -1,6 +1,9 @@
import { ThemeVars, themes } from '@storybook/theming'
import openColor from 'open-color'
+export const THEME_DARK_CLASS = 'theme-dark'
+export const THEME_LIGHT_CLASS = 'theme-light'
+
// Themes use the colors from our webapp.
const common: Omit = {
colorPrimary: openColor.blue[6],
@@ -13,7 +16,7 @@ const common: Omit = {
fontCode: 'sfmono-regular, consolas, menlo, dejavu sans mono, monospace',
}
-export const dark: ThemeVars = {
+export const themeDark: ThemeVars = {
...themes.dark,
...common,
appBg: '#1c2736',
@@ -25,7 +28,7 @@ export const dark: ThemeVars = {
inputTextColor: '#ffffff',
}
-export const light: ThemeVars = {
+export const themeLight: ThemeVars = {
...themes.light,
...common,
appBg: '#fbfdff',
diff --git a/doc/dev/background-information/renovate.md b/doc/dev/background-information/renovate.md
index c3f9156a987..4f03adac46b 100644
--- a/doc/dev/background-information/renovate.md
+++ b/doc/dev/background-information/renovate.md
@@ -41,8 +41,8 @@ We heavily customize Renovate to save more time. Possible configurations include
- Setting different reviewers for certain dependencies
- Grouping certain dependencies
-- Automerging certain low-risk dependencies
-- Updating certain dependencies out-of-schedule aswell
+- Auto merging certain low-risk dependencies
+- Updating certain dependencies out-of-schedule as well
- Assigning certain labels for easier filtering
If you see an opportunity to improve the configuration, raise a pull request to update the `renovate.json` in the repository or our [configuration shared between repositories](https://github.com/sourcegraph/renovate-config/blob/master/renovate.json).
diff --git a/enterprise/dev/ci/internal/ci/is-storybook-affected.go b/enterprise/dev/ci/internal/ci/is-storybook-affected.go
new file mode 100644
index 00000000000..b8e90333ef3
--- /dev/null
+++ b/enterprise/dev/ci/internal/ci/is-storybook-affected.go
@@ -0,0 +1,63 @@
+package ci
+
+import (
+ "path/filepath"
+ "strings"
+)
+
+// Changes in the files below will be ignored by the Storybook workflow.
+var ignoredRootFiles []string = []string{
+ "jest.config.base.js",
+ "graphql-schema-linter.config.js",
+ "libsqlite3-pcre.dylib",
+ ".mocharc.js",
+ "go.mod",
+ "LICENSE",
+ "renovate.json",
+ "jest.config.js",
+ "LICENSE.apache",
+ ".stylelintrc.json",
+ ".percy.yml",
+ ".tool-versions",
+ "go.sum",
+ ".golangci.yml",
+ ".stylelintignore",
+ ".gitmodules",
+ ".prettierignore",
+ ".editorconfig",
+ "prettier.config.js",
+ ".dockerignore",
+ "doc.go",
+ ".gitignore",
+ ".gitattributes",
+ ".eslintrc.js",
+ "sg.config.yaml",
+ ".eslintignore",
+ ".mailmap",
+ "LICENSE.enterprise",
+ "CODENOTIFY",
+}
+
+func contains(s []string, str string) bool {
+ for _, v := range s {
+ if v == str {
+ return true
+ }
+ }
+
+ return false
+}
+
+func isAllowedRootFile(p string) bool {
+ return filepath.Dir(p) == "." && !contains(ignoredRootFiles, p)
+}
+
+// Run Storybook workflow only if related files were changed.
+func (c Config) isStorybookAffected() bool {
+ for _, p := range c.changedFiles {
+ if !strings.HasSuffix(p, ".md") && (strings.HasPrefix(p, "client/") || isAllowedRootFile(p)) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/enterprise/dev/ci/internal/ci/pipeline-steps.go b/enterprise/dev/ci/internal/ci/pipeline-steps.go
index aab872ded43..70c4ab5b502 100644
--- a/enterprise/dev/ci/internal/ci/pipeline-steps.go
+++ b/enterprise/dev/ci/internal/ci/pipeline-steps.go
@@ -90,17 +90,19 @@ func addSharedTests(c Config) func(pipeline *bk.Pipeline) {
bk.Cmd("dev/ci/codecov.sh -c -F typescript -F integration"),
bk.ArtifactPaths("./puppeteer/*.png"))
- // Upload storybook to Chromatic
- chromaticCommand := "yarn chromatic --exit-zero-on-changes --exit-once-uploaded"
- if !c.isPR() {
- chromaticCommand += " --auto-accept-changes"
+ if c.isMasterDryRun || c.isStorybookAffected() {
+ // Upload storybook to Chromatic
+ chromaticCommand := "yarn chromatic --exit-zero-on-changes --exit-once-uploaded"
+ if !c.isPR() {
+ chromaticCommand += " --auto-accept-changes"
+ }
+ pipeline.AddStep(":chromatic: Upload storybook to Chromatic",
+ bk.AutomaticRetry(5),
+ bk.Cmd("yarn --mutex network --frozen-lockfile --network-timeout 60000"),
+ bk.Cmd("yarn gulp generate"),
+ bk.Env("MINIFY", "1"),
+ bk.Cmd(chromaticCommand))
}
- pipeline.AddStep(":chromatic: Upload storybook to Chromatic",
- bk.AutomaticRetry(5),
- bk.Cmd("yarn --mutex network --frozen-lockfile --network-timeout 60000"),
- bk.Cmd("yarn gulp generate"),
- bk.Env("MINIFY", "1"),
- bk.Cmd(chromaticCommand))
// Shared tests
pipeline.AddStep(":jest: Test shared client code",
diff --git a/package.json b/package.json
index 34aeb005b74..464d5ac3cec 100644
--- a/package.json
+++ b/package.json
@@ -121,6 +121,7 @@
"@storybook/addon-toolbars": "^6.2.9",
"@storybook/addons": "^6.2.9",
"@storybook/components": "^6.2.9",
+ "@storybook/client-api": "^6.2.9",
"@storybook/core": "^6.2.9",
"@storybook/react": "^6.2.9",
"@storybook/theming": "^6.2.9",
diff --git a/renovate.json b/renovate.json
index acc7e9901f7..984d40204d9 100644
--- a/renovate.json
+++ b/renovate.json
@@ -2,6 +2,7 @@
"$schema": "http://json.schemastore.org/renovate",
"extends": ["github>sourcegraph/renovate-config"],
"semanticCommits": false,
+ "rebaseWhen": "never",
"packageRules": [
{
"matchDepTypes": ["engines"],
diff --git a/yarn.lock b/yarn.lock
index ca71abd95f8..7671beb0f1e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3275,7 +3275,7 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
-"@storybook/client-api@6.2.9":
+"@storybook/client-api@6.2.9", "@storybook/client-api@^6.2.9":
version "6.2.9"
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.2.9.tgz#f0bb44e9b2692adfbf30d7ff751c6dd44bcfe1ce"
integrity sha512-aLvEUVkbvv6Qo/2mF4rFCecdqi2CGOUDdsV1a6EFIVS/9gXFdpirsOwKHo9qNjacGdWPlBYGCUcbrw+DvNaSFA==