From 4a28f25347addf05708cdff37ecace4139f01779 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Thu, 18 Jun 2020 02:52:33 +0100 Subject: [PATCH] Add support for read-only mode for web UI. Fixes #402. --- commands/webui.go | 11 +++++--- doc/man/git-bug-webui.1 | 4 +++ doc/md/git-bug_webui.md | 9 ++++--- graphql/config/config.go | 7 +++++ graphql/graphql_test.go | 3 ++- graphql/handler.go | 5 ++-- graphql/resolvers/mutation.go | 24 +++++++++++++++++ graphql/resolvers/repo.go | 8 ++++-- graphql/resolvers/root.go | 14 +++++++--- misc/bash_completion/git-bug | 2 ++ misc/powershell_completion/git-bug | 1 + misc/zsh_completion/git-bug | 3 ++- webui/src/layout/CurrentIdentity.tsx | 30 +++++++++++++-------- webui/src/layout/CurrentIdentityContext.tsx | 6 +++++ webui/src/layout/ReadonlyHidden.tsx | 19 +++++++++++++ webui/src/layout/index.tsx | 6 +++-- webui/src/pages/bug/Bug.tsx | 9 ++++--- webui/src/pages/bug/CommentForm.graphql | 4 ++- 18 files changed, 130 insertions(+), 35 deletions(-) create mode 100644 graphql/config/config.go create mode 100644 webui/src/layout/CurrentIdentityContext.tsx create mode 100644 webui/src/layout/ReadonlyHidden.tsx diff --git a/commands/webui.go b/commands/webui.go index 5d3d4b4acb53986016093e4f1f19dd6f60e27eba..7a0fb2cdc65a1d203a8f6d79ea65fdb39eb3efe1 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -19,15 +19,17 @@ import ( "github.com/spf13/cobra" "github.com/MichaelMure/git-bug/graphql" + "github.com/MichaelMure/git-bug/graphql/config" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/webui" ) var ( - webUIPort int - webUIOpen bool - webUINoOpen bool + webUIPort int + webUIOpen bool + webUINoOpen bool + webUIReadOnly bool ) const webUIOpenConfigKey = "git-bug.webui.open" @@ -46,7 +48,7 @@ func runWebUI(cmd *cobra.Command, args []string) error { router := mux.NewRouter() - graphqlHandler, err := graphql.NewHandler(repo) + graphqlHandler, err := graphql.NewHandler(repo, config.Config{ReadOnly: webUIReadOnly}) if err != nil { return err } @@ -261,5 +263,6 @@ func init() { webUICmd.Flags().BoolVar(&webUIOpen, "open", false, "Automatically open the web UI in the default browser") webUICmd.Flags().BoolVar(&webUINoOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser") webUICmd.Flags().IntVarP(&webUIPort, "port", "p", 0, "Port to listen to (default is random)") + webUICmd.Flags().BoolVar(&webUIReadOnly, "read-only", false, "Whether to run the web UI in read-only mode") } diff --git a/doc/man/git-bug-webui.1 b/doc/man/git-bug-webui.1 index 62d2e5dc34b75c7c2f72d043fdd35fe244697018..3fc6fc33a99a935279e95a62bc15a86860e05b1b 100644 --- a/doc/man/git-bug-webui.1 +++ b/doc/man/git-bug-webui.1 @@ -34,6 +34,10 @@ Available git config: \fB\-p\fP, \fB\-\-port\fP=0 Port to listen to (default is random) +.PP +\fB\-\-read\-only\fP[=false] + Whether to run the web UI in read\-only mode + .PP \fB\-h\fP, \fB\-\-help\fP[=false] help for webui diff --git a/doc/md/git-bug_webui.md b/doc/md/git-bug_webui.md index f3ab724ac353498d4adc5945584b21c82830a5f3..98a61eb2b8c6fdb5d49e504b77f6704d63dee7a9 100644 --- a/doc/md/git-bug_webui.md +++ b/doc/md/git-bug_webui.md @@ -17,10 +17,11 @@ git-bug webui [flags] ### Options ``` - --open Automatically open the web UI in the default browser - --no-open Prevent the automatic opening of the web UI in the default browser - -p, --port int Port to listen to (default is random) - -h, --help help for webui + --open Automatically open the web UI in the default browser + --no-open Prevent the automatic opening of the web UI in the default browser + -p, --port int Port to listen to (default is random) + --read-only Whether to run the web UI in read-only mode + -h, --help help for webui ``` ### SEE ALSO diff --git a/graphql/config/config.go b/graphql/config/config.go new file mode 100644 index 0000000000000000000000000000000000000000..2e5f11b37e0b2247f27812ffafe5ffd6ee99af1d --- /dev/null +++ b/graphql/config/config.go @@ -0,0 +1,7 @@ +// Package config contains configuration for GraphQL stuff. +package config + +// Config holds configuration elements. +type Config struct { + ReadOnly bool +} diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go index 0ff2c3fbc247e9c1c6e5d6f5632a953a8ef787f7..7c1ae66bb62fae5d24f3de638cceafd6a5e35395 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -5,6 +5,7 @@ import ( "github.com/99designs/gqlgen/client" + "github.com/MichaelMure/git-bug/graphql/config" "github.com/MichaelMure/git-bug/graphql/models" "github.com/MichaelMure/git-bug/misc/random_bugs" "github.com/MichaelMure/git-bug/repository" @@ -16,7 +17,7 @@ func TestQueries(t *testing.T) { random_bugs.FillRepoWithSeed(repo, 10, 42) - handler, err := NewHandler(repo) + handler, err := NewHandler(repo, config.Config{}) if err != nil { t.Fatal(err) } diff --git a/graphql/handler.go b/graphql/handler.go index 55ef6fc49896f029bf35175550a6c3245c2c08bd..a1be7352f873a024efab54e66379e1b8a037ea04 100644 --- a/graphql/handler.go +++ b/graphql/handler.go @@ -8,6 +8,7 @@ import ( "github.com/99designs/gqlgen/graphql/handler" + "github.com/MichaelMure/git-bug/graphql/config" "github.com/MichaelMure/git-bug/graphql/graph" "github.com/MichaelMure/git-bug/graphql/resolvers" "github.com/MichaelMure/git-bug/repository" @@ -19,9 +20,9 @@ type Handler struct { *resolvers.RootResolver } -func NewHandler(repo repository.ClockedRepo) (Handler, error) { +func NewHandler(repo repository.ClockedRepo, cfg config.Config) (Handler, error) { h := Handler{ - RootResolver: resolvers.NewRootResolver(), + RootResolver: resolvers.NewRootResolver(cfg), } err := h.RootResolver.RegisterDefaultRepository(repo) diff --git a/graphql/resolvers/mutation.go b/graphql/resolvers/mutation.go index 850645f47f826df46371aa2d16dcc03cc3f83e8f..80d6fb1aed9789502845d2510ec1f9985e388fda 100644 --- a/graphql/resolvers/mutation.go +++ b/graphql/resolvers/mutation.go @@ -7,8 +7,32 @@ import ( "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/graphql/graph" "github.com/MichaelMure/git-bug/graphql/models" + "github.com/vektah/gqlparser/gqlerror" ) +var _ graph.MutationResolver = &readonlyMutationResolver{} + +type readonlyMutationResolver struct{} + +func (readonlyMutationResolver) NewBug(_ context.Context, _ models.NewBugInput) (*models.NewBugPayload, error) { + return nil, gqlerror.Errorf("readonly mode") +} +func (readonlyMutationResolver) AddComment(_ context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error) { + return nil, gqlerror.Errorf("readonly mode") +} +func (readonlyMutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) { + return nil, gqlerror.Errorf("readonly mode") +} +func (readonlyMutationResolver) OpenBug(_ context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error) { + return nil, gqlerror.Errorf("readonly mode") +} +func (readonlyMutationResolver) CloseBug(_ context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error) { + return nil, gqlerror.Errorf("readonly mode") +} +func (readonlyMutationResolver) SetTitle(_ context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error) { + return nil, gqlerror.Errorf("readonly mode") +} + var _ graph.MutationResolver = &mutationResolver{} type mutationResolver struct { diff --git a/graphql/resolvers/repo.go b/graphql/resolvers/repo.go index 639e8f90d3f51d228003506ea9b07c27639c57f2..e30b49f0b93d5d2ee55535c34cc615deb45c7cb9 100644 --- a/graphql/resolvers/repo.go +++ b/graphql/resolvers/repo.go @@ -5,6 +5,7 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/graphql/config" "github.com/MichaelMure/git-bug/graphql/connections" "github.com/MichaelMure/git-bug/graphql/graph" "github.com/MichaelMure/git-bug/graphql/models" @@ -13,7 +14,7 @@ import ( var _ graph.RepositoryResolver = &repoResolver{} -type repoResolver struct{} +type repoResolver struct{ cfg config.Config } func (repoResolver) Name(_ context.Context, obj *models.Repository) (*string, error) { name := obj.Repo.Name() @@ -149,7 +150,10 @@ func (repoResolver) Identity(_ context.Context, obj *models.Repository, prefix s return models.NewLazyIdentity(obj.Repo, excerpt), nil } -func (repoResolver) UserIdentity(_ context.Context, obj *models.Repository) (models.IdentityWrapper, error) { +func (r repoResolver) UserIdentity(_ context.Context, obj *models.Repository) (models.IdentityWrapper, error) { + if r.cfg.ReadOnly { + return nil, nil + } excerpt, err := obj.Repo.GetUserIdentityExcerpt() if err != nil { return nil, err diff --git a/graphql/resolvers/root.go b/graphql/resolvers/root.go index 9973ff59afe83cb4f7d0a1b9387a66afa16d127c..214bbae31f69dd588eee8f6a9229a93d992d0c51 100644 --- a/graphql/resolvers/root.go +++ b/graphql/resolvers/root.go @@ -3,6 +3,7 @@ package resolvers import ( "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/graphql/config" "github.com/MichaelMure/git-bug/graphql/graph" ) @@ -10,11 +11,13 @@ var _ graph.ResolverRoot = &RootResolver{} type RootResolver struct { cache.MultiRepoCache + cfg config.Config } -func NewRootResolver() *RootResolver { +func NewRootResolver(cfg config.Config) *RootResolver { return &RootResolver{ MultiRepoCache: cache.NewMultiRepoCache(), + cfg: cfg, } } @@ -25,13 +28,16 @@ func (r RootResolver) Query() graph.QueryResolver { } func (r RootResolver) Mutation() graph.MutationResolver { + if r.cfg.ReadOnly { + return &readonlyMutationResolver{} + } return &mutationResolver{ cache: &r.MultiRepoCache, } } -func (RootResolver) Repository() graph.RepositoryResolver { - return &repoResolver{} +func (r RootResolver) Repository() graph.RepositoryResolver { + return &repoResolver{r.cfg} } func (RootResolver) Bug() graph.BugResolver { @@ -50,7 +56,7 @@ func (RootResolver) Label() graph.LabelResolver { return &labelResolver{} } -func (r RootResolver) Identity() graph.IdentityResolver { +func (RootResolver) Identity() graph.IdentityResolver { return &identityResolver{} } diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index c3e628491d13985cd88d7b01a384dbfb94d307c4..4152725fb2f175d3591aa595d115612a864472cc 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -1213,6 +1213,8 @@ _git-bug_webui() two_word_flags+=("--port") two_word_flags+=("-p") local_nonpersistent_flags+=("--port=") + flags+=("--read-only") + local_nonpersistent_flags+=("--read-only") must_have_one_flag=() must_have_one_noun=() diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug index 0a8c7b5a3767cdbcc2a49717921a3f67a7d2a302..40831a119b80cc9b23538d8f95354d1d34b12b06 100644 --- a/misc/powershell_completion/git-bug +++ b/misc/powershell_completion/git-bug @@ -240,6 +240,7 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { [CompletionResult]::new('--no-open', 'no-open', [CompletionResultType]::ParameterName, 'Prevent the automatic opening of the web UI in the default browser') [CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Port to listen to (default is random)') [CompletionResult]::new('--port', 'port', [CompletionResultType]::ParameterName, 'Port to listen to (default is random)') + [CompletionResult]::new('--read-only', 'read-only', [CompletionResultType]::ParameterName, 'Whether to run the web UI in read-only mode') break } }) diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index ba15f3bcf47bfc046997474ba74cefa7dfd53b34..fbd52be5c198a5c53803f946957b271ea246e8a5 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -459,6 +459,7 @@ function _git-bug_webui { _arguments \ '--open[Automatically open the web UI in the default browser]' \ '--no-open[Prevent the automatic opening of the web UI in the default browser]' \ - '(-p --port)'{-p,--port}'[Port to listen to (default is random)]:' + '(-p --port)'{-p,--port}'[Port to listen to (default is random)]:' \ + '--read-only[Whether to run the web UI in read-only mode]' } diff --git a/webui/src/layout/CurrentIdentity.tsx b/webui/src/layout/CurrentIdentity.tsx index 21f489ef73289010650bac69e1c99bbfce9e2c87..550601799993cededd3028496fb1c965351127d8 100644 --- a/webui/src/layout/CurrentIdentity.tsx +++ b/webui/src/layout/CurrentIdentity.tsx @@ -3,7 +3,7 @@ import React from 'react'; import Avatar from '@material-ui/core/Avatar'; import { makeStyles } from '@material-ui/core/styles'; -import { useCurrentIdentityQuery } from './CurrentIdentity.generated'; +import CurrentIdentityContext from './CurrentIdentityContext'; const useStyles = makeStyles(theme => ({ displayName: { @@ -13,18 +13,26 @@ const useStyles = makeStyles(theme => ({ const CurrentIdentity = () => { const classes = useStyles(); - const { loading, error, data } = useCurrentIdentityQuery(); - if (error || loading || !data?.repository?.userIdentity) return null; - - const user = data.repository.userIdentity; return ( - <> - - {user.displayName.charAt(0).toUpperCase()} - -
{user.displayName}
- + + {context => { + if (!context) return null; + const { loading, error, data } = context as any; + + if (error || loading || !data?.repository?.userIdentity) return null; + + const user = data.repository.userIdentity; + return ( + <> + + {user.displayName.charAt(0).toUpperCase()} + +
{user.displayName}
+ + ); + }} +
); }; diff --git a/webui/src/layout/CurrentIdentityContext.tsx b/webui/src/layout/CurrentIdentityContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..78f2f263f8106be6995db534aee6b63e9706fd42 --- /dev/null +++ b/webui/src/layout/CurrentIdentityContext.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + +import { CurrentIdentityQueryResult } from './CurrentIdentity.generated'; + +const Context = React.createContext(null as CurrentIdentityQueryResult | null); +export default Context; diff --git a/webui/src/layout/ReadonlyHidden.tsx b/webui/src/layout/ReadonlyHidden.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9ed0af6acc625352924d4340b55c04868218a73e --- /dev/null +++ b/webui/src/layout/ReadonlyHidden.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import CurrentIdentityContext from './CurrentIdentityContext'; + +type Props = { children: React.ReactNode }; +const ReadonlyHidden = ({ children }: Props) => ( + + {context => { + if (!context) return null; + const { loading, error, data } = context; + + if (error || loading || !data?.repository?.userIdentity) return null; + + return <>{children}; + }} + +); + +export default ReadonlyHidden; diff --git a/webui/src/layout/index.tsx b/webui/src/layout/index.tsx index 42a0cfc1bc494d7bc08959cdfe659bc76a1c8423..78ff5ae8061982383adb54502dfc3ff51bd03b53 100644 --- a/webui/src/layout/index.tsx +++ b/webui/src/layout/index.tsx @@ -2,16 +2,18 @@ import React from 'react'; import CssBaseline from '@material-ui/core/CssBaseline'; +import { useCurrentIdentityQuery } from './CurrentIdentity.generated'; +import CurrentIdentityContext from './CurrentIdentityContext'; import Header from './Header'; type Props = { children: React.ReactNode }; function Layout({ children }: Props) { return ( - <> +
{children} - + ); } diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index 1bc128dd3ab5bfa1ac594e1c6d86cdf7cc1a9253..99b9bddd41feb016209621a08409cedba4446a62 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -6,6 +6,7 @@ import { makeStyles } from '@material-ui/core/styles'; import Author from 'src/components/Author'; import Date from 'src/components/Date'; import Label from 'src/components/Label'; +import ReadonlyHidden from 'src/layout/ReadonlyHidden'; import { BugFragment } from './Bug.generated'; import CommentForm from './CommentForm'; @@ -88,9 +89,11 @@ function Bug({ bug }: Props) {
-
- -
+ +
+ +
+
Labels diff --git a/webui/src/pages/bug/CommentForm.graphql b/webui/src/pages/bug/CommentForm.graphql index 33d211938e35acb209ef71ba2b0f56cbc16dfa03..f4b618501b3a6becbc56f3cee6976c08ee45f5da 100644 --- a/webui/src/pages/bug/CommentForm.graphql +++ b/webui/src/pages/bug/CommentForm.graphql @@ -1,5 +1,7 @@ mutation AddComment($input: AddCommentInput!) { addComment(input: $input) { - operation { id } + operation { + id + } } }