Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show instructional UI if main.js updating fails #41

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"chromatic": "^6.22.0",
"date-fns": "^2.30.0",
"pluralize": "^8.0.0",
"ts-dedent": "^2.2.0",
"urql": "^4.0.3",
"uuid": "^9.0.0"
},
Expand Down
25 changes: 22 additions & 3 deletions src/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "./constants";
import { Authentication } from "./screens/Authentication/Authentication";
import { LinkedProject } from "./screens/LinkProject/LinkedProject";
import { LinkingProjectFailed } from "./screens/LinkProject/LinkingProjectFailed";
import { LinkProject } from "./screens/LinkProject/LinkProject";
import { VisualTests } from "./screens/VisualTests/VisualTests";
import { client, Provider, useAccessToken } from "./utils/graphQLClient";
Expand Down Expand Up @@ -68,7 +69,15 @@ export const Panel = ({ active }: PanelProps) => {
},
[api]
);
const [projectId, updateProject, projectIdChanged, clearProjectIdChanged] = useProjectId();
const [
projectId,
projectToken,
configDir,
updateProject,
updatingProjectFailed,
projectIdUpdated,
clearProjectIdUpdated,
] = useProjectId();

// Render a hidden element when the addon panel is not active.
// Storybook's AddonPanel component does the same but it's not styleable so we don't use it.
Expand All @@ -84,12 +93,22 @@ export const Panel = ({ active }: PanelProps) => {
</Provider>
);

if (projectIdChanged) {
if (updatingProjectFailed) {
return (
<LinkingProjectFailed
projectId={projectId}
projectToken={projectToken}
configDir={configDir}
/>
);
}

if (projectIdUpdated) {
return (
<Provider key={PANEL_ID} value={client}>
<LinkedProject
projectId={projectId}
goToNext={clearProjectIdChanged}
goToNext={clearProjectIdUpdated}
setAccessToken={setAccessToken}
/>
</Provider>
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ export type UpdateProjectPayload = {
};

export const PROJECT_UPDATED = `${ADDON_ID}/projectUpdated`;
export const PROJECT_UPDATING_FAILED = `${ADDON_ID}/projectUpdatingFailed`;
export type ProjectUpdatingFailedPayload = {
configDir: string;
};
11 changes: 11 additions & 0 deletions src/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export type CompletedBuildTestsArgs = {
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
orderBy?: InputMaybe<CompletedBuildTestsOrder>;
statuses?: InputMaybe<Array<TestStatus>>;
storyId?: InputMaybe<Scalars['String']['input']>;
};

Expand Down Expand Up @@ -556,6 +557,13 @@ export type Image = {
imageWidth: Scalars['Int']['output'];
};

export type LocalBuildsSpecifierInput = {
/** If set, only return builds with isLocalBuild set to this value */
isLocalBuild?: InputMaybe<Scalars['Boolean']['input']>;
/** If set, return all global builds, and only local builds from this email hash */
localBuildEmailHash?: InputMaybe<Scalars['String']['input']>;
};

export type Mutation = {
__typename?: 'Mutation';
bulkCreateFigmaMetadata: Array<FigmaMetadata>;
Expand Down Expand Up @@ -683,6 +691,7 @@ export type PreparedBuildTestsArgs = {
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
orderBy?: InputMaybe<PreparedBuildTestsOrder>;
statuses?: InputMaybe<Array<TestStatus>>;
storyId?: InputMaybe<Scalars['String']['input']>;
};

Expand Down Expand Up @@ -753,6 +762,7 @@ export type ProjectBranchNamesArgs = {
export type ProjectLastBuildArgs = {
branches?: InputMaybe<Array<Scalars['String']['input']>>;
defaultBranch?: InputMaybe<Scalars['Boolean']['input']>;
localBuilds?: InputMaybe<LocalBuildsSpecifierInput>;
results?: InputMaybe<Array<BuildResult>>;
slug?: InputMaybe<Scalars['String']['input']>;
statuses?: InputMaybe<Array<BuildStatus>>;
Expand Down Expand Up @@ -956,6 +966,7 @@ export type StartedBuildTestsArgs = {
first?: InputMaybe<Scalars['Int']['input']>;
last?: InputMaybe<Scalars['Int']['input']>;
orderBy?: InputMaybe<StartedBuildTestsOrder>;
statuses?: InputMaybe<Array<TestStatus>>;
storyId?: InputMaybe<Scalars['String']['input']>;
};

Expand Down
39 changes: 16 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
/* eslint-disable no-console */
import type { Channel } from "@storybook/channels";
import { readConfig, writeConfig } from "@storybook/csf-tools";
// eslint-disable-next-line import/no-unresolved
import { getGitInfo, GitInfo, run } from "chromatic/node";
import { relative } from "path";

import {
BUILD_ANNOUNCED,
BUILD_STARTED,
CHROMATIC_ADDON_NAME,
CHROMATIC_BASE_URL,
GIT_INFO,
PROJECT_UPDATED,
PROJECT_UPDATING_FAILED,
ProjectUpdatingFailedPayload,
START_BUILD,
UPDATE_PROJECT,
UpdateProjectPayload,
} from "./constants";
import { findConfig } from "./utils/storybook.config.utils";
import { updateMain } from "./utils/updateMain";

/**
* to load the built addon in this test Storybook
Expand Down Expand Up @@ -46,7 +48,7 @@ const observeGitInfo = async (

async function serverChannel(
channel: Channel,
{ projectToken: initialProjectToken }: { projectToken: string }
{ projectToken: initialProjectToken, configDir }: { projectToken: string; configDir: string }
) {
let projectToken = initialProjectToken;
channel.on(START_BUILD, async () => {
Expand Down Expand Up @@ -81,25 +83,16 @@ async function serverChannel(
async ({ projectId, projectToken: updatedProjectToken }: UpdateProjectPayload) => {
projectToken = updatedProjectToken;

const mainPath = await findConfig("main");
const MainConfig = await readConfig(mainPath);

const addonsConfig = MainConfig.getFieldValue(["addons"]);
const updatedAddonsConfig = addonsConfig.map(
(addonConfig: string | { name: string; options?: Record<string, string> }) => {
const fullConfig = typeof addonConfig === "string" ? { name: addonConfig } : addonConfig;
if (fullConfig.name === CHROMATIC_ADDON_NAME) {
return {
...fullConfig,
options: { projectId, projectToken, ...fullConfig.options },
};
}
return addonConfig;
}
);

MainConfig.setFieldValue(["addons"], updatedAddonsConfig);
await writeConfig(MainConfig);
try {
await updateMain({ projectId, projectToken });
channel.emit(PROJECT_UPDATED);
} catch (err) {
console.warn(`Failed to update your main configuration:\n\n ${err}`);
const relativeConfigDir = relative(process.cwd(), configDir);
channel.emit(PROJECT_UPDATING_FAILED, {
configDir: relativeConfigDir,
} satisfies ProjectUpdatingFailedPayload);
}
}
);

Expand Down
17 changes: 17 additions & 0 deletions src/screens/LinkProject/LinkingProjectFailed.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from "@storybook/react";

import { LinkingProjectFailed } from "./LinkingProjectFailed";

const meta = {
component: LinkingProjectFailed,
args: {
projectId: "Project:abc123",
projectToken: "xzy789",
configDir: ".storybook",
},
} satisfies Meta<typeof LinkingProjectFailed>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
62 changes: 62 additions & 0 deletions src/screens/LinkProject/LinkingProjectFailed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Code, Link } from "@storybook/components";
import React from "react";
import { dedent } from "ts-dedent";

import { Container } from "../../components/Container";
import { Heading } from "../../components/Heading";
import { Section, Sections, Text } from "../../components/layout";
import { Text as CenterText } from "../../components/Text";

type LinkingProjectFailedProps = {
projectId: string;
projectToken: string;
configDir: string;
};

const addonName = "@chromaui/addon-visual-tests";
const configureDocsLink = "https://www.chromatic.com/docs/addon-visual-tests#configure";

export function LinkingProjectFailed({
projectId,
projectToken,
configDir,
}: LinkingProjectFailedProps) {
return (
<Sections>
<Section grow>
<Container>
<CenterText>
<Heading>Add the Project ID to your Storybook config</Heading>
The Project ID will be used to reference prior tests. Please commit this change to
continue using this addon.
</CenterText>
<Code>
{dedent`
// ${configDir}/main.js|ts|tsx

module.exports = {
// ...,
addons: [
// ...
{
name: '${addonName}',
options: {
projectId: '${projectId}',
projectToken: '${projectToken}',
},
},
],
};
`}
</Code>
<CenterText>
What is this for?{" "}
<Link target="_new" href={configureDocsLink}>
Learn more ≫
</Link>
</CenterText>
</Container>
</Section>
</Sections>
);
}
32 changes: 32 additions & 0 deletions src/utils/updateMain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { readConfig, writeConfig } from "@storybook/csf-tools";

import { CHROMATIC_ADDON_NAME } from "../constants";
import { findConfig } from "./storybook.config.utils";

export async function updateMain({
projectId,
projectToken,
}: {
projectId: string;
projectToken: string;
}) {
const mainPath = await findConfig("main");
const MainConfig = await readConfig(mainPath);

const addonsConfig = MainConfig.getFieldValue(["addons"]);
const updatedAddonsConfig = addonsConfig.map(
(addonConfig: string | { name: string; options?: Record<string, string> }) => {
const fullConfig = typeof addonConfig === "string" ? { name: addonConfig } : addonConfig;
if (fullConfig.name === CHROMATIC_ADDON_NAME) {
return {
...fullConfig,
options: { projectId, projectToken, ...fullConfig.options },
};
}
return addonConfig;
}
);

MainConfig.setFieldValue(["addons"], updatedAddonsConfig);
await writeConfig(MainConfig);
}
47 changes: 38 additions & 9 deletions src/utils/useProjectId.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,54 @@
import { useChannel } from "@storybook/manager-api";
import React from "react";

import { UPDATE_PROJECT, UpdateProjectPayload } from "../constants";
import {
PROJECT_UPDATED,
PROJECT_UPDATING_FAILED,
ProjectUpdatingFailedPayload,
UPDATE_PROJECT,
UpdateProjectPayload,
} from "../constants";

const { CHROMATIC_PROJECT_ID } = process.env;

export const useProjectId = (): [
projectId: string,
projectToken: string,
configDir: string,
updateProject: (projectId: string, projectToken?: string) => void,
projectIdChanged: boolean,
clearProjectIdChanged: () => void
projectUpdatingFailed: boolean,
projectIdUpdated: boolean,
clearProjectIdUpdated: () => void
] => {
const [projectId, setProjectId] = React.useState<string | null>(CHROMATIC_PROJECT_ID);
const [projectIdChanged, setProjectIdChanged] = React.useState(false);
const [projectToken, setProjectToken] = React.useState<string | null>();
const [projectIdUpdated, setProjectIdUpdated] = React.useState(false);
const [projectUpdatingFailed, setProjectUpdatingFailed] = React.useState(false);
const [configDir, setConfigDir] = React.useState<string | null>();

const emit = useChannel({});
const emit = useChannel({
[PROJECT_UPDATED]: () => setProjectIdUpdated(true),
[PROJECT_UPDATING_FAILED]: (payload: ProjectUpdatingFailedPayload) => {
setProjectUpdatingFailed(true);
setConfigDir(payload.configDir);
},
});

const updateProject = (newProjectId: string, projectToken: string) => {
emit(UPDATE_PROJECT, { projectId: newProjectId, projectToken } as UpdateProjectPayload);
const updateProject = (newProjectId: string, newProjectToken: string) => {
emit(UPDATE_PROJECT, {
projectId: newProjectId,
projectToken: newProjectToken,
} as UpdateProjectPayload);
setProjectId(newProjectId);
setProjectIdChanged(true);
setProjectToken(newProjectToken);
};
return [projectId, updateProject, projectIdChanged, () => setProjectIdChanged(false)];
return [
projectId,
projectToken,
configDir,
updateProject,
projectUpdatingFailed,
tmeasday marked this conversation as resolved.
Show resolved Hide resolved
projectIdUpdated,
() => setProjectIdUpdated(false),
];
};
Loading