Skip to content

Commit

Permalink
Merge pull request #97 from chromaui/ghengeveld/ap-3446-dont-show-acc…
Browse files Browse the repository at this point in the history
…ept-button-if-user-doesnt-have-review

Show eyebrow and lock review button when user has no `REVIEWER` permission
  • Loading branch information
tmeasday authored Sep 14, 2023
2 parents 99b5a1e + 0da4dcd commit 23ca833
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 13 deletions.
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

0 comments on commit 23ca833

Please sign in to comment.