diff --git a/admin/src/components/reqlog/ConfirmationDialog.tsx b/admin/src/components/reqlog/ConfirmationDialog.tsx new file mode 100644 index 0000000..0c65398 --- /dev/null +++ b/admin/src/components/reqlog/ConfirmationDialog.tsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; + +export function useConfirmationDialog() { + const [isOpen, setIsOpen] = useState(false); + const close = () => setIsOpen(false); + const open = () => setIsOpen(true); + + return { open, close, isOpen }; +} + +interface ConfirmationDialog { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + children: React.ReactNode; +} + +export function ConfirmationDialog(props: ConfirmationDialog) { + const { onClose, onConfirm, isOpen, children } = props; + + function confirm() { + onConfirm(); + onClose(); + } + + return ( + + Are you sure? + + + {children} + + + + + + + + ); +} diff --git a/admin/src/components/reqlog/LogsOverview.tsx b/admin/src/components/reqlog/LogsOverview.tsx index f8341e3..7216ee0 100644 --- a/admin/src/components/reqlog/LogsOverview.tsx +++ b/admin/src/components/reqlog/LogsOverview.tsx @@ -1,41 +1,24 @@ import { useRouter } from "next/router"; -import { gql, useQuery } from "@apollo/client"; import Link from "next/link"; import { Box, - Typography, CircularProgress, Link as MaterialLink, + Typography, } from "@material-ui/core"; import Alert from "@material-ui/lab/Alert"; import RequestList from "./RequestList"; import LogDetail from "./LogDetail"; import CenteredPaper from "../CenteredPaper"; - -const HTTP_REQUEST_LOGS = gql` - query HttpRequestLogs { - httpRequestLogs { - id - method - url - timestamp - response { - statusCode - statusReason - } - } - } -`; +import { useHttpRequestLogs } from "./hooks/useHttpRequestLogs"; function LogsOverview(): JSX.Element { const router = useRouter(); const detailReqLogId = router.query.id && parseInt(router.query.id as string, 10); - const { loading, error, data } = useQuery(HTTP_REQUEST_LOGS, { - pollInterval: 1000, - }); + const { loading, error, data } = useHttpRequestLogs(); const handleLogClick = (reqId: number) => { router.push("/proxy/logs?id=" + reqId, undefined, { diff --git a/admin/src/components/reqlog/Search.tsx b/admin/src/components/reqlog/Search.tsx index 7f6ae7b..76a2d72 100644 --- a/admin/src/components/reqlog/Search.tsx +++ b/admin/src/components/reqlog/Search.tsx @@ -16,10 +16,16 @@ import { import IconButton from "@material-ui/core/IconButton"; import SearchIcon from "@material-ui/icons/Search"; import FilterListIcon from "@material-ui/icons/FilterList"; +import DeleteIcon from "@material-ui/icons/Delete"; import React, { useRef, useState } from "react"; -import { gql, useApolloClient, useMutation, useQuery } from "@apollo/client"; +import { gql, useMutation, useQuery } from "@apollo/client"; import { withoutTypename } from "../../lib/omitTypename"; import { Alert } from "@material-ui/lab"; +import { useClearHTTPRequestLog } from "./hooks/useClearHTTPRequestLog"; +import { + ConfirmationDialog, + useConfirmationDialog, +} from "./ConfirmationDialog"; const FILTER = gql` query HttpRequestLogFilter { @@ -79,15 +85,14 @@ function Search(): JSX.Element { FILTER ); - const client = useApolloClient(); const [ setFilterMutate, { error: setFilterErr, loading: setFilterLoading }, ] = useMutation<{ setHttpRequestLogFilter: SearchFilter | null; }>(SET_FILTER, { - update(_, { data: { setHttpRequestLogFilter } }) { - client.writeQuery({ + update(cache, { data: { setHttpRequestLogFilter } }) { + cache.writeQuery({ query: FILTER, data: { httpRequestLogFilter: setHttpRequestLogFilter, @@ -96,6 +101,12 @@ function Search(): JSX.Element { }, }); + const [ + clearHTTPRequestLog, + clearHTTPRequestLogResult, + ] = useClearHTTPRequestLog(); + const clearHTTPConfirmationDialog = useConfirmationDialog(); + const filterRef = useRef(); const [filterOpen, setFilterOpen] = useState(false); @@ -111,90 +122,112 @@ function Search(): JSX.Element { }; return ( - - - {filterErr && ( - - - Error fetching filter: {filterErr.message} - - - )} - {setFilterErr && ( - - - Error setting filter: {setFilterErr.message} - - - )} - - - setFilterOpen(!filterOpen)} - style={{ - color: - filter?.httpRequestLogFilter !== null - ? theme.palette.secondary.main - : "inherit", - }} + + + + + + + + + setFilterOpen(!filterOpen)} + style={{ + color: + filter?.httpRequestLogFilter !== null + ? theme.palette.secondary.main + : "inherit", + }} + > + {filterLoading || setFilterLoading ? ( + + ) : ( + + )} + + + setFilterOpen(true)} + /> + + + + + + - {filterLoading || setFilterLoading ? ( - - ) : ( - - )} - - - setFilterOpen(true)} - /> - - - - - - - - - - - setFilterMutate({ - variables: { - filter: { - ...withoutTypename(filter?.httpRequestLogFilter), - onlyInScope: e.target.checked, - }, - }, - }) + + + setFilterMutate({ + variables: { + filter: { + ...withoutTypename(filter?.httpRequestLogFilter), + onlyInScope: e.target.checked, + }, + }, + }) + } + /> } + label="Only show in-scope requests" /> - } - label="Only show in-scope requests" - /> + + - + + + + + + + + - + + All proxy logs are going to be removed. This action cannot be undone. + + + ); +} + +function Error(props: { prefix: string; error?: Error }) { + if (!props.error) return null; + + return ( + + + {props.prefix}: {props.error.message} + + ); } diff --git a/admin/src/components/reqlog/hooks/useClearHTTPRequestLog.ts b/admin/src/components/reqlog/hooks/useClearHTTPRequestLog.ts new file mode 100644 index 0000000..2870712 --- /dev/null +++ b/admin/src/components/reqlog/hooks/useClearHTTPRequestLog.ts @@ -0,0 +1,16 @@ +import { gql, useMutation } from "@apollo/client"; +import { HTTP_REQUEST_LOGS } from "./useHttpRequestLogs"; + +const CLEAR_HTTP_REQUEST_LOG = gql` + mutation ClearHTTPRequestLog { + clearHTTPRequestLog { + success + } + } +`; + +export function useClearHTTPRequestLog() { + return useMutation(CLEAR_HTTP_REQUEST_LOG, { + refetchQueries: [{ query: HTTP_REQUEST_LOGS }], + }); +} diff --git a/admin/src/components/reqlog/hooks/useHttpRequestLogs.ts b/admin/src/components/reqlog/hooks/useHttpRequestLogs.ts new file mode 100644 index 0000000..f426702 --- /dev/null +++ b/admin/src/components/reqlog/hooks/useHttpRequestLogs.ts @@ -0,0 +1,22 @@ +import { gql, useQuery } from "@apollo/client"; + +export const HTTP_REQUEST_LOGS = gql` + query HttpRequestLogs { + httpRequestLogs { + id + method + url + timestamp + response { + statusCode + statusReason + } + } + } +`; + +export function useHttpRequestLogs() { + return useQuery(HTTP_REQUEST_LOGS, { + pollInterval: 1000, + }); +} diff --git a/pkg/api/generated.go b/pkg/api/generated.go index f0abe64..6da8e9d 100644 --- a/pkg/api/generated.go +++ b/pkg/api/generated.go @@ -43,6 +43,10 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + ClearHTTPRequestLogResult struct { + Success func(childComplexity int) int + } + CloseProjectResult struct { Success func(childComplexity int) int } @@ -81,6 +85,7 @@ type ComplexityRoot struct { } Mutation struct { + ClearHTTPRequestLog func(childComplexity int) int CloseProject func(childComplexity int) int DeleteProject func(childComplexity int, name string) int OpenProject func(childComplexity int, name string) int @@ -118,6 +123,7 @@ type MutationResolver interface { OpenProject(ctx context.Context, name string) (*Project, error) CloseProject(ctx context.Context) (*CloseProjectResult, error) DeleteProject(ctx context.Context, name string) (*DeleteProjectResult, error) + ClearHTTPRequestLog(ctx context.Context) (*ClearHTTPRequestLogResult, error) SetScope(ctx context.Context, scope []ScopeRuleInput) ([]ScopeRule, error) SetHTTPRequestLogFilter(ctx context.Context, filter *HTTPRequestLogFilterInput) (*HTTPRequestLogFilter, error) } @@ -145,6 +151,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "ClearHTTPRequestLogResult.success": + if e.complexity.ClearHTTPRequestLogResult.Success == nil { + break + } + + return e.complexity.ClearHTTPRequestLogResult.Success(childComplexity), true + case "CloseProjectResult.success": if e.complexity.CloseProjectResult.Success == nil { break @@ -278,6 +291,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.HTTPResponseLog.StatusReason(childComplexity), true + case "Mutation.clearHTTPRequestLog": + if e.complexity.Mutation.ClearHTTPRequestLog == nil { + break + } + + return e.complexity.Mutation.ClearHTTPRequestLog(childComplexity), true + case "Mutation.closeProject": if e.complexity.Mutation.CloseProject == nil { break @@ -553,6 +573,10 @@ type DeleteProjectResult { success: Boolean! } +type ClearHTTPRequestLogResult { + success: Boolean! +} + input HttpRequestLogFilterInput { onlyInScope: Boolean } @@ -574,6 +598,7 @@ type Mutation { openProject(name: String!): Project closeProject: CloseProjectResult! deleteProject(name: String!): DeleteProjectResult! + clearHTTPRequestLog: ClearHTTPRequestLogResult! setScope(scope: [ScopeRuleInput!]!): [ScopeRule!]! setHttpRequestLogFilter( filter: HttpRequestLogFilterInput @@ -730,6 +755,41 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** +func (ec *executionContext) _ClearHTTPRequestLogResult_success(ctx context.Context, field graphql.CollectedField, obj *ClearHTTPRequestLogResult) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ClearHTTPRequestLogResult", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Success, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _CloseProjectResult_success(ctx context.Context, field graphql.CollectedField, obj *CloseProjectResult) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -1502,6 +1562,41 @@ func (ec *executionContext) _Mutation_deleteProject(ctx context.Context, field g return ec.marshalNDeleteProjectResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐDeleteProjectResult(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_clearHTTPRequestLog(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ClearHTTPRequestLog(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*ClearHTTPRequestLogResult) + fc.Result = res + return ec.marshalNClearHTTPRequestLogResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐClearHTTPRequestLogResult(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_setScope(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3271,6 +3366,33 @@ func (ec *executionContext) unmarshalInputScopeRuleInput(ctx context.Context, ob // region **************************** object.gotpl **************************** +var clearHTTPRequestLogResultImplementors = []string{"ClearHTTPRequestLogResult"} + +func (ec *executionContext) _ClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, obj *ClearHTTPRequestLogResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, clearHTTPRequestLogResultImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ClearHTTPRequestLogResult") + case "success": + out.Values[i] = ec._ClearHTTPRequestLogResult_success(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var closeProjectResultImplementors = []string{"CloseProjectResult"} func (ec *executionContext) _CloseProjectResult(ctx context.Context, sel ast.SelectionSet, obj *CloseProjectResult) graphql.Marshaler { @@ -3516,6 +3638,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "clearHTTPRequestLog": + out.Values[i] = ec._Mutation_clearHTTPRequestLog(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "setScope": out.Values[i] = ec._Mutation_setScope(ctx, field) if out.Values[i] == graphql.Null { @@ -3985,6 +4112,20 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) marshalNClearHTTPRequestLogResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, v ClearHTTPRequestLogResult) graphql.Marshaler { + return ec._ClearHTTPRequestLogResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNClearHTTPRequestLogResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, v *ClearHTTPRequestLogResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._ClearHTTPRequestLogResult(ctx, sel, v) +} + func (ec *executionContext) marshalNCloseProjectResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCloseProjectResult(ctx context.Context, sel ast.SelectionSet, v CloseProjectResult) graphql.Marshaler { return ec._CloseProjectResult(ctx, sel, &v) } diff --git a/pkg/api/models_gen.go b/pkg/api/models_gen.go index 81c83d4..61a695b 100644 --- a/pkg/api/models_gen.go +++ b/pkg/api/models_gen.go @@ -9,6 +9,10 @@ import ( "time" ) +type ClearHTTPRequestLogResult struct { + Success bool `json:"success"` +} + type CloseProjectResult struct { Success bool `json:"success"` } diff --git a/pkg/api/resolvers.go b/pkg/api/resolvers.go index e15b7b1..eeec37b 100644 --- a/pkg/api/resolvers.go +++ b/pkg/api/resolvers.go @@ -209,6 +209,13 @@ func (r *mutationResolver) DeleteProject(ctx context.Context, name string) (*Del }, nil } +func (r *mutationResolver) ClearHTTPRequestLog(ctx context.Context) (*ClearHTTPRequestLogResult, error) { + if err := r.RequestLogService.ClearRequests(ctx); err != nil { + return nil, fmt.Errorf("could not clear request log: %v", err) + } + return &ClearHTTPRequestLogResult{true}, nil +} + func (r *mutationResolver) SetScope(ctx context.Context, input []ScopeRuleInput) ([]ScopeRule, error) { rules := make([]scope.Rule, len(input)) for i, rule := range input { diff --git a/pkg/api/schema.graphql b/pkg/api/schema.graphql index 32691b2..53a3757 100644 --- a/pkg/api/schema.graphql +++ b/pkg/api/schema.graphql @@ -58,6 +58,10 @@ type DeleteProjectResult { success: Boolean! } +type ClearHTTPRequestLogResult { + success: Boolean! +} + input HttpRequestLogFilterInput { onlyInScope: Boolean } @@ -79,6 +83,7 @@ type Mutation { openProject(name: String!): Project closeProject: CloseProjectResult! deleteProject(name: String!): DeleteProjectResult! + clearHTTPRequestLog: ClearHTTPRequestLogResult! setScope(scope: [ScopeRuleInput!]!): [ScopeRule!]! setHttpRequestLogFilter( filter: HttpRequestLogFilterInput diff --git a/pkg/db/sqlite/sqlite.go b/pkg/db/sqlite/sqlite.go index abbcba1..d3fa3ef 100644 --- a/pkg/db/sqlite/sqlite.go +++ b/pkg/db/sqlite/sqlite.go @@ -215,6 +215,17 @@ var headerFieldToColumnMap = map[string]string{ "value": "value", } +func (c *Client) ClearRequestLogs(ctx context.Context) error { + if c.db == nil { + return proj.ErrNoProject + } + _, err := c.db.Exec("DELETE FROM http_requests") + if err != nil { + return fmt.Errorf("sqlite: could not delete requests: %v", err) + } + return nil +} + func (c *Client) FindRequestLogs( ctx context.Context, filter reqlog.FindRequestsFilter, diff --git a/pkg/reqlog/repo.go b/pkg/reqlog/repo.go index 8607a60..7126a43 100644 --- a/pkg/reqlog/repo.go +++ b/pkg/reqlog/repo.go @@ -17,6 +17,7 @@ type Repository interface { FindRequestLogByID(ctx context.Context, id int64) (Request, error) AddRequestLog(ctx context.Context, req http.Request, body []byte, timestamp time.Time) (*Request, error) AddResponseLog(ctx context.Context, reqID int64, res http.Response, body []byte, timestamp time.Time) (*Response, error) + ClearRequestLogs(ctx context.Context) error UpsertSettings(ctx context.Context, module string, settings interface{}) error FindSettingsByModule(ctx context.Context, module string, settings interface{}) error } diff --git a/pkg/reqlog/reqlog.go b/pkg/reqlog/reqlog.go index 0957b56..8866e15 100644 --- a/pkg/reqlog/reqlog.go +++ b/pkg/reqlog/reqlog.go @@ -99,6 +99,10 @@ func (svc *Service) SetRequestLogFilter(ctx context.Context, filter FindRequests return svc.repo.UpsertSettings(ctx, "reqlog", svc) } +func (svc *Service) ClearRequests(ctx context.Context) error { + return svc.repo.ClearRequestLogs(ctx) +} + func (svc *Service) addRequest( ctx context.Context, req http.Request,