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 eyebrow and lock review button when user has no REVIEWER permission #97

Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions src/components/Eyebrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { styled } from "@storybook/theming";

export const Eyebrow = styled.div(({ theme }) => ({
background: theme.background.app,
borderBottom: `1px solid ${theme.appBorderColor}`,
padding: "10px 15px",
lineHeight: "20px",
color: theme.color.defaultText,
}));
4 changes: 2 additions & 2 deletions src/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
const documents = {
"\n query SelectProjectsQuery {\n viewer {\n accounts {\n id\n name\n avatarUrl\n projects {\n id\n name\n webUrl\n projectToken\n lastBuild {\n branch\n number\n }\n }\n }\n }\n }\n": types.SelectProjectsQueryDocument,
"\n query ProjectQuery($projectId: ID!) {\n project(id: $projectId) {\n id\n name\n webUrl\n lastBuild {\n branch\n number\n }\n }\n }\n": types.ProjectQueryDocument,
"\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $slug: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $storyBuildId: ID!\n $hasStoryBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n nextBuild: lastBuild(\n branches: [$branch]\n slug: $slug\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...NextBuildFields\n ...StoryBuildFields @skip(if: $hasStoryBuildId)\n }\n }\n storyBuild: build(id: $storyBuildId) @include(if: $hasStoryBuildId) {\n ...StoryBuildFields\n }\n }\n": types.AddonVisualTestsBuildDocument,
"\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $slug: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $storyBuildId: ID!\n $hasStoryBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n nextBuild: lastBuild(\n branches: [$branch]\n slug: $slug\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...NextBuildFields\n ...StoryBuildFields @skip(if: $hasStoryBuildId)\n }\n }\n storyBuild: build(id: $storyBuildId) @include(if: $hasStoryBuildId) {\n ...StoryBuildFields\n }\n viewer {\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n": types.AddonVisualTestsBuildDocument,
"\n fragment NextBuildFields on Build {\n __typename\n id\n committedAt\n ... on StartedBuild {\n testsForStatus: tests(first: 1000, statuses: $testStatuses) {\n nodes {\n ...StatusTestFields\n }\n }\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...NextStoryTestFields\n }\n }\n }\n ... on CompletedBuild {\n result\n testsForStatus: tests(statuses: $testStatuses) {\n nodes {\n ...StatusTestFields\n }\n }\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...NextStoryTestFields\n }\n }\n }\n }\n": types.NextBuildFieldsFragmentDoc,
"\n fragment StoryBuildFields on Build {\n __typename\n id\n number\n branch\n committedAt\n uncommittedHash\n status\n ... on StartedBuild {\n startedAt\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...StoryTestFields\n }\n }\n }\n ... on CompletedBuild {\n startedAt\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...StoryTestFields\n }\n }\n }\n }\n": types.StoryBuildFieldsFragmentDoc,
"\n fragment StatusTestFields on Test {\n id\n status\n story {\n storyId\n }\n }\n": types.StatusTestFieldsFragmentDoc,
Expand Down Expand Up @@ -49,7 +49,7 @@ export function graphql(source: "\n query ProjectQuery($projectId: ID!) {\n
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $slug: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $storyBuildId: ID!\n $hasStoryBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n nextBuild: lastBuild(\n branches: [$branch]\n slug: $slug\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...NextBuildFields\n ...StoryBuildFields @skip(if: $hasStoryBuildId)\n }\n }\n storyBuild: build(id: $storyBuildId) @include(if: $hasStoryBuildId) {\n ...StoryBuildFields\n }\n }\n"): (typeof documents)["\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $slug: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $storyBuildId: ID!\n $hasStoryBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n nextBuild: lastBuild(\n branches: [$branch]\n slug: $slug\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...NextBuildFields\n ...StoryBuildFields @skip(if: $hasStoryBuildId)\n }\n }\n storyBuild: build(id: $storyBuildId) @include(if: $hasStoryBuildId) {\n ...StoryBuildFields\n }\n }\n"];
export function graphql(source: "\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $slug: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $storyBuildId: ID!\n $hasStoryBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n nextBuild: lastBuild(\n branches: [$branch]\n slug: $slug\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...NextBuildFields\n ...StoryBuildFields @skip(if: $hasStoryBuildId)\n }\n }\n storyBuild: build(id: $storyBuildId) @include(if: $hasStoryBuildId) {\n ...StoryBuildFields\n }\n viewer {\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n"): (typeof documents)["\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $slug: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $storyBuildId: ID!\n $hasStoryBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n nextBuild: lastBuild(\n branches: [$branch]\n slug: $slug\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...NextBuildFields\n ...StoryBuildFields @skip(if: $hasStoryBuildId)\n }\n }\n storyBuild: build(id: $storyBuildId) @include(if: $hasStoryBuildId) {\n ...StoryBuildFields\n }\n viewer {\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
39 changes: 37 additions & 2 deletions src/gql/graphql.ts

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions src/gql/public-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,9 @@ type Query {
type Account implements Node & Temporal {
"""GraphQL node identifier"""
id: ID!

"""Is this a personal account."""
isPersonal: Boolean!
avatarUrl: String

"""Account name, typically the repository owner."""
Expand Down Expand Up @@ -1537,6 +1540,7 @@ type User implements Node {
username: String!
avatarUrl: URL
accounts: [Account!]!
projectMembership(projectId: ID!): ProjectMembership

"""When the entity was first created in Chromatic."""
createdAt: DateTime!
Expand All @@ -1545,6 +1549,29 @@ type User implements Node {
updatedAt: DateTime!
}

type ProjectMembership {
"""GraphQL node identifier"""
id: ID!
user: User
project: Project!
accessLevel: AccessLevel!
meetsAccessLevel(minimumAccessLevel: AccessLevel!): Boolean!

"""When the entity was first created in Chromatic."""
createdAt: DateTime!

"""When the entity was last updated or created in Chromatic."""
updatedAt: DateTime!
}

enum AccessLevel {
OWNER
DEVELOPER
REVIEWER
VIEWER
NONE
}

"""A MongoDB ObjectId."""
scalar ObjID

Expand Down
33 changes: 29 additions & 4 deletions src/screens/VisualTests/BuildResults.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Icons, TooltipNote, WithTooltip } from "@storybook/components";
import { Icons, Link, TooltipNote, WithTooltip } from "@storybook/components";
import React, { useState } from "react";

import { Button } from "../../components/Button";
import { Container } from "../../components/Container";
import { Eyebrow } from "../../components/Eyebrow";
import { FooterMenu } from "../../components/FooterMenu";
import { Heading } from "../../components/Heading";
import { IconButton } from "../../components/IconButton";
Expand Down Expand Up @@ -32,6 +33,7 @@ interface BuildResultsProps {
nextBuild: NextBuildFieldsFragment;
switchToNextBuild?: () => void;
startDevBuild: () => void;
userCanReview: boolean;
isReviewing: boolean;
onAccept: (testId: string, batch: ReviewTestBatch) => Promise<void>;
onUnaccept: (testId: string) => Promise<void>;
Expand All @@ -44,6 +46,7 @@ export const BuildResults = ({
nextBuild,
switchToNextBuild,
startDevBuild,
userCanReview,
isReviewing,
onAccept,
onUnaccept,
Expand Down Expand Up @@ -120,18 +123,39 @@ export const BuildResults = ({
);
}

const { status } = storyBuild;
const startedAt = "startedAt" in storyBuild && storyBuild.startedAt;
const isStoryBuildStarting = [
BuildStatus.Announced,
BuildStatus.Published,
BuildStatus.Prepared,
].includes(storyBuild.status);
const startedAt = "startedAt" in storyBuild && storyBuild.startedAt;
const isBuildFailed = storyBuild.status === BuildStatus.Failed;
].includes(status);
const isBuildFailed = status === BuildStatus.Failed;
const isReviewLocked = status === BuildStatus.Pending && (!userCanReview || !isReviewable);

return (
<Sections>
{buildStatus}

{!buildStatus && isReviewLocked && (
<Eyebrow>
{userCanReview ? (
<>Reviewing is disabled because there's a newer build on {branch}.</>
) : (
<>
You do not have permission to accept changes.{" "}
<Link
href="https://www.chromatic.com/docs/collaborators#roles"
target="_blank"
withArrow
>
Learn about roles
</Link>
</>
)}
</Eyebrow>
)}

<Section grow hidden={settingsVisible || warningsVisible}>
<StoryInfo
{...{
Expand All @@ -148,6 +172,7 @@ export const BuildResults = ({
<SnapshotComparison
{...{
tests: storyTests,
userCanReview,
isReviewable,
isReviewing,
onAccept,
Expand Down
1 change: 1 addition & 0 deletions src/screens/VisualTests/SnapshotComparison.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const meta = {
{ status: TestStatus.Passed, viewport: 1200 },
],
}),
userCanReview: true,
isReviewable: true,
isReviewing: false,
onAccept: action("onAccept"),
Expand Down
14 changes: 9 additions & 5 deletions src/screens/VisualTests/SnapshotComparison.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const StackTrace = styled.div(({ theme }) => ({

interface SnapshotSectionProps {
tests: StoryTestFieldsFragment[];
userCanReview: boolean;
isReviewable: boolean;
isReviewing: boolean;
baselineImageVisible: boolean;
Expand All @@ -50,6 +51,7 @@ interface SnapshotSectionProps {

export const SnapshotComparison = ({
tests,
userCanReview,
isReviewable,
isReviewing,
onAccept,
Expand Down Expand Up @@ -118,19 +120,21 @@ export const SnapshotComparison = ({
</Col>
)}

{(isAcceptable || isUnacceptable) && !isReviewable && (
{(isAcceptable || isUnacceptable) && (!isReviewable || !userCanReview) && (
<Col push>
<WithTooltip
tooltip={<TooltipNote note="This snapshot is outdated so you cannot accept it" />}
tooltip={<TooltipNote note="Reviewing disabled" />}
trigger="hover"
hasChrome={false}
>
<Icons icon="lock" />
<IconButton as="span">
<Icons icon="lock" />
</IconButton>
</WithTooltip>
</Col>
)}

{isAcceptable && isReviewable && (
{userCanReview && isReviewable && isAcceptable && (
<>
<Col push>
<WithTooltip
Expand Down Expand Up @@ -190,7 +194,7 @@ export const SnapshotComparison = ({
</Col>
</>
)}
{isUnacceptable && isReviewable && (
{userCanReview && isReviewable && isUnacceptable && (
<>
<Col push>
<WithTooltip
Expand Down
34 changes: 34 additions & 0 deletions src/screens/VisualTests/VisualTests.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,23 @@ const withGraphQLMutation = (...args: Parameters<typeof graphql.mutation>) => ({
const withBuilds = ({
nextBuild,
storyBuild,
userCanReview = true,
}: {
storyBuild?: StoryBuildFieldsFragment;
nextBuild?: NextBuildFieldsFragment;
userCanReview?: boolean;
}) => {
return withGraphQLQueryResult(QueryBuild, {
project: {
name: "acme",
nextBuild: nextBuild || storyBuild,
},
storyBuild,
viewer: {
projectMembership: {
userCanReview,
},
},
});
};

Expand Down Expand Up @@ -380,6 +387,33 @@ export const Pending: Story = {
},
};

export const NoPermission: Story = {
parameters: {
...withBuilds({ storyBuild: pendingBuild, userCanReview: false }),
...withFigmaDesign(
"https://www.figma.com/file/GFEbCgCVDtbZhngULbw2gP/Visual-testing-in-Storybook?type=design&node-id=2127-449276&mode=design&t=gIM40WT0324ynPQD-4"
),
},
};

export const NoPermissionRunning: Story = {
parameters: {
...withBuilds({ storyBuild: inProgressBuild, userCanReview: false }),
...withFigmaDesign(
"https://www.figma.com/file/GFEbCgCVDtbZhngULbw2gP/Visual-testing-in-Storybook?type=design&node-id=2127-449276&mode=design&t=gIM40WT0324ynPQD-4"
),
},
};

export const NoPermissionNoChanges: Story = {
parameters: {
...withBuilds({ storyBuild: passedBuild, userCanReview: false }),
...withFigmaDesign(
"https://www.figma.com/file/GFEbCgCVDtbZhngULbw2gP/Visual-testing-in-Storybook?type=design&node-id=2127-449276&mode=design&t=gIM40WT0324ynPQD-4"
),
},
};

export const ToggleSnapshot: Story = {
parameters: {
...withBuilds({ storyBuild: pendingBuild }),
Expand Down
3 changes: 3 additions & 0 deletions src/screens/VisualTests/VisualTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export const VisualTests = ({
return () => clearInterval(interval);
}, [rerun]);

const { userCanReview } = data?.viewer?.projectMembership || {};

const [{ fetching: isReviewing }, reviewTest] = useMutation(MutationReviewTest);

const onReview = useCallback(
Expand Down Expand Up @@ -203,6 +205,7 @@ export const VisualTests = ({
nextBuild,
switchToNextBuild: canSwitchToNextBuild && switchToNextBuild,
startDevBuild,
userCanReview,
isReviewing,
onAccept,
onUnaccept,
Expand Down
5 changes: 5 additions & 0 deletions src/screens/VisualTests/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export const QueryBuild = graphql(/* GraphQL */ `
storyBuild: build(id: $storyBuildId) @include(if: $hasStoryBuildId) {
...StoryBuildFields
}
viewer {
projectMembership(projectId: $projectId) {
userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)
}
}
}
`);

Expand Down
Loading