Clear all HTTP request logs (#49)

* Create mutation to clear request logs

* Add UI for clearing all HTTP request logs

* Use consistent naming

* Explicitly delete only from http_requests

* Check if datebase is open

* Add confirmation dialog
This commit is contained in:
Michał Załęcki
2020-11-28 15:48:19 +01:00
committed by GitHub
parent efc115e961
commit e59b9d6663
12 changed files with 385 additions and 105 deletions

View File

@ -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 (
<Dialog
open={isOpen}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{children}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Abort</Button>
<Button onClick={confirm} autoFocus>
Confirm
</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -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, {

View File

@ -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<HTMLElement | null>();
const [filterOpen, setFilterOpen] = useState(false);
@ -111,90 +122,112 @@ function Search(): JSX.Element {
};
return (
<ClickAwayListener onClickAway={handleClickAway}>
<Box style={{ display: "inline-block" }}>
{filterErr && (
<Box mb={4}>
<Alert severity="error">
Error fetching filter: {filterErr.message}
</Alert>
</Box>
)}
{setFilterErr && (
<Box mb={4}>
<Alert severity="error">
Error setting filter: {setFilterErr.message}
</Alert>
</Box>
)}
<Paper
component="form"
onSubmit={handleSubmit}
ref={filterRef}
className={classes.root}
>
<Tooltip title="Toggle filter options">
<IconButton
className={classes.iconButton}
onClick={() => setFilterOpen(!filterOpen)}
style={{
color:
filter?.httpRequestLogFilter !== null
? theme.palette.secondary.main
: "inherit",
}}
>
{filterLoading || setFilterLoading ? (
<CircularProgress className={classes.filterLoading} size={23} />
) : (
<FilterListIcon />
)}
</IconButton>
</Tooltip>
<InputBase
className={classes.input}
placeholder="Search proxy logs…"
onFocus={() => setFilterOpen(true)}
/>
<Tooltip title="Search">
<IconButton type="submit" className={classes.iconButton}>
<SearchIcon />
</IconButton>
</Tooltip>
</Paper>
<Popper
className={classes.filterPopper}
open={filterOpen}
anchorEl={filterRef.current}
placement="bottom-start"
>
<Paper className={classes.filterOptions}>
<FormControlLabel
control={
<Checkbox
checked={
filter?.httpRequestLogFilter?.onlyInScope ? true : false
}
disabled={filterLoading || setFilterLoading}
onChange={(e) =>
setFilterMutate({
variables: {
filter: {
...withoutTypename(filter?.httpRequestLogFilter),
onlyInScope: e.target.checked,
},
},
})
}
/>
}
label="Only show in-scope requests"
<Box>
<Error prefix="Error fetching filter" error={filterErr} />
<Error prefix="Error setting filter" error={setFilterErr} />
<Error
prefix="Error clearing all HTTP logs"
error={clearHTTPRequestLogResult.error}
/>
<Box style={{ display: "flex", flex: 1 }}>
<ClickAwayListener onClickAway={handleClickAway}>
<Paper
component="form"
onSubmit={handleSubmit}
ref={filterRef}
className={classes.root}
>
<Tooltip title="Toggle filter options">
<IconButton
className={classes.iconButton}
onClick={() => setFilterOpen(!filterOpen)}
style={{
color:
filter?.httpRequestLogFilter !== null
? theme.palette.secondary.main
: "inherit",
}}
>
{filterLoading || setFilterLoading ? (
<CircularProgress
className={classes.filterLoading}
size={23}
/>
) : (
<FilterListIcon />
)}
</IconButton>
</Tooltip>
<InputBase
className={classes.input}
placeholder="Search proxy logs…"
onFocus={() => setFilterOpen(true)}
/>
<Tooltip title="Search">
<IconButton type="submit" className={classes.iconButton}>
<SearchIcon />
</IconButton>
</Tooltip>
<Popper
className={classes.filterPopper}
open={filterOpen}
anchorEl={filterRef.current}
placement="bottom-start"
>
<Paper className={classes.filterOptions}>
<FormControlLabel
control={
<Checkbox
checked={
filter?.httpRequestLogFilter?.onlyInScope ? true : false
}
disabled={filterLoading || setFilterLoading}
onChange={(e) =>
setFilterMutate({
variables: {
filter: {
...withoutTypename(filter?.httpRequestLogFilter),
onlyInScope: e.target.checked,
},
},
})
}
/>
}
label="Only show in-scope requests"
/>
</Paper>
</Popper>
</Paper>
</Popper>
</ClickAwayListener>
<Box style={{ marginLeft: "auto" }}>
<Tooltip title="Clear all">
<IconButton onClick={clearHTTPConfirmationDialog.open}>
<DeleteIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
</ClickAwayListener>
<ConfirmationDialog
isOpen={clearHTTPConfirmationDialog.isOpen}
onClose={clearHTTPConfirmationDialog.close}
onConfirm={clearHTTPRequestLog}
>
All proxy logs are going to be removed. This action cannot be undone.
</ConfirmationDialog>
</Box>
);
}
function Error(props: { prefix: string; error?: Error }) {
if (!props.error) return null;
return (
<Box mb={4}>
<Alert severity="error">
{props.prefix}: {props.error.message}
</Alert>
</Box>
);
}

View File

@ -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 }],
});
}

View File

@ -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,
});
}

View File

@ -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)
}

View File

@ -9,6 +9,10 @@ import (
"time"
)
type ClearHTTPRequestLogResult struct {
Success bool `json:"success"`
}
type CloseProjectResult struct {
Success bool `json:"success"`
}

View File

@ -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 {

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -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,