Add support for read-only mode for web UI.

Fixes #402.
This commit is contained in:
Luke Granger-Brown 2020-06-18 02:52:33 +01:00 committed by Michael Muré
parent 23228101a2
commit 4a28f25347
No known key found for this signature in database
GPG Key ID: A4457C029293126F
18 changed files with 130 additions and 35 deletions

View File

@ -19,15 +19,17 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/MichaelMure/git-bug/graphql" "github.com/MichaelMure/git-bug/graphql"
"github.com/MichaelMure/git-bug/graphql/config"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/webui" "github.com/MichaelMure/git-bug/webui"
) )
var ( var (
webUIPort int webUIPort int
webUIOpen bool webUIOpen bool
webUINoOpen bool webUINoOpen bool
webUIReadOnly bool
) )
const webUIOpenConfigKey = "git-bug.webui.open" const webUIOpenConfigKey = "git-bug.webui.open"
@ -46,7 +48,7 @@ func runWebUI(cmd *cobra.Command, args []string) error {
router := mux.NewRouter() router := mux.NewRouter()
graphqlHandler, err := graphql.NewHandler(repo) graphqlHandler, err := graphql.NewHandler(repo, config.Config{ReadOnly: webUIReadOnly})
if err != nil { if err != nil {
return err 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(&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().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().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")
} }

View File

@ -34,6 +34,10 @@ Available git config:
\fB\-p\fP, \fB\-\-port\fP=0 \fB\-p\fP, \fB\-\-port\fP=0
Port to listen to (default is random) Port to listen to (default is random)
.PP
\fB\-\-read\-only\fP[=false]
Whether to run the web UI in read\-only mode
.PP .PP
\fB\-h\fP, \fB\-\-help\fP[=false] \fB\-h\fP, \fB\-\-help\fP[=false]
help for webui help for webui

View File

@ -17,10 +17,11 @@ git-bug webui [flags]
### Options ### Options
``` ```
--open Automatically open the web UI in the default browser --open Automatically open the web UI in the default browser
--no-open Prevent the automatic opening of 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) -p, --port int Port to listen to (default is random)
-h, --help help for webui --read-only Whether to run the web UI in read-only mode
-h, --help help for webui
``` ```
### SEE ALSO ### SEE ALSO

7
graphql/config/config.go Normal file
View File

@ -0,0 +1,7 @@
// Package config contains configuration for GraphQL stuff.
package config
// Config holds configuration elements.
type Config struct {
ReadOnly bool
}

View File

@ -5,6 +5,7 @@ import (
"github.com/99designs/gqlgen/client" "github.com/99designs/gqlgen/client"
"github.com/MichaelMure/git-bug/graphql/config"
"github.com/MichaelMure/git-bug/graphql/models" "github.com/MichaelMure/git-bug/graphql/models"
"github.com/MichaelMure/git-bug/misc/random_bugs" "github.com/MichaelMure/git-bug/misc/random_bugs"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
@ -16,7 +17,7 @@ func TestQueries(t *testing.T) {
random_bugs.FillRepoWithSeed(repo, 10, 42) random_bugs.FillRepoWithSeed(repo, 10, 42)
handler, err := NewHandler(repo) handler, err := NewHandler(repo, config.Config{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/99designs/gqlgen/graphql/handler" "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/graph"
"github.com/MichaelMure/git-bug/graphql/resolvers" "github.com/MichaelMure/git-bug/graphql/resolvers"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
@ -19,9 +20,9 @@ type Handler struct {
*resolvers.RootResolver *resolvers.RootResolver
} }
func NewHandler(repo repository.ClockedRepo) (Handler, error) { func NewHandler(repo repository.ClockedRepo, cfg config.Config) (Handler, error) {
h := Handler{ h := Handler{
RootResolver: resolvers.NewRootResolver(), RootResolver: resolvers.NewRootResolver(cfg),
} }
err := h.RootResolver.RegisterDefaultRepository(repo) err := h.RootResolver.RegisterDefaultRepository(repo)

View File

@ -7,8 +7,32 @@ import (
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/graphql/graph" "github.com/MichaelMure/git-bug/graphql/graph"
"github.com/MichaelMure/git-bug/graphql/models" "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{} var _ graph.MutationResolver = &mutationResolver{}
type mutationResolver struct { type mutationResolver struct {

View File

@ -5,6 +5,7 @@ import (
"github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity" "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/connections"
"github.com/MichaelMure/git-bug/graphql/graph" "github.com/MichaelMure/git-bug/graphql/graph"
"github.com/MichaelMure/git-bug/graphql/models" "github.com/MichaelMure/git-bug/graphql/models"
@ -13,7 +14,7 @@ import (
var _ graph.RepositoryResolver = &repoResolver{} var _ graph.RepositoryResolver = &repoResolver{}
type repoResolver struct{} type repoResolver struct{ cfg config.Config }
func (repoResolver) Name(_ context.Context, obj *models.Repository) (*string, error) { func (repoResolver) Name(_ context.Context, obj *models.Repository) (*string, error) {
name := obj.Repo.Name() 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 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() excerpt, err := obj.Repo.GetUserIdentityExcerpt()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -3,6 +3,7 @@ package resolvers
import ( import (
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/graphql/config"
"github.com/MichaelMure/git-bug/graphql/graph" "github.com/MichaelMure/git-bug/graphql/graph"
) )
@ -10,11 +11,13 @@ var _ graph.ResolverRoot = &RootResolver{}
type RootResolver struct { type RootResolver struct {
cache.MultiRepoCache cache.MultiRepoCache
cfg config.Config
} }
func NewRootResolver() *RootResolver { func NewRootResolver(cfg config.Config) *RootResolver {
return &RootResolver{ return &RootResolver{
MultiRepoCache: cache.NewMultiRepoCache(), MultiRepoCache: cache.NewMultiRepoCache(),
cfg: cfg,
} }
} }
@ -25,13 +28,16 @@ func (r RootResolver) Query() graph.QueryResolver {
} }
func (r RootResolver) Mutation() graph.MutationResolver { func (r RootResolver) Mutation() graph.MutationResolver {
if r.cfg.ReadOnly {
return &readonlyMutationResolver{}
}
return &mutationResolver{ return &mutationResolver{
cache: &r.MultiRepoCache, cache: &r.MultiRepoCache,
} }
} }
func (RootResolver) Repository() graph.RepositoryResolver { func (r RootResolver) Repository() graph.RepositoryResolver {
return &repoResolver{} return &repoResolver{r.cfg}
} }
func (RootResolver) Bug() graph.BugResolver { func (RootResolver) Bug() graph.BugResolver {
@ -50,7 +56,7 @@ func (RootResolver) Label() graph.LabelResolver {
return &labelResolver{} return &labelResolver{}
} }
func (r RootResolver) Identity() graph.IdentityResolver { func (RootResolver) Identity() graph.IdentityResolver {
return &identityResolver{} return &identityResolver{}
} }

View File

@ -1213,6 +1213,8 @@ _git-bug_webui()
two_word_flags+=("--port") two_word_flags+=("--port")
two_word_flags+=("-p") two_word_flags+=("-p")
local_nonpersistent_flags+=("--port=") local_nonpersistent_flags+=("--port=")
flags+=("--read-only")
local_nonpersistent_flags+=("--read-only")
must_have_one_flag=() must_have_one_flag=()
must_have_one_noun=() must_have_one_noun=()

View File

@ -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('--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('-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('--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 break
} }
}) })

View File

@ -459,6 +459,7 @@ function _git-bug_webui {
_arguments \ _arguments \
'--open[Automatically open the web UI in the default browser]' \ '--open[Automatically open the web UI in the default browser]' \
'--no-open[Prevent the automatic opening of 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]'
} }

View File

@ -3,7 +3,7 @@ import React from 'react';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import { useCurrentIdentityQuery } from './CurrentIdentity.generated'; import CurrentIdentityContext from './CurrentIdentityContext';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
displayName: { displayName: {
@ -13,18 +13,26 @@ const useStyles = makeStyles(theme => ({
const CurrentIdentity = () => { const CurrentIdentity = () => {
const classes = useStyles(); const classes = useStyles();
const { loading, error, data } = useCurrentIdentityQuery();
if (error || loading || !data?.repository?.userIdentity) return null;
const user = data.repository.userIdentity;
return ( return (
<> <CurrentIdentityContext.Consumer>
<Avatar src={user.avatarUrl ? user.avatarUrl : undefined}> {context => {
{user.displayName.charAt(0).toUpperCase()} if (!context) return null;
</Avatar> const { loading, error, data } = context as any;
<div className={classes.displayName}>{user.displayName}</div>
</> if (error || loading || !data?.repository?.userIdentity) return null;
const user = data.repository.userIdentity;
return (
<>
<Avatar src={user.avatarUrl ? user.avatarUrl : undefined}>
{user.displayName.charAt(0).toUpperCase()}
</Avatar>
<div className={classes.displayName}>{user.displayName}</div>
</>
);
}}
</CurrentIdentityContext.Consumer>
); );
}; };

View File

@ -0,0 +1,6 @@
import React from 'react';
import { CurrentIdentityQueryResult } from './CurrentIdentity.generated';
const Context = React.createContext(null as CurrentIdentityQueryResult | null);
export default Context;

View File

@ -0,0 +1,19 @@
import React from 'react';
import CurrentIdentityContext from './CurrentIdentityContext';
type Props = { children: React.ReactNode };
const ReadonlyHidden = ({ children }: Props) => (
<CurrentIdentityContext.Consumer>
{context => {
if (!context) return null;
const { loading, error, data } = context;
if (error || loading || !data?.repository?.userIdentity) return null;
return <>{children}</>;
}}
</CurrentIdentityContext.Consumer>
);
export default ReadonlyHidden;

View File

@ -2,16 +2,18 @@ import React from 'react';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import { useCurrentIdentityQuery } from './CurrentIdentity.generated';
import CurrentIdentityContext from './CurrentIdentityContext';
import Header from './Header'; import Header from './Header';
type Props = { children: React.ReactNode }; type Props = { children: React.ReactNode };
function Layout({ children }: Props) { function Layout({ children }: Props) {
return ( return (
<> <CurrentIdentityContext.Provider value={useCurrentIdentityQuery()}>
<CssBaseline /> <CssBaseline />
<Header /> <Header />
{children} {children}
</> </CurrentIdentityContext.Provider>
); );
} }

View File

@ -6,6 +6,7 @@ import { makeStyles } from '@material-ui/core/styles';
import Author from 'src/components/Author'; import Author from 'src/components/Author';
import Date from 'src/components/Date'; import Date from 'src/components/Date';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
import ReadonlyHidden from 'src/layout/ReadonlyHidden';
import { BugFragment } from './Bug.generated'; import { BugFragment } from './Bug.generated';
import CommentForm from './CommentForm'; import CommentForm from './CommentForm';
@ -88,9 +89,11 @@ function Bug({ bug }: Props) {
<div className={classes.container}> <div className={classes.container}>
<div className={classes.timeline}> <div className={classes.timeline}>
<TimelineQuery id={bug.id} /> <TimelineQuery id={bug.id} />
<div className={classes.commentForm}> <ReadonlyHidden>
<CommentForm bugId={bug.id} /> <div className={classes.commentForm}>
</div> <CommentForm bugId={bug.id} />
</div>
</ReadonlyHidden>
</div> </div>
<div className={classes.sidebar}> <div className={classes.sidebar}>
<span className={classes.sidebarTitle}>Labels</span> <span className={classes.sidebarTitle}>Labels</span>

View File

@ -1,5 +1,7 @@
mutation AddComment($input: AddCommentInput!) { mutation AddComment($input: AddCommentInput!) {
addComment(input: $input) { addComment(input: $input) {
operation { id } operation {
id
}
} }
} }