Compare commits
82 Commits
Author | SHA1 | Date | |
---|---|---|---|
29550ff43b | |||
7afc23b3ff | |||
6aa93b782e | |||
ed9a539ce3 | |||
857aa0c49e | |||
af26987601 | |||
ad26478043 | |||
ca0c085021 | |||
d438f93ee0 | |||
fa3f24eb70 | |||
f15438e10b | |||
bef52d956e | |||
8269af9478 | |||
c5f76e1f9a | |||
2ddf2a77e8 | |||
d2858a2be4 | |||
7e43479b54 | |||
11f70282d7 | |||
efc20564c1 | |||
afa211d0ec | |||
44193cd723 | |||
e07163fef3 | |||
ed394507d3 | |||
cd5403e353 | |||
565c370bb8 | |||
2dc6538a3b | |||
aa8ddf4122 | |||
73ebb89863 | |||
1489cb16bf | |||
d84d2d0905 | |||
8a3b3cbf02 | |||
b3225bfb99 | |||
4e2eaea499 | |||
8122b2552d | |||
569f7bc76f | |||
ca3a729c36 | |||
ad3dc0da70 | |||
49547f535f | |||
e42e1c212b | |||
6e38b16cf2 | |||
078bf303be | |||
a42f003919 | |||
50c2eac42d | |||
4ead501f53 | |||
d2e97f2acc | |||
ad3fa7d379 | |||
8c2efdb285 | |||
194d727f4f | |||
8ab65fb55f | |||
5bce912e89 | |||
16910bb637 | |||
e59b9d6663 | |||
efc115e961 | |||
471fa212ef | |||
07ef2f9090 | |||
f7550d649a | |||
dbc25774c2 | |||
430670ab54 | |||
f6789fa245 | |||
81fbfe4cb3 | |||
6931d63250 | |||
71e87d3cd3 | |||
0ffbb618fa | |||
c01f190fc8 | |||
ad98dd7f01 | |||
0d04996f06 | |||
98dacbe849 | |||
fedb425381 | |||
ca707d17ea | |||
13240109b6 | |||
fa41e9c46c | |||
5f4bff0155 | |||
073bcea565 | |||
6244d4aa74 | |||
cf687f0bd3 | |||
6fad74c0a5 | |||
248001ec8a | |||
ba7d88dfc5 | |||
d48f1f058d | |||
46caa05d20 | |||
c5bfb96454 | |||
f97e0526d7 |
@ -1,5 +1,8 @@
|
||||
**/rice-box.go
|
||||
/admin/.env
|
||||
/admin/.next
|
||||
/admin/dist
|
||||
/admin/node_modules
|
||||
/admin/node_modules
|
||||
/dist
|
||||
/docs
|
||||
/hetty
|
||||
/cmd/hetty/admin
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ask a question
|
||||
url: https://github.com/dstotijn/hetty/discussions
|
||||
about: Ask questions and discuss with other community members
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
52
.github/workflows/build-test.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
name: Build and Test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.17", "1.16"]
|
||||
name: Go ${{ matrix.go }} - Build
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ github.workspace }}/admin/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
||||
- run: make build
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.17", "1.16"]
|
||||
name: Go ${{ matrix.go }} - Test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- run: go test ./pkg/...
|
16
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: Lint
|
||||
on: [push, pull_request]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./admin
|
||||
jobs:
|
||||
lint-admin:
|
||||
runs-on: ubuntu-latest
|
||||
name: Admin (Next.js)
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
- run: yarn install
|
||||
- run: yarn run lint
|
11
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
**/rice-box.go
|
||||
dist
|
||||
hetty
|
||||
hetty.bolt
|
||||
*.test
|
||||
*.vscode
|
||||
/dist
|
||||
/hetty
|
||||
/cmd/hetty/admin
|
||||
*.pem
|
||||
*.test
|
53
.golangci.yml
Normal file
@ -0,0 +1,53 @@
|
||||
linters:
|
||||
presets:
|
||||
- bugs
|
||||
- comment
|
||||
- error
|
||||
- format
|
||||
- import
|
||||
- metalinter
|
||||
- module
|
||||
- performance
|
||||
- style
|
||||
- test
|
||||
- unused
|
||||
disable:
|
||||
- dupl
|
||||
- exhaustive
|
||||
- exhaustivestruct
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- godox
|
||||
- goerr113
|
||||
- gomnd
|
||||
- interfacer
|
||||
- maligned
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- scopelint
|
||||
- testpackage
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
|
||||
linters-settings:
|
||||
gci:
|
||||
local-prefixes: github.com/dstotijn/hetty
|
||||
godot:
|
||||
capital: true
|
||||
ireturn:
|
||||
allow: "error,empty,anon,stdlib,.*(or|er)$,github.com/99designs/gqlgen/graphql.Marshaler,github.com/dstotijn/hetty/pkg/api.QueryResolver,github.com/dstotijn/hetty/pkg/search.Expression"
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- gosec
|
||||
# Ignore SHA1 usage.
|
||||
text: "G(401|505):"
|
||||
- linters:
|
||||
- wsl
|
||||
# Ignore cuddled defer statements.
|
||||
text: "only one cuddle assignment allowed before defer statement"
|
||||
- linters:
|
||||
- nlreturn
|
||||
# Ignore `break` without leading blank line.
|
||||
text: "break with no blank line before"
|
@ -1,29 +1,53 @@
|
||||
before:
|
||||
hooks:
|
||||
- make clean
|
||||
- go mod download
|
||||
- go generate ./...
|
||||
- sh -c "NEXT_PUBLIC_VERSION={{ .Version}} make build-admin"
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- main: ./cmd/hetty
|
||||
env:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
main: ./cmd/hetty
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}}
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
hooks:
|
||||
pre: make embed
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
brews:
|
||||
- tap:
|
||||
owner: hettysoft
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
homepage: https://hetty.xyz
|
||||
description: An HTTP toolkit for security research.
|
||||
license: MIT
|
||||
commit_author:
|
||||
name: David Stotijn
|
||||
email: dstotijn@gmail.com
|
||||
test: |
|
||||
system "#{bin}/hetty -v"
|
||||
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
|
76
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at dstotijn@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
35
CONTRIBUTING.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
Thank you for taking an interest in Hetty! If you want to contribute to the
|
||||
project, please read the guidelines below to ensure a smooth develop experience.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Please first read the [code of conduct](CODE_OF_CONDUCT.md), and abide to it
|
||||
whenever you interact with the community.
|
||||
|
||||
## Issues
|
||||
|
||||
Use [issues](https://github.com/dstotijn/hetty/issues) for reporting bugs,
|
||||
adding feature requests and giving context to PRs you submit. Please use [labels](https://github.com/dstotijn/hetty/labels)
|
||||
in favor of category prefixes in issue titles. To keep the issue tracker
|
||||
focused on development, use [discussions](https://github.com/dstotijn/hetty/discussions)
|
||||
for usage questions and non-code related discourse.
|
||||
|
||||
Before submitting new feature requests, check out the Kanban board for the
|
||||
status of on-going work. There might already be a card/issue.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Before submitting a pull request that introduces a new feature or significantly
|
||||
changes the behavior of Hetty, please consider first using [discussions](https://github.com/dstotijn/hetty/discussions)
|
||||
or commenting on a relevant existing issue to share what you have in mind.
|
||||
Because the project is in an early stage, this is especially important; there
|
||||
are still a lot of major design decisions to be made. Until the foundation has
|
||||
solidified, design and implementation leading up to the first milestone (v1.0)
|
||||
is highly in flux, and your work might not align/be applicable for what the
|
||||
maintainers have envisioned.
|
||||
|
||||
## Development
|
||||
|
||||
_Todo: Write steps for setting up local development environment._
|
30
Dockerfile
@ -1,14 +1,6 @@
|
||||
ARG GO_VERSION=1.15
|
||||
ARG CGO_ENABLED=0
|
||||
ARG NODE_VERSION=14.11
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine AS go-builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY cmd ./cmd
|
||||
COPY pkg ./pkg
|
||||
RUN go build -o hetty ./cmd/hetty
|
||||
ARG GO_VERSION=1.17
|
||||
ARG NODE_VERSION=16.13
|
||||
ARG ALPINE_VERSION=3.15
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine AS node-builder
|
||||
WORKDIR /app
|
||||
@ -18,11 +10,21 @@ COPY admin/ .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN yarn run export
|
||||
|
||||
FROM alpine:3.12
|
||||
FROM golang:${GO_VERSION}-alpine AS go-builder
|
||||
ARG HETTY_VERSION=0.0.0
|
||||
ENV CGO_ENABLED=0
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY cmd ./cmd
|
||||
COPY pkg ./pkg
|
||||
COPY --from=node-builder /app/dist ./cmd/hetty/admin
|
||||
RUN go build -ldflags="-s -w -X main.version=${HETTY_VERSION}" ./cmd/hetty
|
||||
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
WORKDIR /app
|
||||
COPY --from=go-builder /app/hetty .
|
||||
COPY --from=node-builder /app/dist admin
|
||||
|
||||
ENTRYPOINT ["./hetty", "-adminPath=./admin"]
|
||||
ENTRYPOINT ["./hetty"]
|
||||
|
||||
EXPOSE 8080
|
4
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 David Stotijn
|
||||
Copyright (c) 2021 David Stotijn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
32
Makefile
@ -1,20 +1,20 @@
|
||||
setup:
|
||||
go mod download
|
||||
go generate ./...
|
||||
.PHONY: setup
|
||||
export CGO_ENABLED = 0
|
||||
export NEXT_TELEMETRY_DISABLED = 1
|
||||
|
||||
embed:
|
||||
go install github.com/GeertJohan/go.rice/rice
|
||||
cd cmd/hetty && rice embed-go
|
||||
.PHONY: embed
|
||||
|
||||
build: embed
|
||||
go build ./cmd/hetty
|
||||
.PHONY: build
|
||||
build: build-admin
|
||||
go build ./cmd/hetty
|
||||
|
||||
.PHONY: build-admin
|
||||
build-admin:
|
||||
cd admin && \
|
||||
yarn install --frozen-lockfile && \
|
||||
yarn run export && \
|
||||
mv dist ../cmd/hetty/admin
|
||||
|
||||
clean:
|
||||
rm -rf cmd/hetty/rice-box.go
|
||||
.PHONY: clean
|
||||
|
||||
release:
|
||||
goreleaser -p 1
|
||||
clean:
|
||||
rm -f hetty
|
||||
rm -rf ./cmd/hetty/admin
|
||||
rm -rf ./admin/dist
|
||||
rm -rf ./admin/.next
|
197
README.md
@ -1,129 +1,148 @@
|
||||
<img src="https://i.imgur.com/AT71SBq.png" width="346" />
|
||||
<h1>
|
||||
<img src="https://hetty.xyz/img/hetty_light.svg#gh-light-mode-only" width="240"/>
|
||||
<img src="https://hetty.xyz/img/hetty_dark.svg#gh-dark-mode-only" width="240"/>
|
||||
</h1>
|
||||
|
||||
> Hetty is an HTTP toolkit for security research. It aims to become an open source
|
||||
> alternative to commercial software like Burp Suite Pro, with powerful features
|
||||
> tailored to the needs of the infosec and bug bounty community.
|
||||
[](https://github.com/dstotijn/hetty/releases/latest)
|
||||
[](https://github.com/dstotijn/hetty/actions/workflows/build-test.yml)
|
||||

|
||||
[](https://github.com/dstotijn/hetty/blob/master/LICENSE)
|
||||
[](https://hetty.xyz/)
|
||||
|
||||
<img src="https://i.imgur.com/ZZ6o83X.png">
|
||||
**Hetty** is an HTTP toolkit for security research. It aims to become an open
|
||||
source alternative to commercial software like Burp Suite Pro, with powerful
|
||||
features tailored to the needs of the infosec and bug bounty community.
|
||||
|
||||
## Features/to do
|
||||
<img src="https://hetty.xyz/img/hero.png" width="907" alt="Hetty proxy logs (screenshot)" />
|
||||
|
||||
- [x] HTTP man-in-the-middle (MITM) proxy and GraphQL server.
|
||||
- [x] Web interface (Next.js) with proxy log viewer.
|
||||
- [ ] Add scope support to the proxy.
|
||||
- [ ] Full text search (with regex) in proxy log viewer.
|
||||
- [ ] Project management.
|
||||
- [ ] Sender module for sending manual HTTP requests, either from scratch or based
|
||||
off requests from the proxy log.
|
||||
- [ ] Attacker module for automated sending of HTTP requests. Leverage the concurrency
|
||||
features of Go and its `net/http` package to make it blazingly fast.
|
||||
## Features
|
||||
|
||||
## Installation
|
||||
- Machine-in-the-middle (MITM) HTTP proxy, with logs and advanced search
|
||||
- HTTP client for manually creating/editing requests, and replay proxied requests
|
||||
- Scope support, to help keep work organized
|
||||
- Easy-to-use web based admin interface
|
||||
- Project based database storage, to help keep work organized
|
||||
|
||||
Hetty is packaged on GitHub as a single binary, with the web interface resources
|
||||
embedded.
|
||||
👷♂️ Hetty is under active development. Check the <a
|
||||
href="https://github.com/dstotijn/hetty/projects/1">backlog</a> for the current
|
||||
status.
|
||||
|
||||
👉 You can find downloads for Linux, macOS and Windows on the [releases page](https://github.com/dstotijn/hetty/releases).
|
||||
📣 Are you pen testing professionaly in a team? I would love to hear your
|
||||
thoughts on tooling via [this 5 minute
|
||||
survey](https://forms.gle/36jtgNc3TJ2imi5A8). Thank you!
|
||||
|
||||
### Alternatives:
|
||||
## Getting started
|
||||
|
||||
**Build from source**
|
||||
💡 The [Getting started](https://hetty.xyz/docs/getting-started) doc has more
|
||||
detailed install and usage instructions.
|
||||
|
||||
```
|
||||
$ GO111MODULE=auto go get -u -v github.com/dstotijn/hetty/cmd/hetty
|
||||
### Installation
|
||||
|
||||
The quickest way to install and update Hetty is via a package manager:
|
||||
|
||||
#### macOS
|
||||
|
||||
```sh
|
||||
brew install hettysoft/tap/hetty
|
||||
```
|
||||
|
||||
Then export the Next.js frontend app:
|
||||
#### Linux
|
||||
|
||||
```
|
||||
$ cd admin
|
||||
$ yarn install
|
||||
$ yarn export
|
||||
```sh
|
||||
sudo snap install hetty
|
||||
```
|
||||
|
||||
This will ensure a folder `./admin/dist` exists.
|
||||
Then, you can bundle the frontend app using `rice`.
|
||||
The easiest way to do this is via a supplied `Makefile` command in the root of
|
||||
the project:
|
||||
#### Windows
|
||||
|
||||
```
|
||||
make build
|
||||
```sh
|
||||
scoop bucket add hettysoft https://github.com/hettysoft/scoop.git
|
||||
scoop install hettysoft/hetty
|
||||
```
|
||||
|
||||
**Docker**
|
||||
#### Other
|
||||
|
||||
Alternatively, you can run Hetty via Docker. See: [`dstotijn/hetty`](https://hub.docker.com/r/dstotijn/hetty)
|
||||
on Docker Hub.
|
||||
Alternatively, you can [download the latest release from
|
||||
GitHub](https://github.com/dstotijn/hetty/releases/latest) for your OS and
|
||||
architecture, and move the binary to a directory in your `$PATH`. If your OS is
|
||||
not available for one of the package managers or not listed in the GitHub
|
||||
releases, you can compile from source _(link coming soon)_ or use a Docker image
|
||||
_(link coming soon)_.
|
||||
|
||||
```
|
||||
$ docker run \
|
||||
-v $HOME/.hetty/hetty_key.pem:/root/.hetty/hetty_key.pem \
|
||||
-v $HOME/.hetty/hetty_cert.pem:/root/.hetty/hetty_cert.pem \
|
||||
-v $HOME/.hetty/hetty.bolt:/root/.hetty/hetty.bolt \
|
||||
-p 127.0.0.1:8080:8080 \
|
||||
dstotijn/hetty
|
||||
### Usage
|
||||
|
||||
Once installed, start Hetty via:
|
||||
|
||||
```sh
|
||||
hetty
|
||||
```
|
||||
|
||||
## Usage
|
||||
💡 Read the [Getting started](https://hetty.xyz/docs/getting-started) doc for
|
||||
more details.
|
||||
|
||||
Hetty is packaged as a single binary, with the web interface resources embedded.
|
||||
When the program is run, it listens by default on `:8080` and is accessible via
|
||||
http://localhost:8080. Depending on incoming HTTP requests, it either acts as a
|
||||
MITM proxy, or it serves the GraphQL API and web interface (Next.js).
|
||||
To list all available options, run: `hetty --help`:
|
||||
|
||||
```
|
||||
$ Usage of ./hetty:
|
||||
-addr string
|
||||
TCP address to listen on, in the form "host:port" (default ":80")
|
||||
-adminPath string
|
||||
File path to admin build
|
||||
-cert string
|
||||
CA certificate filepath. Creates a new CA certificate is file doesn't exist (default "~/.hetty/hetty_cert.pem")
|
||||
-db string
|
||||
Database file path (default "hetty.db")
|
||||
-key string
|
||||
CA private key filepath. Creates a new CA private key if file doesn't exist (default "~/.hetty/hetty_key.pem")
|
||||
$ hetty --help
|
||||
|
||||
Usage:
|
||||
hetty [flags] [subcommand] [flags]
|
||||
|
||||
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface.
|
||||
|
||||
Options:
|
||||
--cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem")
|
||||
--key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem")
|
||||
--db Database directory path. (Default: "~/.hetty/db")
|
||||
--addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080")
|
||||
--chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false)
|
||||
--verbose Enable verbose logging.
|
||||
--json Encode logs as JSON, instead of pretty/human readable output.
|
||||
--version, -v Output version.
|
||||
--help, -h Output this usage text.
|
||||
|
||||
Subcommands:
|
||||
- cert Certificate management
|
||||
|
||||
Run `hetty <subcommand> --help` for subcommand specific usage instructions.
|
||||
|
||||
Visit https://hetty.xyz to learn more about Hetty.
|
||||
```
|
||||
|
||||
⚠️ _Todo: Write instructions for installing CA certificate in local CA store, and_
|
||||
_configuring Hetty to be used as a proxy server._
|
||||
## Documentation
|
||||
|
||||
## Vision and roadmap
|
||||
📖 [Read the docs](https://hetty.xyz/docs)
|
||||
|
||||
The project has just gotten underway, and as such I haven’t had time yet to do a
|
||||
write-up on its mission and roadmap. A short summary/braindump:
|
||||
## Support
|
||||
|
||||
- Fast core/engine, built with Go, with a minimal memory footprint.
|
||||
- GraphQL server to interact with the backend.
|
||||
- Easy to use web interface, built with Next.js and Material UI.
|
||||
- Extensibility is top of mind. All modules are written as Go packages, to
|
||||
be used by the main `hetty` program, but also usable as libraries for other software.
|
||||
Aside from the GraphQL server, it should (eventually) be possible to also use
|
||||
it as a CLI tool.
|
||||
- Pluggable architecture for the MITM proxy and future modules, making it
|
||||
possible for hook into the core engine.
|
||||
- I’ve chosen [Cayley](https://cayley.io/) as the graph database (backed by
|
||||
BoltDB storage on disk) for now (not sure if it will work in the long run).
|
||||
The benefit is that Cayley (also written in Go)
|
||||
is embedded as a library. Because of this, the complete application is self contained
|
||||
in a single running binary.
|
||||
- Talk to the community, and focus on the features that the majority.
|
||||
Less features means less code to maintain.
|
||||
Use [issues](https://github.com/dstotijn/hetty/issues) for bug reports and
|
||||
feature requests, and
|
||||
[discussions](https://github.com/dstotijn/hetty/discussions) for questions and
|
||||
troubleshooting.
|
||||
|
||||
## Status
|
||||
## Community
|
||||
|
||||
The project is currently under active development. Please star/follow and check
|
||||
back soon. 🤗
|
||||
💬 [Join the Hetty Discord server](https://discord.gg/3HVsj5pTFP)
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to contribute? Great! Please check the [Contribution
|
||||
Guidelines](CONTRIBUTING.md) for details.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Thanks to the [Hacker101 community on Discord](https://discordapp.com/channels/514337135491416065)
|
||||
for all the encouragement to actually start building this thing!
|
||||
- Thanks to the [Hacker101 community on Discord](https://www.hacker101.com/discord)
|
||||
for the encouragement and early feedback.
|
||||
- The font used in the logo and admin interface is [JetBrains
|
||||
Mono](https://www.jetbrains.com/lp/mono/).
|
||||
|
||||
## Sponsors
|
||||
|
||||
<a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty">
|
||||
<img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
© 2020 David Stotijn — [Twitter](https://twitter.com/dstotijn), [Email](mailto:dstotijn@gmail.com)
|
||||
© 2022 Hetty Software
|
||||
|
51
admin/.eslintrc.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["next/core-web-vitals", "prettier", "plugin:@typescript-eslint/recommended", "plugin:import/typescript"],
|
||||
"plugins": ["prettier", "@typescript-eslint", "import"],
|
||||
"ignorePatterns": ["next*", "src/lib/graphql/generated.tsx"],
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": ["error"],
|
||||
"@next/next/no-css-tags": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
|
||||
"import/default": "off",
|
||||
|
||||
"import/no-unresolved": "error",
|
||||
"import/named": "error",
|
||||
"import/namespace": "error",
|
||||
"import/export": "error",
|
||||
"import/no-deprecated": "error",
|
||||
"import/no-cycle": "error",
|
||||
|
||||
"import/no-named-as-default": "warn",
|
||||
"import/no-named-as-default-member": "warn",
|
||||
"import/no-duplicates": "warn",
|
||||
"import/newline-after-import": "warn",
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
"alphabetize": { "order": "asc", "caseInsensitive": false },
|
||||
"newlines-between": "always",
|
||||
"groups": ["builtin", "external", "parent", "sibling", "index"]
|
||||
}
|
||||
],
|
||||
"import/no-unused-modules": [
|
||||
"error",
|
||||
{
|
||||
"missingExports": true,
|
||||
"ignoreExports": ["./src/pages"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
4
admin/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
/.next/
|
||||
/out/
|
||||
/build
|
||||
/coverage
|
3
admin/.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 120
|
||||
}
|
9
admin/gqlcodegen.yml
Normal file
@ -0,0 +1,9 @@
|
||||
overwrite: true
|
||||
schema: "../pkg/api/schema.graphql"
|
||||
documents: "src/**/*.graphql"
|
||||
generates:
|
||||
src/lib/graphql/generated.tsx:
|
||||
plugins:
|
||||
- "typescript"
|
||||
- "typescript-operations"
|
||||
- "typescript-react-apollo"
|
5
admin/next-env.d.ts
vendored
@ -1,2 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
@ -1,7 +1,10 @@
|
||||
const withCSS = require("@zeit/next-css");
|
||||
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
|
||||
// @ts-check
|
||||
|
||||
module.exports = withCSS({
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
**/
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
trailingSlash: true,
|
||||
async rewrites() {
|
||||
return [
|
||||
@ -11,24 +14,6 @@ module.exports = withCSS({
|
||||
},
|
||||
];
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.module.rules.push({
|
||||
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
|
||||
use: {
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 100000,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
config.plugins.push(
|
||||
new MonacoWebpackPlugin({
|
||||
languages: ["html", "json", "javascript"],
|
||||
filename: "static/[name].worker.js",
|
||||
})
|
||||
);
|
||||
|
||||
return config;
|
||||
},
|
||||
});
|
||||
module.exports = nextConfig;
|
||||
|
@ -6,31 +6,51 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"export": "next build && next export -o dist"
|
||||
"lint": "next lint",
|
||||
"export": "next build && next export -o dist",
|
||||
"generate": "graphql-codegen --config gqlcodegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.2.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||
"@zeit/next-css": "^1.0.1",
|
||||
"graphql": "^15.3.0",
|
||||
"monaco-editor": "^0.20.0",
|
||||
"monaco-editor-webpack-plugin": "^1.9.0",
|
||||
"next": "^9.5.3",
|
||||
"@emotion/react": "^11.7.1",
|
||||
"@emotion/server": "^11.4.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@mui/icons-material": "^5.3.1",
|
||||
"@mui/lab": "^5.0.0-alpha.66",
|
||||
"@mui/material": "^5.3.1",
|
||||
"@mui/styles": "^5.4.2",
|
||||
"allotment": "^1.9.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"graphql": "^16.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"monaco-editor": "^0.31.1",
|
||||
"next": "^12.0.8",
|
||||
"next-fonts": "^1.0.3",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-monaco-editor": "^0.34.0",
|
||||
"react-syntax-highlighter": "^13.5.3",
|
||||
"typescript": "^4.0.3"
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-split-pane": "^0.1.92"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.11.1",
|
||||
"@types/react": "^16.9.49",
|
||||
"eslint": "^7.9.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"prettier": "^2.1.2"
|
||||
"@babel/core": "^7.0.0",
|
||||
"@graphql-codegen/cli": "2.6.1",
|
||||
"@graphql-codegen/introspection": "2.1.1",
|
||||
"@graphql-codegen/typescript": "2.4.3",
|
||||
"@graphql-codegen/typescript-operations": "2.3.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "3.2.6",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^17.0.12",
|
||||
"@types/react": "^17.0.38",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||
"@typescript-eslint/parser": "^5.12.0",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-next": "12.0.8",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"prettier": "^2.1.2",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^5.67.0"
|
||||
}
|
||||
}
|
||||
|
BIN
admin/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
admin/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
admin/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
admin/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 454 B |
BIN
admin/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
1
admin/public/site.webmanifest
Normal file
@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
@ -1,25 +0,0 @@
|
||||
import { Paper } from "@material-ui/core";
|
||||
|
||||
function CenteredPaper({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Paper
|
||||
elevation={0}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 36,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CenteredPaper;
|
@ -1,246 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
makeStyles,
|
||||
Theme,
|
||||
createStyles,
|
||||
useTheme,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
Typography,
|
||||
Drawer,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import Link from "next/link";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
import HomeIcon from "@material-ui/icons/Home";
|
||||
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
|
||||
import SendIcon from "@material-ui/icons/Send";
|
||||
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
||||
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
||||
import clsx from "clsx";
|
||||
|
||||
export enum Page {
|
||||
Home,
|
||||
ProxySetup,
|
||||
ProxyLogs,
|
||||
Sender,
|
||||
}
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: 28,
|
||||
},
|
||||
hide: {
|
||||
display: "none",
|
||||
},
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
drawerOpen: {
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
drawerClose: {
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: "hidden",
|
||||
width: theme.spacing(7) + 1,
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
width: theme.spacing(7) + 8,
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
padding: theme.spacing(0, 1),
|
||||
// necessary for content to be below app bar
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
listItem: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
},
|
||||
},
|
||||
listItemIcon: {
|
||||
minWidth: 42,
|
||||
},
|
||||
titleHighlight: {
|
||||
color: theme.palette.secondary.main,
|
||||
marginRight: 4,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
page: Page;
|
||||
}
|
||||
|
||||
export function Layout({ title, page, children }: Props): JSX.Element {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar
|
||||
position="fixed"
|
||||
className={clsx(classes.appBar, {
|
||||
[classes.appBarShift]: open,
|
||||
})}
|
||||
>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={handleDrawerOpen}
|
||||
edge="start"
|
||||
className={clsx(classes.menuButton, {
|
||||
[classes.hide]: open,
|
||||
})}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h5" noWrap>
|
||||
<span className={title !== "" && classes.titleHighlight}>
|
||||
Hetty://
|
||||
</span>
|
||||
{title}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
className={clsx(classes.drawer, {
|
||||
[classes.drawerOpen]: open,
|
||||
[classes.drawerClose]: !open,
|
||||
})}
|
||||
classes={{
|
||||
paper: clsx({
|
||||
[classes.drawerOpen]: open,
|
||||
[classes.drawerClose]: !open,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div className={classes.toolbar}>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === "rtl" ? (
|
||||
<ChevronRightIcon />
|
||||
) : (
|
||||
<ChevronLeftIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
<Divider />
|
||||
<List>
|
||||
<Link href="/" passHref>
|
||||
<ListItem
|
||||
button
|
||||
component="a"
|
||||
key="home"
|
||||
selected={page === Page.Home}
|
||||
className={classes.listItem}
|
||||
>
|
||||
<Tooltip title="Home">
|
||||
<ListItemIcon className={classes.listItemIcon}>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Home" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link href="/proxy/logs" passHref>
|
||||
<ListItem
|
||||
button
|
||||
component="a"
|
||||
key="proxyLogs"
|
||||
selected={page === Page.ProxyLogs}
|
||||
className={classes.listItem}
|
||||
>
|
||||
<Tooltip title="Proxy">
|
||||
<ListItemIcon className={classes.listItemIcon}>
|
||||
<SettingsEthernetIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Proxy" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link href="/sender" passHref>
|
||||
<ListItem
|
||||
button
|
||||
component="a"
|
||||
key="sender"
|
||||
selected={page === Page.Sender}
|
||||
className={classes.listItem}
|
||||
>
|
||||
<Tooltip title="Sender">
|
||||
<ListItemIcon className={classes.listItemIcon}>
|
||||
<SendIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Sender" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
</List>
|
||||
</Drawer>
|
||||
<main className={classes.content}>
|
||||
<div className={classes.toolbar} />
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
@ -1,60 +0,0 @@
|
||||
import dynamic from "next/dynamic";
|
||||
const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false });
|
||||
|
||||
const monacoOptions = {
|
||||
readOnly: true,
|
||||
wordWrap: "on",
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
type language = "html" | "typescript" | "json";
|
||||
|
||||
function editorDidMount() {
|
||||
return ((window as any).MonacoEnvironment.getWorkerUrl = (
|
||||
moduleId,
|
||||
label
|
||||
) => {
|
||||
if (label === "json") return "/_next/static/json.worker.js";
|
||||
if (label === "html") return "/_next/static/html.worker.js";
|
||||
if (label === "javascript") return "/_next/static/ts.worker.js";
|
||||
|
||||
return "/_next/static/editor.worker.js";
|
||||
});
|
||||
}
|
||||
|
||||
function languageForContentType(contentType: string): language {
|
||||
switch (contentType) {
|
||||
case "text/html":
|
||||
return "html";
|
||||
case "application/json":
|
||||
case "application/json; charset=utf-8":
|
||||
return "json";
|
||||
case "application/javascript":
|
||||
case "application/javascript; charset=utf-8":
|
||||
return "typescript";
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
function Editor({ content, contentType }: Props): JSX.Element {
|
||||
return (
|
||||
<MonacoEditor
|
||||
height={"600px"}
|
||||
language={languageForContentType(contentType)}
|
||||
theme="vs-dark"
|
||||
editorDidMount={editorDidMount}
|
||||
options={monacoOptions as any}
|
||||
value={content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Editor;
|
@ -1,120 +0,0 @@
|
||||
import {
|
||||
makeStyles,
|
||||
Theme,
|
||||
createStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Snackbar,
|
||||
} from "@material-ui/core";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
const paddingX = 0;
|
||||
const paddingY = theme.spacing(1) / 3;
|
||||
const tableCell = {
|
||||
paddingLeft: paddingX,
|
||||
paddingRight: paddingX,
|
||||
paddingTop: paddingY,
|
||||
paddingBottom: paddingY,
|
||||
verticalAlign: "top",
|
||||
border: "none",
|
||||
whiteSpace: "nowrap" as any,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
"&:hover": {
|
||||
color: theme.palette.secondary.main,
|
||||
whiteSpace: "inherit" as any,
|
||||
overflow: "inherit",
|
||||
textOverflow: "inherit",
|
||||
cursor: "copy",
|
||||
},
|
||||
};
|
||||
return createStyles({
|
||||
root: {},
|
||||
table: {
|
||||
tableLayout: "fixed",
|
||||
width: "100%",
|
||||
},
|
||||
keyCell: {
|
||||
...tableCell,
|
||||
paddingRight: theme.spacing(1),
|
||||
width: "40%",
|
||||
fontWeight: "bold",
|
||||
fontSize: ".75rem",
|
||||
},
|
||||
valueCell: {
|
||||
...tableCell,
|
||||
width: "60%",
|
||||
border: "none",
|
||||
fontSize: ".75rem",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
interface Props {
|
||||
headers: Array<{ key: string; value: string }>;
|
||||
}
|
||||
|
||||
function HttpHeadersTable({ headers }: Props): JSX.Element {
|
||||
const classes = useStyles();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const r = document.createRange();
|
||||
r.selectNode(e.currentTarget);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(r);
|
||||
document.execCommand("copy");
|
||||
window.getSelection().removeAllRanges();
|
||||
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = (event?: React.SyntheticEvent, reason?: string) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Snackbar open={open} autoHideDuration={3000} onClose={handleClose}>
|
||||
<Alert onClose={handleClose} severity="info">
|
||||
Copied to clipboard.
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<TableContainer className={classes.root}>
|
||||
<Table className={classes.table} size="small">
|
||||
<TableBody>
|
||||
{headers.map(({ key, value }, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
className={classes.keyCell}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<code>{key}:</code>
|
||||
</TableCell>
|
||||
<TableCell className={classes.valueCell} onClick={handleClick}>
|
||||
<code>{value}</code>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HttpHeadersTable;
|
@ -1,31 +0,0 @@
|
||||
import { Theme, withTheme } from "@material-ui/core";
|
||||
import { orange, red } from "@material-ui/core/colors";
|
||||
import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
|
||||
|
||||
interface Props {
|
||||
status: number;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
function HttpStatusIcon({ status, theme }: Props): JSX.Element {
|
||||
const style = { marginTop: "-.25rem", verticalAlign: "middle" };
|
||||
switch (Math.floor(status / 100)) {
|
||||
case 2:
|
||||
case 3:
|
||||
return (
|
||||
<FiberManualRecordIcon
|
||||
style={{ ...style, color: theme.palette.secondary.main }}
|
||||
/>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<FiberManualRecordIcon style={{ ...style, color: orange["A400"] }} />
|
||||
);
|
||||
case 5:
|
||||
return <FiberManualRecordIcon style={{ ...style, color: red["A400"] }} />;
|
||||
default:
|
||||
return <FiberManualRecordIcon style={style} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(HttpStatusIcon);
|
@ -1,84 +0,0 @@
|
||||
import { gql, useQuery } from "@apollo/client";
|
||||
import { Box, Grid, Paper, CircularProgress } from "@material-ui/core";
|
||||
|
||||
import ResponseDetail from "./ResponseDetail";
|
||||
import RequestDetail from "./RequestDetail";
|
||||
import Alert from "@material-ui/lab/Alert";
|
||||
|
||||
const HTTP_REQUEST_LOG = gql`
|
||||
query HttpRequestLog($id: ID!) {
|
||||
httpRequestLog(id: $id) {
|
||||
id
|
||||
method
|
||||
url
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
response {
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
status
|
||||
statusCode
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
function LogDetail({ requestId: id }: Props): JSX.Element {
|
||||
const { loading, error, data } = useQuery(HTTP_REQUEST_LOG, {
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Error fetching logs details: {error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.httpRequestLog) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
Request <strong>{id}</strong> was not found.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const { method, url, proto, headers, body, response } = data.httpRequestLog;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container item spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Box component={Paper}>
|
||||
<RequestDetail request={{ method, url, proto, headers, body }} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
{response && (
|
||||
<Box component={Paper}>
|
||||
<ResponseDetail response={response} />
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogDetail;
|
@ -1,71 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { gql, useQuery } from "@apollo/client";
|
||||
import { useState } from "react";
|
||||
import { Box, Typography, CircularProgress } 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 {
|
||||
status
|
||||
statusCode
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function LogsOverview(): JSX.Element {
|
||||
const router = useRouter();
|
||||
const detailReqLogId = router.query.id as string;
|
||||
console.log(detailReqLogId);
|
||||
|
||||
const { loading, error, data } = useQuery(HTTP_REQUEST_LOGS, {
|
||||
pollInterval: 1000,
|
||||
});
|
||||
|
||||
const handleLogClick = (reqId: string) => {
|
||||
router.push("/proxy/logs?id=" + reqId, undefined, {
|
||||
shallow: false,
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
if (error) {
|
||||
return <Alert severity="error">Error fetching logs: {error.message}</Alert>;
|
||||
}
|
||||
|
||||
const { httpRequestLogs: logs } = data;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box mb={2}>
|
||||
<RequestList
|
||||
logs={logs}
|
||||
selectedReqLogId={detailReqLogId}
|
||||
onLogClick={handleLogClick}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
{detailReqLogId && <LogDetail requestId={detailReqLogId} />}
|
||||
{logs.length !== 0 && !detailReqLogId && (
|
||||
<CenteredPaper>
|
||||
<Typography>Select a log entry…</Typography>
|
||||
</CenteredPaper>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsOverview;
|
@ -1,91 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Divider,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import HttpHeadersTable from "./HttpHeadersTable";
|
||||
import Editor from "./Editor";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
requestTitle: {
|
||||
width: "calc(100% - 80px)",
|
||||
fontSize: "1rem",
|
||||
wordBreak: "break-all",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
headersTable: {
|
||||
tableLayout: "fixed",
|
||||
width: "100%",
|
||||
},
|
||||
headerKeyCell: {
|
||||
verticalAlign: "top",
|
||||
width: "30%",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
headerValueCell: {
|
||||
width: "70%",
|
||||
verticalAlign: "top",
|
||||
wordBreak: "break-all",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
request: {
|
||||
method: string;
|
||||
url: string;
|
||||
proto: string;
|
||||
headers: Array<{ key: string; value: string }>;
|
||||
body?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function RequestDetail({ request }: Props): JSX.Element {
|
||||
const { method, url, proto, headers, body } = request;
|
||||
const classes = useStyles();
|
||||
|
||||
const contentType = headers.find((header) => header.key === "Content-Type")
|
||||
?.value;
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box p={2}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
color="textSecondary"
|
||||
style={{ float: "right" }}
|
||||
>
|
||||
Request
|
||||
</Typography>
|
||||
<Typography className={classes.requestTitle} variant="h6">
|
||||
{method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
color="textSecondary"
|
||||
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
||||
>
|
||||
{proto}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box p={2}>
|
||||
<HttpHeadersTable headers={headers} />
|
||||
</Box>
|
||||
|
||||
{body && <Editor content={body} contentType={contentType} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RequestDetail;
|
@ -1,144 +0,0 @@
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Typography,
|
||||
Box,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
withTheme,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import HttpStatusIcon from "./HttpStatusCode";
|
||||
import CenteredPaper from "../CenteredPaper";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
row: {
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
},
|
||||
/* Pseudo-class applied to the root element if `hover={true}`. */
|
||||
hover: {},
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
logs: Array<any>;
|
||||
selectedReqLogId?: string;
|
||||
onLogClick(requestId: string): void;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
function RequestList({
|
||||
logs,
|
||||
onLogClick,
|
||||
selectedReqLogId,
|
||||
theme,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<RequestListTable
|
||||
onLogClick={onLogClick}
|
||||
logs={logs}
|
||||
selectedReqLogId={selectedReqLogId}
|
||||
theme={theme}
|
||||
/>
|
||||
{logs.length === 0 && (
|
||||
<Box my={1}>
|
||||
<CenteredPaper>
|
||||
<Typography>No logs found.</Typography>
|
||||
</CenteredPaper>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RequestListTableProps {
|
||||
logs?: any;
|
||||
selectedReqLogId?: string;
|
||||
onLogClick(requestId: string): void;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
function RequestListTable({
|
||||
logs,
|
||||
selectedReqLogId,
|
||||
onLogClick,
|
||||
theme,
|
||||
}: RequestListTableProps): JSX.Element {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
style={{
|
||||
minHeight: logs.length ? 200 : 0,
|
||||
height: logs.length ? "24vh" : "inherit",
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Method</TableCell>
|
||||
<TableCell>Origin</TableCell>
|
||||
<TableCell>Path</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{logs.map(({ id, method, url, response }) => {
|
||||
const { origin, pathname, search, hash } = new URL(url);
|
||||
|
||||
const cellStyle = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
} as any;
|
||||
|
||||
const rowStyle = {
|
||||
backgroundColor:
|
||||
id === selectedReqLogId && theme.palette.action.selected,
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={id}
|
||||
className={classes.row}
|
||||
style={rowStyle}
|
||||
hover
|
||||
onClick={() => onLogClick(id)}
|
||||
>
|
||||
<TableCell style={{ ...cellStyle, width: "100px" }}>
|
||||
<code>{method}</code>
|
||||
</TableCell>
|
||||
<TableCell style={{ ...cellStyle, maxWidth: "100px" }}>
|
||||
{origin}
|
||||
</TableCell>
|
||||
<TableCell style={{ ...cellStyle, maxWidth: "200px" }}>
|
||||
{decodeURIComponent(pathname + search + hash)}
|
||||
</TableCell>
|
||||
<TableCell style={{ maxWidth: "100px" }}>
|
||||
{response && (
|
||||
<div>
|
||||
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||
<code>{response.status}</code>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(RequestList);
|
@ -1,62 +0,0 @@
|
||||
import { Typography, Box, Divider } from "@material-ui/core";
|
||||
|
||||
import HttpStatusIcon from "./HttpStatusCode";
|
||||
import Editor from "./Editor";
|
||||
import HttpHeadersTable from "./HttpHeadersTable";
|
||||
|
||||
interface Props {
|
||||
response: {
|
||||
proto: string;
|
||||
statusCode: number;
|
||||
status: string;
|
||||
headers: Array<{ key: string; value: string }>;
|
||||
body?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function ResponseDetail({ response }: Props): JSX.Element {
|
||||
const contentType = response.headers.find(
|
||||
(header) => header.key === "Content-Type"
|
||||
)?.value;
|
||||
return (
|
||||
<div>
|
||||
<Box p={2}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
color="textSecondary"
|
||||
style={{ float: "right" }}
|
||||
>
|
||||
Response
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
style={{ fontSize: "1rem", whiteSpace: "nowrap" }}
|
||||
>
|
||||
<HttpStatusIcon status={response.statusCode} />{" "}
|
||||
<Typography component="span" color="textSecondary">
|
||||
<Typography
|
||||
component="span"
|
||||
color="textSecondary"
|
||||
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
||||
>
|
||||
{response.proto}
|
||||
</Typography>
|
||||
</Typography>{" "}
|
||||
{response.status}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box p={2}>
|
||||
<HttpHeadersTable headers={response.headers} />
|
||||
</Box>
|
||||
|
||||
{response.body && (
|
||||
<Editor content={response.body} contentType={contentType} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResponseDetail;
|
252
admin/src/features/Layout.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import LocationSearchingIcon from "@mui/icons-material/LocationSearching";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet";
|
||||
import {
|
||||
Theme,
|
||||
useTheme,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
Typography,
|
||||
Divider,
|
||||
List,
|
||||
Tooltip,
|
||||
styled,
|
||||
CSSObject,
|
||||
Box,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar";
|
||||
import MuiDrawer from "@mui/material/Drawer";
|
||||
import MuiListItemButton, { ListItemButtonProps } from "@mui/material/ListItemButton";
|
||||
import MuiListItemIcon, { ListItemIconProps } from "@mui/material/ListItemIcon";
|
||||
import Link from "next/link";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useActiveProject } from "lib/ActiveProjectContext";
|
||||
|
||||
export enum Page {
|
||||
Home,
|
||||
GetStarted,
|
||||
Projects,
|
||||
ProxySetup,
|
||||
ProxyLogs,
|
||||
Sender,
|
||||
Scope,
|
||||
}
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
const openedMixin = (theme: Theme): CSSObject => ({
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
overflowX: "hidden",
|
||||
});
|
||||
|
||||
const closedMixin = (theme: Theme): CSSObject => ({
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: "hidden",
|
||||
width: 56,
|
||||
});
|
||||
|
||||
const DrawerHeader = styled("div")(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
padding: theme.spacing(0, 1),
|
||||
// necessary for content to be below app bar
|
||||
...theme.mixins.toolbar,
|
||||
}));
|
||||
|
||||
interface AppBarProps extends MuiAppBarProps {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
const AppBar = styled(MuiAppBar, {
|
||||
shouldForwardProp: (prop) => prop !== "open",
|
||||
})<AppBarProps>(({ theme, open }) => ({
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
...(open && {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: "nowrap",
|
||||
boxSizing: "border-box",
|
||||
...(open && {
|
||||
...openedMixin(theme),
|
||||
"& .MuiDrawer-paper": openedMixin(theme),
|
||||
}),
|
||||
...(!open && {
|
||||
...closedMixin(theme),
|
||||
"& .MuiDrawer-paper": closedMixin(theme),
|
||||
}),
|
||||
}));
|
||||
|
||||
const ListItemButton = styled(MuiListItemButton)<ListItemButtonProps>(({ theme }) => ({
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
px: 1,
|
||||
},
|
||||
"&.MuiListItemButton-root": {
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
"& .MuiListItemIcon-root": {
|
||||
color: theme.palette.secondary.dark,
|
||||
},
|
||||
"& .MuiListItemText-root": {
|
||||
color: theme.palette.secondary.dark,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const ListItemIcon = styled(MuiListItemIcon)<ListItemIconProps>(() => ({
|
||||
minWidth: 42,
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
page: Page;
|
||||
}
|
||||
|
||||
export function Layout({ title, page, children }: Props): JSX.Element {
|
||||
const activeProject = useActiveProject();
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const SiteTitle = styled("span")({
|
||||
...(title !== "" && {
|
||||
color: theme.palette.primary.main,
|
||||
marginRight: 4,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", height: "100%" }}>
|
||||
<AppBar position="fixed" open={open}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="Open drawer"
|
||||
onClick={handleDrawerOpen}
|
||||
edge="start"
|
||||
sx={{
|
||||
mr: 5,
|
||||
...(open && { display: "none" }),
|
||||
}}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-around",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" noWrap sx={{ width: "100%" }}>
|
||||
<SiteTitle>Hetty://</SiteTitle>
|
||||
{title}
|
||||
</Typography>
|
||||
<Box sx={{ flexShrink: 0, pt: 0.75 }}>v{process.env.NEXT_PUBLIC_VERSION || "0.0"}</Box>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer variant="permanent" open={open}>
|
||||
<DrawerHeader>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === "rtl" ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||
</IconButton>
|
||||
</DrawerHeader>
|
||||
<Divider />
|
||||
<List sx={{ p: 0 }}>
|
||||
<Link href="/" passHref>
|
||||
<ListItemButton key="home" selected={page === Page.Home}>
|
||||
<Tooltip title="Home">
|
||||
<ListItemIcon>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Home" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link href="/proxy/logs" passHref>
|
||||
<ListItemButton key="proxyLogs" disabled={!activeProject} selected={page === Page.ProxyLogs}>
|
||||
<Tooltip title="Proxy">
|
||||
<ListItemIcon>
|
||||
<SettingsEthernetIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Proxy" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link href="/sender" passHref>
|
||||
<ListItemButton key="sender" disabled={!activeProject} selected={page === Page.Sender}>
|
||||
<Tooltip title="Sender">
|
||||
<ListItemIcon>
|
||||
<SendIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Sender" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link href="/scope" passHref>
|
||||
<ListItemButton key="scope" disabled={!activeProject} selected={page === Page.Scope}>
|
||||
<Tooltip title="Scope">
|
||||
<ListItemIcon>
|
||||
<LocationSearchingIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Scope" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link href="/projects" passHref>
|
||||
<ListItemButton key="projects" selected={page === Page.Projects}>
|
||||
<Tooltip title="Projects">
|
||||
<ListItemIcon>
|
||||
<FolderIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Projects" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
</List>
|
||||
</Drawer>
|
||||
<Box component="main" sx={{ flexGrow: 1, mx: 3, mt: 11 }}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
67
admin/src/features/projects/components/NewProject.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { Box, Button, CircularProgress, TextField, Typography } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import useOpenProjectMutation from "../hooks/useOpenProjectMutation";
|
||||
|
||||
import { useCreateProjectMutation } from "lib/graphql/generated";
|
||||
|
||||
function NewProject(): JSX.Element {
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const [createProject, createProjResult] = useCreateProjectMutation({
|
||||
onCompleted(data) {
|
||||
setName("");
|
||||
if (data?.createProject) {
|
||||
openProject({ variables: { id: data.createProject?.id } });
|
||||
}
|
||||
},
|
||||
});
|
||||
const [openProject, openProjResult] = useOpenProjectMutation();
|
||||
|
||||
const handleCreateAndOpenProjectForm = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
createProject({ variables: { name } });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box mb={3}>
|
||||
<Typography variant="h6">New project</Typography>
|
||||
</Box>
|
||||
<form onSubmit={handleCreateAndOpenProjectForm} autoComplete="off">
|
||||
<TextField
|
||||
sx={{
|
||||
mr: 2,
|
||||
}}
|
||||
color="primary"
|
||||
size="small"
|
||||
label="Project name"
|
||||
placeholder="Project name…"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
error={Boolean(createProjResult.error || openProjResult.error)}
|
||||
helperText={
|
||||
(createProjResult.error && createProjResult.error.message) ||
|
||||
(openProjResult.error && openProjResult.error.message)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
sx={{
|
||||
pt: 0.9,
|
||||
pb: 0.7,
|
||||
}}
|
||||
disabled={createProjResult.loading || openProjResult.loading}
|
||||
startIcon={createProjResult.loading || openProjResult.loading ? <CircularProgress size={22} /> : <AddIcon />}
|
||||
>
|
||||
Create & open project
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewProject;
|
225
admin/src/features/projects/components/ProjectList.tsx
Normal file
@ -0,0 +1,225 @@
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import DescriptionIcon from "@mui/icons-material/Description";
|
||||
import LaunchIcon from "@mui/icons-material/Launch";
|
||||
import { Alert } from "@mui/lab";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Snackbar,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import useOpenProjectMutation from "../hooks/useOpenProjectMutation";
|
||||
|
||||
import {
|
||||
ProjectsQuery,
|
||||
useCloseProjectMutation,
|
||||
useDeleteProjectMutation,
|
||||
useProjectsQuery,
|
||||
} from "lib/graphql/generated";
|
||||
|
||||
function ProjectList(): JSX.Element {
|
||||
const theme = useTheme();
|
||||
const projResult = useProjectsQuery({ fetchPolicy: "network-only" });
|
||||
const [openProject, openProjResult] = useOpenProjectMutation();
|
||||
const [closeProject, closeProjResult] = useCloseProjectMutation({
|
||||
errorPolicy: "all",
|
||||
onCompleted() {
|
||||
closeProjResult.client.resetStore();
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
activeProject() {
|
||||
return null;
|
||||
},
|
||||
projects(_, { DELETE }) {
|
||||
return DELETE;
|
||||
},
|
||||
httpRequestLogFilter(_, { DELETE }) {
|
||||
return DELETE;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const [deleteProject, deleteProjResult] = useDeleteProjectMutation({
|
||||
errorPolicy: "all",
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
projects(_, { DELETE }) {
|
||||
return DELETE;
|
||||
},
|
||||
},
|
||||
});
|
||||
setDeleteDiagOpen(false);
|
||||
setDeleteNotifOpen(true);
|
||||
},
|
||||
});
|
||||
|
||||
const [deleteProj, setDeleteProj] = useState<ProjectsQuery["projects"][number]>();
|
||||
const [deleteDiagOpen, setDeleteDiagOpen] = useState(false);
|
||||
const handleDeleteButtonClick = (project: ProjectsQuery["projects"][number]) => {
|
||||
setDeleteProj(project);
|
||||
setDeleteDiagOpen(true);
|
||||
};
|
||||
const handleDeleteConfirm = () => {
|
||||
if (deleteProj) {
|
||||
deleteProject({ variables: { id: deleteProj.id } });
|
||||
}
|
||||
};
|
||||
const handleDeleteCancel = () => {
|
||||
setDeleteDiagOpen(false);
|
||||
};
|
||||
|
||||
const [deleteNotifOpen, setDeleteNotifOpen] = useState(false);
|
||||
const handleCloseDeleteNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
setDeleteNotifOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={deleteDiagOpen} onClose={handleDeleteCancel}>
|
||||
<DialogTitle>
|
||||
Delete project “<strong>{deleteProj?.name}</strong>”?
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Deleting a project permanently removes all its data from the database. This action is irreversible.
|
||||
</DialogContentText>
|
||||
{deleteProjResult.error && (
|
||||
<Alert severity="error">Error closing project: {deleteProjResult.error.message}</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDeleteCancel} autoFocus color="secondary" variant="contained">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
color: "white",
|
||||
backgroundColor: "error.main",
|
||||
"&:hover": {
|
||||
backgroundColor: "error.dark",
|
||||
},
|
||||
}}
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={deleteProjResult.loading}
|
||||
variant="contained"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Snackbar
|
||||
open={deleteNotifOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleCloseDeleteNotif}
|
||||
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
|
||||
>
|
||||
<Alert onClose={handleCloseDeleteNotif} severity="info">
|
||||
Project <strong>{deleteProj?.name}</strong> was deleted.
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography variant="h6">Manage projects</Typography>
|
||||
</Box>
|
||||
|
||||
<Box mb={4}>
|
||||
{projResult.loading && <CircularProgress />}
|
||||
{projResult.error && <Alert severity="error">Error fetching projects: {projResult.error.message}</Alert>}
|
||||
{openProjResult.error && <Alert severity="error">Error opening project: {openProjResult.error.message}</Alert>}
|
||||
{closeProjResult.error && (
|
||||
<Alert severity="error">Error closing project: {closeProjResult.error.message}</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{projResult.data && projResult.data.projects.length > 0 && (
|
||||
<Paper>
|
||||
<List>
|
||||
{projResult.data.projects.map((project) => (
|
||||
<ListItem key={project.id}>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
...(project.isActive && {
|
||||
color: theme.palette.secondary.dark,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<DescriptionIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
{project.name} {project.isActive && <em>(Active)</em>}
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
{project.isActive && (
|
||||
<Tooltip title="Close project">
|
||||
<IconButton onClick={() => closeProject()}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!project.isActive && (
|
||||
<Tooltip title="Open project">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={openProjResult.loading || projResult.loading}
|
||||
onClick={() =>
|
||||
openProject({
|
||||
variables: { id: project.id },
|
||||
})
|
||||
}
|
||||
>
|
||||
<LaunchIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Delete project">
|
||||
<span>
|
||||
<IconButton onClick={() => handleDeleteButtonClick(project)} disabled={project.isActive}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
{projResult.data?.projects.length === 0 && (
|
||||
<Alert severity="info">There are no projects. Create one to get started.</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectList;
|
5
admin/src/features/projects/graphql/closeProject.graphql
Normal file
@ -0,0 +1,5 @@
|
||||
mutation CloseProject {
|
||||
closeProject {
|
||||
success
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
mutation CreateProject($name: String!) {
|
||||
createProject(name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mutation DeleteProject($id: ID!) {
|
||||
deleteProject(id: $id) {
|
||||
success
|
||||
}
|
||||
}
|
7
admin/src/features/projects/graphql/openProject.graphql
Normal file
@ -0,0 +1,7 @@
|
||||
mutation OpenProject($id: ID!) {
|
||||
openProject(id: $id) {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
}
|
||||
}
|
7
admin/src/features/projects/graphql/projects.graphql
Normal file
@ -0,0 +1,7 @@
|
||||
query Projects {
|
||||
projects {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
}
|
||||
}
|
47
admin/src/features/projects/hooks/useOpenProjectMutation.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
import { useOpenProjectMutation as _useOpenProjectMutation } from "lib/graphql/generated";
|
||||
|
||||
export default function useOpenProjectMutation() {
|
||||
return _useOpenProjectMutation({
|
||||
errorPolicy: "all",
|
||||
update(cache, { data }) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
activeProject() {
|
||||
const activeProjRef = cache.writeFragment({
|
||||
data: data?.openProject,
|
||||
fragment: gql`
|
||||
fragment ActiveProject on Project {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
type
|
||||
}
|
||||
`,
|
||||
});
|
||||
return activeProjRef;
|
||||
},
|
||||
projects(_, { DELETE }) {
|
||||
cache.writeFragment({
|
||||
id: data?.openProject?.id,
|
||||
data: data?.openProject,
|
||||
fragment: gql`
|
||||
fragment OpenProject on Project {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
type
|
||||
}
|
||||
`,
|
||||
});
|
||||
return DELETE;
|
||||
},
|
||||
httpRequestLogFilter(_, { DELETE }) {
|
||||
return DELETE;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
57
admin/src/features/reqlog/components/LogDetail.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import Alert from "@mui/lab/Alert";
|
||||
import { Box, CircularProgress, Paper, Typography } from "@mui/material";
|
||||
|
||||
import RequestDetail from "./RequestDetail";
|
||||
|
||||
import Response from "lib/components/Response";
|
||||
import SplitPane from "lib/components/SplitPane";
|
||||
import { useHttpRequestLogQuery } from "lib/graphql/generated";
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function LogDetail({ id }: Props): JSX.Element {
|
||||
const { loading, error, data } = useHttpRequestLogQuery({
|
||||
variables: { id: id as string },
|
||||
skip: id === undefined,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
if (error) {
|
||||
return <Alert severity="error">Error fetching logs details: {error.message}</Alert>;
|
||||
}
|
||||
|
||||
if (data && !data.httpRequestLog) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
Request <strong>{id}</strong> was not found.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data?.httpRequestLog) {
|
||||
return (
|
||||
<Paper variant="centered" sx={{ mt: 2 }}>
|
||||
<Typography>Select a log entry…</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
const reqLog = data.httpRequestLog;
|
||||
|
||||
return (
|
||||
<SplitPane split="vertical" size={"50%"}>
|
||||
<RequestDetail request={reqLog} />
|
||||
{reqLog.response && (
|
||||
<Box sx={{ height: "100%", pt: 1, pl: 2, pb: 2 }}>
|
||||
<Response response={reqLog.response} />
|
||||
</Box>
|
||||
)}
|
||||
</SplitPane>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogDetail;
|
47
admin/src/features/reqlog/components/RequestDetail.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Typography, Box } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
import RequestTabs from "lib/components/RequestTabs";
|
||||
import { HttpRequestLogQuery } from "lib/graphql/generated";
|
||||
import { queryParamsFromURL } from "lib/queryParamsFromURL";
|
||||
|
||||
interface Props {
|
||||
request: NonNullable<HttpRequestLogQuery["httpRequestLog"]>;
|
||||
}
|
||||
|
||||
function RequestDetail({ request }: Props): JSX.Element {
|
||||
const { method, url, headers, body } = request;
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
return (
|
||||
<Box sx={{ height: "100%", display: "flex", flexDirection: "column", pr: 2, pb: 2 }}>
|
||||
<Box sx={{ p: 2, pb: 0 }}>
|
||||
<Typography variant="overline" color="textSecondary" style={{ float: "right" }}>
|
||||
Request
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h2"
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
display: "block",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
pr: 2,
|
||||
}}
|
||||
>
|
||||
{method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box flex="1 auto" overflow="scroll">
|
||||
<RequestTabs headers={headers} queryParams={queryParamsFromURL(url)} body={body} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default RequestDetail;
|
129
admin/src/features/reqlog/components/RequestLogs.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
IconButton,
|
||||
Link,
|
||||
MenuItem,
|
||||
Snackbar,
|
||||
styled,
|
||||
TableCell,
|
||||
TableCellProps,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import LogDetail from "./LogDetail";
|
||||
import Search from "./Search";
|
||||
|
||||
import RequestsTable from "lib/components/RequestsTable";
|
||||
import SplitPane from "lib/components/SplitPane";
|
||||
import useContextMenu from "lib/components/useContextMenu";
|
||||
import { useCreateSenderRequestFromHttpRequestLogMutation, useHttpRequestLogsQuery } from "lib/graphql/generated";
|
||||
|
||||
const ActionsTableCell = styled(TableCell)<TableCellProps>(() => ({
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
}));
|
||||
|
||||
export function RequestLogs(): JSX.Element {
|
||||
const router = useRouter();
|
||||
const id = router.query.id as string | undefined;
|
||||
const { data } = useHttpRequestLogsQuery({
|
||||
pollInterval: 1000,
|
||||
});
|
||||
|
||||
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({
|
||||
onCompleted({ createSenderRequestFromHttpRequestLog }) {
|
||||
const { id } = createSenderRequestFromHttpRequestLog;
|
||||
setNewSenderReqId(id);
|
||||
setCopiedReqNotifOpen(true);
|
||||
},
|
||||
});
|
||||
|
||||
const [copyToSenderId, setCopyToSenderId] = useState("");
|
||||
const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu();
|
||||
|
||||
const handleCopyToSenderClick = () => {
|
||||
createSenderReqFromLog({
|
||||
variables: {
|
||||
id: copyToSenderId,
|
||||
},
|
||||
});
|
||||
handleContextMenuClose();
|
||||
};
|
||||
|
||||
const [newSenderReqId, setNewSenderReqId] = useState("");
|
||||
const [copiedReqNotifOpen, setCopiedReqNotifOpen] = useState(false);
|
||||
const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
setCopiedReqNotifOpen(false);
|
||||
};
|
||||
|
||||
const handleRowClick = (id: string) => {
|
||||
router.push(`/proxy/logs?id=${id}`);
|
||||
};
|
||||
|
||||
const handleRowContextClick = (e: React.MouseEvent, id: string) => {
|
||||
setCopyToSenderId(id);
|
||||
handleContextMenu(e);
|
||||
};
|
||||
|
||||
const actionsCell = (id: string) => (
|
||||
<ActionsTableCell>
|
||||
<Tooltip title="Copy to Sender">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setCopyToSenderId(id);
|
||||
createSenderReqFromLog({
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ActionsTableCell>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" height="100%">
|
||||
<Search />
|
||||
<Box sx={{ display: "flex", flex: "1 auto", position: "relative" }}>
|
||||
<SplitPane split="horizontal" size={"40%"}>
|
||||
<Box sx={{ width: "100%", height: "100%", pb: 2 }}>
|
||||
<Box sx={{ width: "100%", height: "100%", overflow: "scroll" }}>
|
||||
<Menu>
|
||||
<MenuItem onClick={handleCopyToSenderClick}>Copy request to Sender</MenuItem>
|
||||
</Menu>
|
||||
<Snackbar
|
||||
open={copiedReqNotifOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleCloseCopiedNotif}
|
||||
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
|
||||
>
|
||||
<Alert onClose={handleCloseCopiedNotif} severity="info">
|
||||
Request was copied. <Link href={`/sender?id=${newSenderReqId}`}>Edit in Sender.</Link>
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<RequestsTable
|
||||
requests={data?.httpRequestLogs || []}
|
||||
activeRowId={id}
|
||||
actionsCell={actionsCell}
|
||||
onRowClick={handleRowClick}
|
||||
onContextMenu={handleRowContextClick}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<LogDetail id={id} />
|
||||
</SplitPane>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
195
admin/src/features/reqlog/components/Search.tsx
Normal file
@ -0,0 +1,195 @@
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { Alert } from "@mui/lab";
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
ClickAwayListener,
|
||||
FormControlLabel,
|
||||
InputBase,
|
||||
Paper,
|
||||
Popper,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog";
|
||||
import {
|
||||
HttpRequestLogFilterDocument,
|
||||
HttpRequestLogsDocument,
|
||||
useClearHttpRequestLogMutation,
|
||||
useHttpRequestLogFilterQuery,
|
||||
useSetHttpRequestLogFilterMutation,
|
||||
} from "lib/graphql/generated";
|
||||
import { withoutTypename } from "lib/graphql/omitTypename";
|
||||
|
||||
function Search(): JSX.Element {
|
||||
const theme = useTheme();
|
||||
|
||||
const [searchExpr, setSearchExpr] = useState("");
|
||||
const filterResult = useHttpRequestLogFilterQuery({
|
||||
onCompleted: (data) => {
|
||||
setSearchExpr(data.httpRequestLogFilter?.searchExpression || "");
|
||||
},
|
||||
});
|
||||
const filter = filterResult.data?.httpRequestLogFilter;
|
||||
|
||||
const [setFilterMutate, setFilterResult] = useSetHttpRequestLogFilterMutation({
|
||||
update(cache, { data }) {
|
||||
cache.writeQuery({
|
||||
query: HttpRequestLogFilterDocument,
|
||||
data: {
|
||||
httpRequestLogFilter: data?.setHttpRequestLogFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [clearHTTPRequestLog, clearHTTPRequestLogResult] = useClearHttpRequestLogMutation({
|
||||
refetchQueries: [{ query: HttpRequestLogsDocument }],
|
||||
});
|
||||
const clearHTTPConfirmationDialog = useConfirmationDialog();
|
||||
|
||||
const filterRef = useRef<HTMLFormElement>(null);
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||
setFilterMutate({
|
||||
variables: {
|
||||
filter: {
|
||||
...withoutTypename(filter),
|
||||
searchExpression: searchExpr,
|
||||
},
|
||||
},
|
||||
});
|
||||
setFilterOpen(false);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleClickAway = (event: MouseEvent | TouchEvent) => {
|
||||
if (filterRef?.current && filterRef.current.contains(event.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
setFilterOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Error prefix="Error fetching filter" error={filterResult.error} />
|
||||
<Error prefix="Error setting filter" error={setFilterResult.error} />
|
||||
<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}
|
||||
sx={{
|
||||
padding: "2px 4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Toggle filter options">
|
||||
<IconButton
|
||||
onClick={() => setFilterOpen(!filterOpen)}
|
||||
sx={{
|
||||
p: 1,
|
||||
color: filter?.onlyInScope ? "primary.main" : "inherit",
|
||||
}}
|
||||
>
|
||||
{filterResult.loading || setFilterResult.loading ? (
|
||||
<CircularProgress sx={{ color: theme.palette.text.primary }} size={23} />
|
||||
) : (
|
||||
<FilterListIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<InputBase
|
||||
sx={{
|
||||
ml: 1,
|
||||
flex: 1,
|
||||
}}
|
||||
placeholder="Search proxy logs…"
|
||||
value={searchExpr}
|
||||
onChange={(e) => setSearchExpr(e.target.value)}
|
||||
onFocus={() => setFilterOpen(true)}
|
||||
/>
|
||||
<Tooltip title="Search">
|
||||
<IconButton type="submit" sx={{ padding: 1.25 }}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
open={filterOpen}
|
||||
anchorEl={filterRef.current}
|
||||
placement="bottom"
|
||||
style={{ zIndex: theme.zIndex.appBar }}
|
||||
>
|
||||
<Paper
|
||||
sx={{
|
||||
width: 400,
|
||||
marginTop: 0.5,
|
||||
p: 1.5,
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={filter?.onlyInScope ? true : false}
|
||||
disabled={filterResult.loading || setFilterResult.loading}
|
||||
onChange={(e) =>
|
||||
setFilterMutate({
|
||||
variables: {
|
||||
filter: {
|
||||
...withoutTypename(filter),
|
||||
onlyInScope: e.target.checked,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Only show in-scope requests"
|
||||
/>
|
||||
</Paper>
|
||||
</Popper>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
<Box style={{ marginLeft: "auto" }}>
|
||||
<Tooltip title="Clear all">
|
||||
<IconButton onClick={clearHTTPConfirmationDialog.open}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
@ -0,0 +1,5 @@
|
||||
mutation ClearHTTPRequestLog {
|
||||
clearHTTPRequestLog {
|
||||
success
|
||||
}
|
||||
}
|
24
admin/src/features/reqlog/graphql/httpRequestLog.graphql
Normal file
@ -0,0 +1,24 @@
|
||||
query HttpRequestLog($id: ID!) {
|
||||
httpRequestLog(id: $id) {
|
||||
id
|
||||
method
|
||||
url
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
response {
|
||||
id
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
statusCode
|
||||
statusReason
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
query HttpRequestLogFilter {
|
||||
httpRequestLogFilter {
|
||||
onlyInScope
|
||||
searchExpression
|
||||
}
|
||||
}
|
12
admin/src/features/reqlog/graphql/httpRequestLogs.graphql
Normal file
@ -0,0 +1,12 @@
|
||||
query HttpRequestLogs {
|
||||
httpRequestLogs {
|
||||
id
|
||||
method
|
||||
url
|
||||
timestamp
|
||||
response {
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) {
|
||||
setHttpRequestLogFilter(filter: $filter) {
|
||||
onlyInScope
|
||||
searchExpression
|
||||
}
|
||||
}
|
3
admin/src/features/reqlog/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { RequestLogs } from "./components/RequestLogs";
|
||||
|
||||
export default RequestLogs;
|
108
admin/src/features/scope/components/AddRule.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { Alert } from "@mui/lab";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { ScopeDocument, ScopeQuery, ScopeRule, useSetScopeMutation } from "lib/graphql/generated";
|
||||
|
||||
function AddRule(): JSX.Element {
|
||||
const [ruleType, setRuleType] = useState("url");
|
||||
const [expression, setExpression] = useState("");
|
||||
|
||||
const client = useApolloClient();
|
||||
const [setScope, { error, loading }] = useSetScopeMutation({
|
||||
onCompleted({ setScope }) {
|
||||
client.writeQuery({
|
||||
query: ScopeDocument,
|
||||
data: { scope: setScope },
|
||||
});
|
||||
setExpression("");
|
||||
},
|
||||
});
|
||||
|
||||
const handleTypeChange = (e: React.ChangeEvent, value: string) => {
|
||||
setRuleType(value);
|
||||
};
|
||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
let scope: ScopeRule[] = [];
|
||||
|
||||
try {
|
||||
const data = client.readQuery<ScopeQuery>({
|
||||
query: ScopeDocument,
|
||||
});
|
||||
if (data) {
|
||||
scope = data.scope;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
setScope({
|
||||
variables: {
|
||||
scope: [...scope.map(({ url }) => ({ url })), { url: expression }],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && (
|
||||
<Box mb={4}>
|
||||
<Alert severity="error">Error adding rule: {error.message}</Alert>
|
||||
</Box>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} autoComplete="off">
|
||||
<FormControl fullWidth>
|
||||
<FormLabel color="primary" component="legend">
|
||||
Rule Type
|
||||
</FormLabel>
|
||||
<RadioGroup row name="ruleType" value={ruleType} onChange={handleTypeChange}>
|
||||
<FormControlLabel value="url" control={<Radio />} label="URL" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
label="Expression"
|
||||
placeholder="^https:\/\/(.*)example.com(.*)"
|
||||
helperText="Regular expression to match on."
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
required
|
||||
value={expression}
|
||||
onChange={(e) => setExpression(e.target.value)}
|
||||
InputProps={{
|
||||
sx: { fontFamily: "'JetBrains Mono', monospace" },
|
||||
}}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
margin="normal"
|
||||
/>
|
||||
</FormControl>
|
||||
<Box my={2}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={22} /> : <AddIcon />}
|
||||
>
|
||||
Add rule
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddRule;
|
91
admin/src/features/scope/components/RuleListItem.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import CodeIcon from "@mui/icons-material/Code";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import {
|
||||
Avatar,
|
||||
Chip,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
import { ScopeDocument, ScopeQuery, useSetScopeMutation } from "lib/graphql/generated";
|
||||
|
||||
type ScopeRule = ScopeQuery["scope"][number];
|
||||
|
||||
type RuleListItemProps = {
|
||||
scope: ScopeQuery["scope"];
|
||||
rule: ScopeRule;
|
||||
index: number;
|
||||
};
|
||||
|
||||
function RuleListItem({ scope, rule, index }: RuleListItemProps): JSX.Element {
|
||||
const client = useApolloClient();
|
||||
const [setScope, { loading }] = useSetScopeMutation({
|
||||
onCompleted({ setScope }) {
|
||||
client.writeQuery({
|
||||
query: ScopeDocument,
|
||||
data: { scope: setScope },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
const clone = [...scope];
|
||||
clone.splice(index, 1);
|
||||
setScope({
|
||||
variables: {
|
||||
scope: clone.map(({ url }) => ({ url })),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<CodeIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<RuleListItemText rule={rule} />
|
||||
<ListItemSecondaryAction>
|
||||
<RuleTypeChip rule={rule} />
|
||||
<Tooltip title="Delete rule">
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
<IconButton onClick={() => handleDelete(index)} disabled={loading}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleListItemText({ rule }: { rule: ScopeRule }): JSX.Element {
|
||||
let text: JSX.Element = <div></div>;
|
||||
|
||||
if (rule.url) {
|
||||
text = <code>{rule.url}</code>;
|
||||
}
|
||||
|
||||
// TODO: Parse and handle rule.header and rule.body.
|
||||
|
||||
return <ListItemText>{text}</ListItemText>;
|
||||
}
|
||||
|
||||
function RuleTypeChip({ rule }: { rule: ScopeRule }): JSX.Element {
|
||||
let label = "Unknown";
|
||||
|
||||
if (rule.url) {
|
||||
label = "URL";
|
||||
}
|
||||
|
||||
return <Chip label={label} variant="outlined" />;
|
||||
}
|
||||
|
||||
export default RuleListItem;
|
31
admin/src/features/scope/components/Rules.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Alert } from "@mui/lab";
|
||||
import { CircularProgress, List } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
import RuleListItem from "./RuleListItem";
|
||||
|
||||
import { useScopeQuery } from "lib/graphql/generated";
|
||||
|
||||
function Rules(): JSX.Element {
|
||||
const { loading, error, data } = useScopeQuery();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && <CircularProgress />}
|
||||
{error && <Alert severity="error">Error fetching scope: {error.message}</Alert>}
|
||||
{data && data.scope.length > 0 && (
|
||||
<List
|
||||
sx={{
|
||||
bgcolor: "background.paper",
|
||||
}}
|
||||
>
|
||||
{data.scope.map((rule, index) => (
|
||||
<RuleListItem key={index} rule={rule} scope={data.scope} index={index} />
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Rules;
|
5
admin/src/features/scope/graphql/scope.graphql
Normal file
@ -0,0 +1,5 @@
|
||||
query Scope {
|
||||
scope {
|
||||
url
|
||||
}
|
||||
}
|
5
admin/src/features/scope/graphql/setScope.graphql
Normal file
@ -0,0 +1,5 @@
|
||||
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
||||
setScope(scope: $scope) {
|
||||
url
|
||||
}
|
||||
}
|
356
admin/src/features/sender/components/EditRequest.tsx
Normal file
@ -0,0 +1,356 @@
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
BoxProps,
|
||||
Button,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair";
|
||||
import RequestTabs from "lib/components/RequestTabs";
|
||||
import Response from "lib/components/Response";
|
||||
import SplitPane from "lib/components/SplitPane";
|
||||
import {
|
||||
GetSenderRequestQuery,
|
||||
useCreateOrUpdateSenderRequestMutation,
|
||||
HttpProtocol,
|
||||
useGetSenderRequestQuery,
|
||||
useSendRequestMutation,
|
||||
} from "lib/graphql/generated";
|
||||
import { queryParamsFromURL } from "lib/queryParamsFromURL";
|
||||
|
||||
enum HttpMethod {
|
||||
Get = "GET",
|
||||
Post = "POST",
|
||||
Put = "PUT",
|
||||
Patch = "PATCH",
|
||||
Delete = "DELETE",
|
||||
Head = "HEAD",
|
||||
Options = "OPTIONS",
|
||||
Connect = "CONNECT",
|
||||
Trace = "TRACE",
|
||||
}
|
||||
|
||||
enum HttpProto {
|
||||
Http10 = "HTTP/1.0",
|
||||
Http11 = "HTTP/1.1",
|
||||
Http20 = "HTTP/2.0",
|
||||
}
|
||||
|
||||
const httpProtoMap = new Map([
|
||||
[HttpProto.Http10, HttpProtocol.Http10],
|
||||
[HttpProto.Http11, HttpProtocol.Http11],
|
||||
[HttpProto.Http20, HttpProtocol.Http20],
|
||||
]);
|
||||
|
||||
function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] {
|
||||
const updated = [...items];
|
||||
updated[idx] = { key, value };
|
||||
|
||||
// Append an empty key-value pair if the last item in the array isn't blank
|
||||
// anymore.
|
||||
if (items.length - 1 === idx && items[idx].key === "" && items[idx].value === "") {
|
||||
updated.push({ key: "", value: "" });
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) {
|
||||
// Note: We don't use the `URL` interface, because we're potentially dealing
|
||||
// with malformed/incorrect URLs, which would yield TypeErrors when constructed
|
||||
// via `URL`.
|
||||
let newURL = url;
|
||||
|
||||
const questionMarkIndex = url.indexOf("?");
|
||||
if (questionMarkIndex !== -1) {
|
||||
newURL = newURL.slice(0, questionMarkIndex);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const { key, value } of queryParams.filter(({ key }) => key !== "")) {
|
||||
searchParams.append(key, value);
|
||||
}
|
||||
|
||||
const rawQueryParams = decodeURI(searchParams.toString());
|
||||
|
||||
if (rawQueryParams == "") {
|
||||
return newURL;
|
||||
}
|
||||
|
||||
return newURL + "?" + rawQueryParams;
|
||||
}
|
||||
|
||||
function EditRequest(): JSX.Element {
|
||||
const router = useRouter();
|
||||
const reqId = router.query.id as string | undefined;
|
||||
|
||||
const [method, setMethod] = useState(HttpMethod.Get);
|
||||
const [url, setURL] = useState("");
|
||||
const [proto, setProto] = useState(HttpProto.Http20);
|
||||
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||
const [body, setBody] = useState("");
|
||||
|
||||
const handleQueryParamChange = (key: string, value: string, idx: number) => {
|
||||
setQueryParams((prev) => {
|
||||
const updated = updateKeyPairItem(key, value, idx, prev);
|
||||
setURL((prev) => updateURLQueryParams(prev, updated));
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
const handleQueryParamDelete = (idx: number) => {
|
||||
setQueryParams((prev) => {
|
||||
const updated = prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length));
|
||||
setURL((prev) => updateURLQueryParams(prev, updated));
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleHeaderChange = (key: string, value: string, idx: number) => {
|
||||
setHeaders((prev) => updateKeyPairItem(key, value, idx, prev));
|
||||
};
|
||||
const handleHeaderDelete = (idx: number) => {
|
||||
setHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)));
|
||||
};
|
||||
|
||||
const handleURLChange = (url: string) => {
|
||||
setURL(url);
|
||||
|
||||
const questionMarkIndex = url.indexOf("?");
|
||||
if (questionMarkIndex === -1) {
|
||||
setQueryParams([{ key: "", value: "" }]);
|
||||
return;
|
||||
}
|
||||
|
||||
const newQueryParams = queryParamsFromURL(url);
|
||||
// Push empty row.
|
||||
newQueryParams.push({ key: "", value: "" });
|
||||
setQueryParams(newQueryParams);
|
||||
};
|
||||
|
||||
const [response, setResponse] = useState<NonNullable<GetSenderRequestQuery["senderRequest"]>["response"]>(null);
|
||||
const getReqResult = useGetSenderRequestQuery({
|
||||
variables: { id: reqId as string },
|
||||
skip: reqId === undefined,
|
||||
onCompleted: ({ senderRequest }) => {
|
||||
if (!senderRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setURL(senderRequest.url);
|
||||
setMethod(senderRequest.method);
|
||||
setBody(senderRequest.body || "");
|
||||
|
||||
const newQueryParams = queryParamsFromURL(senderRequest.url);
|
||||
// Push empty row.
|
||||
newQueryParams.push({ key: "", value: "" });
|
||||
setQueryParams(newQueryParams);
|
||||
|
||||
const newHeaders = sortKeyValuePairs(senderRequest.headers || []);
|
||||
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||
setResponse(senderRequest.response);
|
||||
},
|
||||
});
|
||||
|
||||
const [createOrUpdateRequest, createResult] = useCreateOrUpdateSenderRequestMutation();
|
||||
const [sendRequest, sendResult] = useSendRequestMutation();
|
||||
|
||||
const createOrUpdateRequestAndSend = () => {
|
||||
const senderReq = getReqResult?.data?.senderRequest;
|
||||
createOrUpdateRequest({
|
||||
variables: {
|
||||
request: {
|
||||
// Update existing sender request if it was cloned from a request log
|
||||
// and it doesn't have a response body yet (e.g. not sent yet).
|
||||
...(senderReq && senderReq.sourceRequestLogID && !senderReq.response && { id: senderReq.id }),
|
||||
url,
|
||||
method,
|
||||
proto: httpProtoMap.get(proto),
|
||||
headers: headers.filter((kv) => kv.key !== ""),
|
||||
body: body || undefined,
|
||||
},
|
||||
},
|
||||
onCompleted: ({ createOrUpdateSenderRequest }) => {
|
||||
const { id } = createOrUpdateSenderRequest;
|
||||
sendRequestAndPushRoute(id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const sendRequestAndPushRoute = (id: string) => {
|
||||
sendRequest({
|
||||
errorPolicy: "all",
|
||||
onCompleted: () => {
|
||||
router.push(`/sender?id=${id}`);
|
||||
},
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit: React.FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
createOrUpdateRequestAndSend();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" height="100%" gap={2}>
|
||||
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<UrlBar
|
||||
method={method}
|
||||
onMethodChange={setMethod}
|
||||
url={url.toString()}
|
||||
onUrlChange={handleURLChange}
|
||||
proto={proto}
|
||||
onProtoChange={setProto}
|
||||
sx={{ flex: "1 auto" }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
sx={{ width: "8rem" }}
|
||||
type="submit"
|
||||
disabled={createResult.loading || sendResult.loading}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</Box>
|
||||
{createResult.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{createResult.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
{sendResult.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{sendResult.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flex="1 auto" position="relative">
|
||||
<SplitPane split="vertical" size={"50%"}>
|
||||
<Box sx={{ height: "100%", mr: 2, pb: 2, position: "relative" }}>
|
||||
<Typography variant="overline" color="textSecondary" sx={{ position: "absolute", right: 0, mt: 1.2 }}>
|
||||
Request
|
||||
</Typography>
|
||||
<RequestTabs
|
||||
queryParams={queryParams}
|
||||
headers={headers}
|
||||
body={body}
|
||||
onQueryParamChange={handleQueryParamChange}
|
||||
onQueryParamDelete={handleQueryParamDelete}
|
||||
onHeaderChange={handleHeaderChange}
|
||||
onHeaderDelete={handleHeaderDelete}
|
||||
onBodyChange={setBody}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ height: "100%", position: "relative", ml: 2, pb: 2 }}>
|
||||
<Response response={response} />
|
||||
</Box>
|
||||
</SplitPane>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface UrlBarProps extends BoxProps {
|
||||
method: HttpMethod;
|
||||
onMethodChange: (method: HttpMethod) => void;
|
||||
url: string;
|
||||
onUrlChange: (url: string) => void;
|
||||
proto: HttpProto;
|
||||
onProtoChange: (proto: HttpProto) => void;
|
||||
}
|
||||
|
||||
function UrlBar(props: UrlBarProps) {
|
||||
const { method, onMethodChange, url, onUrlChange, proto, onProtoChange, ...other } = props;
|
||||
|
||||
return (
|
||||
<Box {...other} sx={{ ...other.sx, display: "flex" }}>
|
||||
<FormControl>
|
||||
<InputLabel id="req-method-label">Method</InputLabel>
|
||||
<Select
|
||||
labelId="req-method-label"
|
||||
id="req-method"
|
||||
value={method}
|
||||
label="Method"
|
||||
onChange={(e) => onMethodChange(e.target.value as HttpMethod)}
|
||||
sx={{
|
||||
width: "8rem",
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderRightWidth: 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(HttpMethod).map((method) => (
|
||||
<MenuItem key={method} value={method}>
|
||||
{method}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="URL"
|
||||
placeholder="E.g. “https://example.com/foobar”"
|
||||
value={url}
|
||||
onChange={(e) => onUrlChange(e.target.value)}
|
||||
required
|
||||
variant="outlined"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: {
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<FormControl>
|
||||
<InputLabel id="req-proto-label">Protocol</InputLabel>
|
||||
<Select
|
||||
labelId="req-proto-label"
|
||||
id="req-proto"
|
||||
value={proto}
|
||||
label="Protocol"
|
||||
onChange={(e) => onProtoChange(e.target.value as HttpProto)}
|
||||
sx={{
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderLeftWidth: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderLeftWidth: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(HttpProto).map((proto) => (
|
||||
<MenuItem key={proto} value={proto}>
|
||||
{proto}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRequest;
|
35
admin/src/features/sender/components/History.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Box, Paper, Typography } from "@mui/material";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import RequestsTable from "lib/components/RequestsTable";
|
||||
import { useGetSenderRequestsQuery } from "lib/graphql/generated";
|
||||
|
||||
function History(): JSX.Element {
|
||||
const { data, loading } = useGetSenderRequestsQuery({
|
||||
pollInterval: 1000,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const activeId = router.query.id as string | undefined;
|
||||
|
||||
const handleRowClick = (id: string) => {
|
||||
router.push(`/sender?id=${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{!loading && data?.senderRequests && data?.senderRequests.length > 0 && (
|
||||
<RequestsTable requests={data.senderRequests} onRowClick={handleRowClick} activeRowId={activeId} />
|
||||
)}
|
||||
<Box sx={{ mt: 2, height: "100%" }}>
|
||||
{!loading && data?.senderRequests.length === 0 && (
|
||||
<Paper variant="centered">
|
||||
<Typography>No requests created yet.</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default History;
|
21
admin/src/features/sender/components/Sender.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import EditRequest from "./EditRequest";
|
||||
import History from "./History";
|
||||
|
||||
import SplitPane from "lib/components/SplitPane";
|
||||
|
||||
export default function Sender(): JSX.Element {
|
||||
return (
|
||||
<Box sx={{ height: "100%", position: "relative" }}>
|
||||
<SplitPane split="horizontal" size="70%">
|
||||
<Box sx={{ width: "100%", pt: "0.75rem" }}>
|
||||
<EditRequest />
|
||||
</Box>
|
||||
<Box sx={{ height: "100%", overflow: "scroll" }}>
|
||||
<History />
|
||||
</Box>
|
||||
</SplitPane>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mutation CreateOrUpdateSenderRequest($request: SenderRequestInput!) {
|
||||
createOrUpdateSenderRequest(request: $request) {
|
||||
id
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mutation CreateSenderRequestFromHttpRequestLog($id: ID!) {
|
||||
createSenderRequestFromHttpRequestLog(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
5
admin/src/features/sender/graphql/sendRequest.graphql
Normal file
@ -0,0 +1,5 @@
|
||||
mutation SendRequest($id: ID!) {
|
||||
sendRequest(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
26
admin/src/features/sender/graphql/senderRequest.graphql
Normal file
@ -0,0 +1,26 @@
|
||||
query GetSenderRequest($id: ID!) {
|
||||
senderRequest(id: $id) {
|
||||
id
|
||||
sourceRequestLogID
|
||||
url
|
||||
method
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
timestamp
|
||||
response {
|
||||
id
|
||||
proto
|
||||
statusCode
|
||||
statusReason
|
||||
body
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
admin/src/features/sender/graphql/senderRequests.graphql
Normal file
@ -0,0 +1,12 @@
|
||||
query GetSenderRequests {
|
||||
senderRequests {
|
||||
id
|
||||
url
|
||||
method
|
||||
response {
|
||||
id
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
3
admin/src/features/sender/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Sender from "./components/Sender";
|
||||
|
||||
export default Sender;
|
20
admin/src/lib/ActiveProjectContext.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
import { Project, useProjectsQuery } from "./graphql/generated";
|
||||
|
||||
const ActiveProjectContext = createContext<Project | null>(null);
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode | undefined;
|
||||
}
|
||||
|
||||
export function ActiveProjectProvider({ children }: Props): JSX.Element {
|
||||
const { data } = useProjectsQuery();
|
||||
const project = data?.projects.find((project) => project.isActive) || null;
|
||||
|
||||
return <ActiveProjectContext.Provider value={project}>{children}</ActiveProjectContext.Provider>;
|
||||
}
|
||||
|
||||
export function useActiveProject() {
|
||||
return useContext(ActiveProjectContext);
|
||||
}
|
51
admin/src/lib/components/ConfirmationDialog.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import React, { useState } from "react";
|
||||
|
||||
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}>Cancel</Button>
|
||||
<Button onClick={confirm} autoFocus>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
48
admin/src/lib/components/Editor.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import MonacoEditor, { EditorProps } from "@monaco-editor/react";
|
||||
|
||||
const defaultMonacoOptions: EditorProps["options"] = {
|
||||
readOnly: true,
|
||||
wordWrap: "on",
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
type language = "html" | "typescript" | "json";
|
||||
|
||||
function languageForContentType(contentType?: string): language | undefined {
|
||||
switch (contentType?.toLowerCase()) {
|
||||
case "text/html":
|
||||
case "text/html; charset=utf-8":
|
||||
return "html";
|
||||
case "application/json":
|
||||
case "application/json; charset=utf-8":
|
||||
return "json";
|
||||
case "application/javascript":
|
||||
case "application/javascript; charset=utf-8":
|
||||
return "typescript";
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
contentType?: string;
|
||||
monacoOptions?: EditorProps["options"];
|
||||
onChange?: EditorProps["onChange"];
|
||||
}
|
||||
|
||||
function Editor({ content, contentType, monacoOptions, onChange }: Props): JSX.Element {
|
||||
return (
|
||||
<MonacoEditor
|
||||
language={languageForContentType(contentType)}
|
||||
theme="vs-dark"
|
||||
options={{ ...defaultMonacoOptions, ...monacoOptions }}
|
||||
value={content}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Editor;
|
25
admin/src/lib/components/HttpStatusIcon.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
||||
import { SvgIconTypeMap } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
status: number;
|
||||
}
|
||||
|
||||
export default function HttpStatusIcon({ status }: Props): JSX.Element {
|
||||
let color: SvgIconTypeMap["props"]["color"] = "inherit";
|
||||
|
||||
switch (Math.floor(status / 100)) {
|
||||
case 2:
|
||||
case 3:
|
||||
color = "primary";
|
||||
break;
|
||||
case 4:
|
||||
color = "warning";
|
||||
break;
|
||||
case 5:
|
||||
color = "error";
|
||||
break;
|
||||
}
|
||||
|
||||
return <FiberManualRecordIcon sx={{ marginTop: "-.25rem", verticalAlign: "middle" }} color={color} />;
|
||||
}
|
203
admin/src/lib/components/KeyValuePair.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import {
|
||||
Alert,
|
||||
IconButton,
|
||||
InputBase,
|
||||
InputBaseProps,
|
||||
Snackbar,
|
||||
styled,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableRowProps,
|
||||
} from "@mui/material";
|
||||
import { useState } from "react";
|
||||
|
||||
const StyledInputBase = styled(InputBase)<InputBaseProps>(() => ({
|
||||
fontSize: "0.875rem",
|
||||
"&.MuiInputBase-root input": {
|
||||
p: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledTableRow = styled(TableRow)<TableRowProps>(() => ({
|
||||
"& .delete-button": {
|
||||
visibility: "hidden",
|
||||
},
|
||||
"&:hover .delete-button": {
|
||||
visibility: "inherit",
|
||||
},
|
||||
}));
|
||||
|
||||
export interface KeyValuePair {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface KeyValuePairTableProps {
|
||||
items: KeyValuePair[];
|
||||
onChange?: (key: string, value: string, index: number) => void;
|
||||
onDelete?: (index: number) => void;
|
||||
}
|
||||
|
||||
export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTableProps): JSX.Element {
|
||||
const [copyConfOpen, setCopyConfOpen] = useState(false);
|
||||
|
||||
const handleCellClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const windowSel = window.getSelection();
|
||||
|
||||
if (!windowSel || !document) {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = document.createRange();
|
||||
r.selectNode(e.currentTarget);
|
||||
windowSel.removeAllRanges();
|
||||
windowSel.addRange(r);
|
||||
document.execCommand("copy");
|
||||
windowSel.removeAllRanges();
|
||||
|
||||
setCopyConfOpen(true);
|
||||
};
|
||||
|
||||
const handleCopyConfClose = (_: Event | React.SyntheticEvent, reason?: string) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
|
||||
setCopyConfOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Snackbar open={copyConfOpen} autoHideDuration={3000} onClose={handleCopyConfClose}>
|
||||
<Alert onClose={handleCopyConfClose} severity="info">
|
||||
Copied to clipboard.
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<TableContainer sx={{ overflowX: "initial" }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Key</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
{onDelete && <TableCell padding="checkbox"></TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody
|
||||
sx={{
|
||||
"td, th, input": {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: "0.75rem",
|
||||
py: 0.2,
|
||||
},
|
||||
"td span, th span": {
|
||||
display: "block",
|
||||
py: 0.7,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{items.map(({ key, value }, idx) => (
|
||||
<StyledTableRow key={idx} hover>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
onClick={(e) => {
|
||||
!onChange && handleCellClick(e);
|
||||
}}
|
||||
sx={{
|
||||
...(!onChange && {
|
||||
"&:hover": {
|
||||
cursor: "copy",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{!onChange && <span>{key}</span>}
|
||||
{onChange && (
|
||||
<StyledInputBase
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
onChange && onChange(e.target.value, value, idx);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
onClick={(e) => {
|
||||
!onChange && handleCellClick(e);
|
||||
}}
|
||||
sx={{
|
||||
width: "60%",
|
||||
wordBreak: "break-all",
|
||||
...(!onChange && {
|
||||
"&:hover": {
|
||||
cursor: "copy",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{!onChange && value}
|
||||
{onChange && (
|
||||
<StyledInputBase
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange && onChange(key, e.target.value, idx);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
{onDelete && (
|
||||
<TableCell>
|
||||
<div className="delete-button">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onDelete && onDelete(idx);
|
||||
}}
|
||||
sx={{
|
||||
visibility: onDelete === undefined || items.length === idx + 1 ? "hidden" : "inherit",
|
||||
}}
|
||||
>
|
||||
<ClearIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] {
|
||||
const sorted = [...items];
|
||||
|
||||
sorted.sort((a, b) => {
|
||||
if (a.key < b.key) {
|
||||
return -1;
|
||||
}
|
||||
if (a.key > b.key) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export default KeyValuePairTable;
|
91
admin/src/lib/components/RequestTabs.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||
import { Box, Tab } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { KeyValuePairTable, KeyValuePair, KeyValuePairTableProps } from "./KeyValuePair";
|
||||
|
||||
import Editor from "lib/components/Editor";
|
||||
|
||||
enum TabValue {
|
||||
QueryParams = "queryParams",
|
||||
Headers = "headers",
|
||||
Body = "body",
|
||||
}
|
||||
|
||||
interface RequestTabsProps {
|
||||
queryParams: KeyValuePair[];
|
||||
headers: KeyValuePair[];
|
||||
onQueryParamChange?: KeyValuePairTableProps["onChange"];
|
||||
onQueryParamDelete?: KeyValuePairTableProps["onDelete"];
|
||||
onHeaderChange?: KeyValuePairTableProps["onChange"];
|
||||
onHeaderDelete?: KeyValuePairTableProps["onDelete"];
|
||||
body?: string | null;
|
||||
onBodyChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
function RequestTabs(props: RequestTabsProps): JSX.Element {
|
||||
const {
|
||||
queryParams,
|
||||
onQueryParamChange,
|
||||
onQueryParamDelete,
|
||||
headers,
|
||||
onHeaderChange,
|
||||
onHeaderDelete,
|
||||
body,
|
||||
onBodyChange,
|
||||
} = props;
|
||||
const [tabValue, setTabValue] = useState(TabValue.QueryParams);
|
||||
|
||||
const tabSx = {
|
||||
textTransform: "none",
|
||||
};
|
||||
|
||||
const queryParamsLength = onQueryParamChange ? queryParams.length - 1 : queryParams.length;
|
||||
const headersLength = onHeaderChange ? headers.length - 1 : headers.length;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||
<TabContext value={tabValue}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 1 }}>
|
||||
<TabList onChange={(_, value) => setTabValue(value)}>
|
||||
<Tab
|
||||
value={TabValue.QueryParams}
|
||||
label={"Query Params" + (queryParamsLength ? ` (${queryParamsLength})` : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
<Tab value={TabValue.Headers} label={"Headers" + (headersLength ? ` (${headersLength})` : "")} sx={tabSx} />
|
||||
<Tab
|
||||
value={TabValue.Body}
|
||||
label={"Body" + (body?.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
</TabList>
|
||||
</Box>
|
||||
<Box flex="1 auto" overflow="scroll" height="100%">
|
||||
<TabPanel value={TabValue.QueryParams} sx={{ p: 0, height: "100%" }}>
|
||||
<Box>
|
||||
<KeyValuePairTable items={queryParams} onChange={onQueryParamChange} onDelete={onQueryParamDelete} />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%" }}>
|
||||
<Box>
|
||||
<KeyValuePairTable items={headers} onChange={onHeaderChange} onDelete={onHeaderDelete} />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
|
||||
<Editor
|
||||
content={body || ""}
|
||||
onChange={(value) => {
|
||||
onBodyChange && onBodyChange(value || "");
|
||||
}}
|
||||
monacoOptions={{ readOnly: onBodyChange === undefined }}
|
||||
contentType={headers.find(({ key }) => key.toLowerCase() === "content-type")?.value}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</TabContext>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default RequestTabs;
|
128
admin/src/lib/components/RequestsTable.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
styled,
|
||||
TableCellProps,
|
||||
TableRowProps,
|
||||
} from "@mui/material";
|
||||
|
||||
import HttpStatusIcon from "./HttpStatusIcon";
|
||||
|
||||
import { HttpMethod } from "lib/graphql/generated";
|
||||
|
||||
const baseCellStyle = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
} as const;
|
||||
|
||||
const MethodTableCell = styled(TableCell)<TableCellProps>(() => ({
|
||||
...baseCellStyle,
|
||||
width: "100px",
|
||||
}));
|
||||
|
||||
const OriginTableCell = styled(TableCell)<TableCellProps>(() => ({
|
||||
...baseCellStyle,
|
||||
maxWidth: "100px",
|
||||
}));
|
||||
|
||||
const PathTableCell = styled(TableCell)<TableCellProps>(() => ({
|
||||
...baseCellStyle,
|
||||
maxWidth: "200px",
|
||||
}));
|
||||
|
||||
const StatusTableCell = styled(TableCell)<TableCellProps>(() => ({
|
||||
...baseCellStyle,
|
||||
width: "100px",
|
||||
}));
|
||||
|
||||
const RequestTableRow = styled(TableRow)<TableRowProps>(() => ({
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
}));
|
||||
|
||||
interface HttpRequest {
|
||||
id: string;
|
||||
url: string;
|
||||
method: HttpMethod;
|
||||
response?: HttpResponse | null;
|
||||
}
|
||||
|
||||
interface HttpResponse {
|
||||
statusCode: number;
|
||||
statusReason: string;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
requests: HttpRequest[];
|
||||
activeRowId?: string;
|
||||
actionsCell?: (id: string) => JSX.Element;
|
||||
onRowClick?: (id: string) => void;
|
||||
onContextMenu?: (e: React.MouseEvent, id: string) => void;
|
||||
}
|
||||
|
||||
export default function RequestsTable(props: Props): JSX.Element {
|
||||
const { requests, activeRowId, actionsCell, onRowClick, onContextMenu } = props;
|
||||
|
||||
return (
|
||||
<TableContainer sx={{ overflowX: "initial" }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Method</TableCell>
|
||||
<TableCell>Origin</TableCell>
|
||||
<TableCell>Path</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
{actionsCell && <TableCell padding="checkbox"></TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{requests.map(({ id, method, url, response }) => {
|
||||
const { origin, pathname, search, hash } = new URL(url);
|
||||
|
||||
return (
|
||||
<RequestTableRow
|
||||
key={id}
|
||||
hover
|
||||
selected={id === activeRowId}
|
||||
onClick={() => {
|
||||
onRowClick && onRowClick(id);
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
onContextMenu && onContextMenu(e, id);
|
||||
}}
|
||||
>
|
||||
<MethodTableCell>
|
||||
<code>{method}</code>
|
||||
</MethodTableCell>
|
||||
<OriginTableCell>{origin}</OriginTableCell>
|
||||
<PathTableCell>{decodeURIComponent(pathname + search + hash)}</PathTableCell>
|
||||
<StatusTableCell>
|
||||
{response && <Status code={response.statusCode} reason={response.statusReason} />}
|
||||
</StatusTableCell>
|
||||
{actionsCell && actionsCell(id)}
|
||||
</RequestTableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function Status({ code, reason }: { code: number; reason: string }): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<HttpStatusIcon status={code} />{" "}
|
||||
<code>
|
||||
{code} {reason}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
39
admin/src/lib/components/Response.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
import { sortKeyValuePairs } from "./KeyValuePair";
|
||||
import ResponseTabs from "./ResponseTabs";
|
||||
|
||||
import ResponseStatus from "lib/components/ResponseStatus";
|
||||
import { HttpResponseLog } from "lib/graphql/generated";
|
||||
|
||||
interface ResponseProps {
|
||||
response?: HttpResponseLog | null;
|
||||
}
|
||||
|
||||
function Response({ response }: ResponseProps): JSX.Element {
|
||||
return (
|
||||
<Box height="100%">
|
||||
<Box sx={{ position: "absolute", right: 0, mt: 1.4 }}>
|
||||
<Typography variant="overline" color="textSecondary" sx={{ float: "right", ml: 3 }}>
|
||||
Response
|
||||
</Typography>
|
||||
{response && (
|
||||
<Box sx={{ float: "right", mt: 0.2 }}>
|
||||
<ResponseStatus
|
||||
proto={response.proto}
|
||||
statusCode={response.statusCode}
|
||||
statusReason={response.statusReason}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<ResponseTabs
|
||||
body={response?.body}
|
||||
headers={sortKeyValuePairs(response?.headers || [])}
|
||||
hasResponse={response !== undefined && response !== null}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Response;
|
38
admin/src/lib/components/ResponseStatus.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
import HttpStatusIcon from "./HttpStatusIcon";
|
||||
|
||||
import { HttpProtocol } from "lib/graphql/generated";
|
||||
|
||||
type ResponseStatusProps = {
|
||||
proto: HttpProtocol;
|
||||
statusCode: number;
|
||||
statusReason: string;
|
||||
};
|
||||
|
||||
function mapProto(proto: HttpProtocol): string {
|
||||
switch (proto) {
|
||||
case HttpProtocol.Http10:
|
||||
return "HTTP/1.0";
|
||||
case HttpProtocol.Http11:
|
||||
return "HTTP/1.1";
|
||||
case HttpProtocol.Http20:
|
||||
return "HTTP/2.0";
|
||||
default:
|
||||
return proto;
|
||||
}
|
||||
}
|
||||
|
||||
export default function ResponseStatus({ proto, statusCode, statusReason }: ResponseStatusProps): JSX.Element {
|
||||
return (
|
||||
<Typography variant="h6" style={{ fontSize: "1rem", whiteSpace: "nowrap" }}>
|
||||
<HttpStatusIcon status={statusCode} />{" "}
|
||||
<Typography component="span" color="textSecondary">
|
||||
<Typography component="span" color="textSecondary" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
||||
{mapProto(proto)}
|
||||
</Typography>
|
||||
</Typography>{" "}
|
||||
{statusCode} {statusReason}
|
||||
</Typography>
|
||||
);
|
||||
}
|
68
admin/src/lib/components/ResponseTabs.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||
import { Box, Paper, Tab, Typography } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Editor from "lib/components/Editor";
|
||||
import { KeyValuePairTable } from "lib/components/KeyValuePair";
|
||||
import { HttpResponseLog } from "lib/graphql/generated";
|
||||
|
||||
interface ResponseTabsProps {
|
||||
headers: HttpResponseLog["headers"];
|
||||
body: HttpResponseLog["body"];
|
||||
hasResponse: boolean;
|
||||
}
|
||||
|
||||
enum TabValue {
|
||||
Body = "body",
|
||||
Headers = "headers",
|
||||
}
|
||||
|
||||
const reqNotSent = (
|
||||
<Paper variant="centered">
|
||||
<Typography>Response not received yet.</Typography>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
function ResponseTabs(props: ResponseTabsProps): JSX.Element {
|
||||
const { headers, body, hasResponse } = props;
|
||||
const [tabValue, setTabValue] = useState(TabValue.Body);
|
||||
|
||||
const contentType = headers.find((header) => header.key.toLowerCase() === "content-type")?.value;
|
||||
|
||||
const tabSx = {
|
||||
textTransform: "none",
|
||||
};
|
||||
|
||||
return (
|
||||
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<TabContext value={tabValue}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 1 }}>
|
||||
<TabList onChange={(_, value) => setTabValue(value)}>
|
||||
<Tab
|
||||
value={TabValue.Body}
|
||||
label={"Body" + (body?.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
<Tab
|
||||
value={TabValue.Headers}
|
||||
label={"Headers" + (headers.length ? ` (${headers.length})` : "")}
|
||||
sx={tabSx}
|
||||
/>
|
||||
</TabList>
|
||||
</Box>
|
||||
<Box flex="1 auto" overflow="hidden">
|
||||
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
|
||||
{body && <Editor content={body} contentType={contentType} />}
|
||||
{!hasResponse && reqNotSent}
|
||||
</TabPanel>
|
||||
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
|
||||
{headers.length > 0 && <KeyValuePairTable items={headers} />}
|
||||
{!hasResponse && reqNotSent}
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</TabContext>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResponseTabs;
|
53
admin/src/lib/components/SplitPane.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { alpha, styled } from "@mui/material/styles";
|
||||
import ReactSplitPane, { SplitPaneProps } from "react-split-pane";
|
||||
|
||||
const BORDER_WIDTH_FACTOR = 1.75;
|
||||
const SIZE_FACTOR = 4;
|
||||
const MARGIN_FACTOR = -1.75;
|
||||
|
||||
const SplitPane = styled(ReactSplitPane)<SplitPaneProps>(({ theme }) => ({
|
||||
".Resizer": {
|
||||
zIndex: theme.zIndex.mobileStepper,
|
||||
boxSizing: "border-box",
|
||||
backgroundClip: "padding-box",
|
||||
backgroundColor: alpha(theme.palette.grey[400], 0.05),
|
||||
},
|
||||
".Resizer:hover": {
|
||||
transition: "all 0.5s ease",
|
||||
backgroundColor: alpha(theme.palette.primary.main, 1),
|
||||
},
|
||||
|
||||
".Resizer.horizontal": {
|
||||
height: theme.spacing(SIZE_FACTOR),
|
||||
marginTop: theme.spacing(MARGIN_FACTOR),
|
||||
marginBottom: theme.spacing(MARGIN_FACTOR),
|
||||
borderTop: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`,
|
||||
borderBottom: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`,
|
||||
borderBottomColor: "rgba(255, 255, 255, 0)",
|
||||
cursor: "row-resize",
|
||||
width: "100%",
|
||||
},
|
||||
|
||||
".Resizer.vertical": {
|
||||
width: theme.spacing(SIZE_FACTOR),
|
||||
marginLeft: theme.spacing(MARGIN_FACTOR),
|
||||
marginRight: theme.spacing(MARGIN_FACTOR),
|
||||
borderLeft: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`,
|
||||
borderRight: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`,
|
||||
cursor: "col-resize",
|
||||
},
|
||||
|
||||
".Resizer.disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
|
||||
".Resizer.disabled:hover": {
|
||||
borderColor: "transparent",
|
||||
},
|
||||
|
||||
".Pane": {
|
||||
overflow: "hidden",
|
||||
},
|
||||
}));
|
||||
|
||||
export default SplitPane;
|
49
admin/src/lib/components/useContextMenu.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Menu } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface ContextMenuProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function useContextMenu(): [
|
||||
(props: ContextMenuProps) => JSX.Element,
|
||||
(e: React.MouseEvent) => void,
|
||||
() => void
|
||||
] {
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setContextMenu(
|
||||
contextMenu === null
|
||||
? {
|
||||
mouseX: event.clientX - 2,
|
||||
mouseY: event.clientY - 4,
|
||||
}
|
||||
: // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
|
||||
// Other native context menus might behave different.
|
||||
// With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setContextMenu(null);
|
||||
};
|
||||
|
||||
const menu = ({ children }: ContextMenuProps): JSX.Element => (
|
||||
<Menu
|
||||
open={contextMenu !== null}
|
||||
onClose={handleClose}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={contextMenu !== null ? { top: contextMenu.mouseY, left: contextMenu.mouseX } : undefined}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return [menu, handleContextMenu, handleClose];
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
|
||||
import { concatPagination } from "@apollo/client/utilities";
|
||||
|
||||
let apolloClient;
|
||||
|
||||
function createApolloClient() {
|
||||
return new ApolloClient({
|
||||
ssrMode: typeof window === "undefined",
|
||||
link: new HttpLink({
|
||||
uri: "/api/graphql/",
|
||||
}),
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
allPosts: concatPagination(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function initializeApollo(initialState = null) {
|
||||
const _apolloClient = apolloClient ?? createApolloClient();
|
||||
|
||||
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
|
||||
// gets hydrated here
|
||||
if (initialState) {
|
||||
// Get existing cache, loaded during client side data fetching
|
||||
const existingCache = _apolloClient.extract();
|
||||
// Restore the cache using the data passed from getStaticProps/getServerSideProps
|
||||
// combined with the existing cached data
|
||||
_apolloClient.cache.restore({ ...existingCache, ...initialState });
|
||||
}
|
||||
// For SSG and SSR always create a new Apollo Client
|
||||
if (typeof window === "undefined") return _apolloClient;
|
||||
// Create the Apollo Client once in the client
|
||||
if (!apolloClient) apolloClient = _apolloClient;
|
||||
|
||||
return _apolloClient;
|
||||
}
|
||||
|
||||
export function useApollo(initialState) {
|
||||
const store = useMemo(() => initializeApollo(initialState), [initialState]);
|
||||
return store;
|
||||
}
|
985
admin/src/lib/graphql/generated.tsx
Normal file
@ -0,0 +1,985 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||
const defaultOptions = {} as const;
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
String: string;
|
||||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
Regexp: any;
|
||||
Time: any;
|
||||
URL: any;
|
||||
};
|
||||
|
||||
export type ClearHttpRequestLogResult = {
|
||||
__typename?: 'ClearHTTPRequestLogResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type CloseProjectResult = {
|
||||
__typename?: 'CloseProjectResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type DeleteProjectResult = {
|
||||
__typename?: 'DeleteProjectResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type DeleteSenderRequestsResult = {
|
||||
__typename?: 'DeleteSenderRequestsResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type HttpHeader = {
|
||||
__typename?: 'HttpHeader';
|
||||
key: Scalars['String'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export type HttpHeaderInput = {
|
||||
key: Scalars['String'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum HttpMethod {
|
||||
Connect = 'CONNECT',
|
||||
Delete = 'DELETE',
|
||||
Get = 'GET',
|
||||
Head = 'HEAD',
|
||||
Options = 'OPTIONS',
|
||||
Patch = 'PATCH',
|
||||
Post = 'POST',
|
||||
Put = 'PUT',
|
||||
Trace = 'TRACE'
|
||||
}
|
||||
|
||||
export enum HttpProtocol {
|
||||
Http10 = 'HTTP10',
|
||||
Http11 = 'HTTP11',
|
||||
Http20 = 'HTTP20'
|
||||
}
|
||||
|
||||
export type HttpRequestLog = {
|
||||
__typename?: 'HttpRequestLog';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
headers: Array<HttpHeader>;
|
||||
id: Scalars['ID'];
|
||||
method: HttpMethod;
|
||||
proto: Scalars['String'];
|
||||
response?: Maybe<HttpResponseLog>;
|
||||
timestamp: Scalars['Time'];
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
export type HttpRequestLogFilter = {
|
||||
__typename?: 'HttpRequestLogFilter';
|
||||
onlyInScope: Scalars['Boolean'];
|
||||
searchExpression?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type HttpRequestLogFilterInput = {
|
||||
onlyInScope?: InputMaybe<Scalars['Boolean']>;
|
||||
searchExpression?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type HttpResponseLog = {
|
||||
__typename?: 'HttpResponseLog';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
headers: Array<HttpHeader>;
|
||||
/** Will be the same ID as its related request ID. */
|
||||
id: Scalars['ID'];
|
||||
proto: HttpProtocol;
|
||||
statusCode: Scalars['Int'];
|
||||
statusReason: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
clearHTTPRequestLog: ClearHttpRequestLogResult;
|
||||
closeProject: CloseProjectResult;
|
||||
createOrUpdateSenderRequest: SenderRequest;
|
||||
createProject?: Maybe<Project>;
|
||||
createSenderRequestFromHttpRequestLog: SenderRequest;
|
||||
deleteProject: DeleteProjectResult;
|
||||
deleteSenderRequests: DeleteSenderRequestsResult;
|
||||
openProject?: Maybe<Project>;
|
||||
sendRequest: SenderRequest;
|
||||
setHttpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||
setScope: Array<ScopeRule>;
|
||||
setSenderRequestFilter?: Maybe<SenderRequestFilter>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOrUpdateSenderRequestArgs = {
|
||||
request: SenderRequestInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateProjectArgs = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateSenderRequestFromHttpRequestLogArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteProjectArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationOpenProjectArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSendRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetHttpRequestLogFilterArgs = {
|
||||
filter?: InputMaybe<HttpRequestLogFilterInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetScopeArgs = {
|
||||
scope: Array<ScopeRuleInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetSenderRequestFilterArgs = {
|
||||
filter?: InputMaybe<SenderRequestFilterInput>;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
__typename?: 'Project';
|
||||
id: Scalars['ID'];
|
||||
isActive: Scalars['Boolean'];
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
activeProject?: Maybe<Project>;
|
||||
httpRequestLog?: Maybe<HttpRequestLog>;
|
||||
httpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
|
||||
httpRequestLogs: Array<HttpRequestLog>;
|
||||
projects: Array<Project>;
|
||||
scope: Array<ScopeRule>;
|
||||
senderRequest?: Maybe<SenderRequest>;
|
||||
senderRequests: Array<SenderRequest>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryHttpRequestLogArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QuerySenderRequestArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type ScopeHeader = {
|
||||
__typename?: 'ScopeHeader';
|
||||
key?: Maybe<Scalars['Regexp']>;
|
||||
value?: Maybe<Scalars['Regexp']>;
|
||||
};
|
||||
|
||||
export type ScopeHeaderInput = {
|
||||
key?: InputMaybe<Scalars['Regexp']>;
|
||||
value?: InputMaybe<Scalars['Regexp']>;
|
||||
};
|
||||
|
||||
export type ScopeRule = {
|
||||
__typename?: 'ScopeRule';
|
||||
body?: Maybe<Scalars['Regexp']>;
|
||||
header?: Maybe<ScopeHeader>;
|
||||
url?: Maybe<Scalars['Regexp']>;
|
||||
};
|
||||
|
||||
export type ScopeRuleInput = {
|
||||
body?: InputMaybe<Scalars['Regexp']>;
|
||||
header?: InputMaybe<ScopeHeaderInput>;
|
||||
url?: InputMaybe<Scalars['Regexp']>;
|
||||
};
|
||||
|
||||
export type SenderRequest = {
|
||||
__typename?: 'SenderRequest';
|
||||
body?: Maybe<Scalars['String']>;
|
||||
headers?: Maybe<Array<HttpHeader>>;
|
||||
id: Scalars['ID'];
|
||||
method: HttpMethod;
|
||||
proto: HttpProtocol;
|
||||
response?: Maybe<HttpResponseLog>;
|
||||
sourceRequestLogID?: Maybe<Scalars['ID']>;
|
||||
timestamp: Scalars['Time'];
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type SenderRequestFilter = {
|
||||
__typename?: 'SenderRequestFilter';
|
||||
onlyInScope: Scalars['Boolean'];
|
||||
searchExpression?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SenderRequestFilterInput = {
|
||||
onlyInScope?: InputMaybe<Scalars['Boolean']>;
|
||||
searchExpression?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SenderRequestInput = {
|
||||
body?: InputMaybe<Scalars['String']>;
|
||||
headers?: InputMaybe<Array<HttpHeaderInput>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
method?: InputMaybe<HttpMethod>;
|
||||
proto?: InputMaybe<HttpProtocol>;
|
||||
url: Scalars['URL'];
|
||||
};
|
||||
|
||||
export type CloseProjectMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type CloseProjectMutation = { __typename?: 'Mutation', closeProject: { __typename?: 'CloseProjectResult', success: boolean } };
|
||||
|
||||
export type CreateProjectMutationVariables = Exact<{
|
||||
name: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateProjectMutation = { __typename?: 'Mutation', createProject?: { __typename?: 'Project', id: string, name: string } | null };
|
||||
|
||||
export type DeleteProjectMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteProjectMutation = { __typename?: 'Mutation', deleteProject: { __typename?: 'DeleteProjectResult', success: boolean } };
|
||||
|
||||
export type OpenProjectMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type OpenProjectMutation = { __typename?: 'Mutation', openProject?: { __typename?: 'Project', id: string, name: string, isActive: boolean } | null };
|
||||
|
||||
export type ProjectsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ProjectsQuery = { __typename?: 'Query', projects: Array<{ __typename?: 'Project', id: string, name: string, isActive: boolean }> };
|
||||
|
||||
export type ClearHttpRequestLogMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ClearHttpRequestLogMutation = { __typename?: 'Mutation', clearHTTPRequestLog: { __typename?: 'ClearHTTPRequestLogResult', success: boolean } };
|
||||
|
||||
export type HttpRequestLogQueryVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type HttpRequestLogQuery = { __typename?: 'Query', httpRequestLog?: { __typename?: 'HttpRequestLog', id: string, method: HttpMethod, url: string, proto: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }>, response?: { __typename?: 'HttpResponseLog', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null };
|
||||
|
||||
export type HttpRequestLogFilterQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type HttpRequestLogFilterQuery = { __typename?: 'Query', httpRequestLogFilter?: { __typename?: 'HttpRequestLogFilter', onlyInScope: boolean, searchExpression?: string | null } | null };
|
||||
|
||||
export type HttpRequestLogsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type HttpRequestLogsQuery = { __typename?: 'Query', httpRequestLogs: Array<{ __typename?: 'HttpRequestLog', id: string, method: HttpMethod, url: string, timestamp: any, response?: { __typename?: 'HttpResponseLog', statusCode: number, statusReason: string } | null }> };
|
||||
|
||||
export type SetHttpRequestLogFilterMutationVariables = Exact<{
|
||||
filter?: InputMaybe<HttpRequestLogFilterInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type SetHttpRequestLogFilterMutation = { __typename?: 'Mutation', setHttpRequestLogFilter?: { __typename?: 'HttpRequestLogFilter', onlyInScope: boolean, searchExpression?: string | null } | null };
|
||||
|
||||
export type ScopeQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ScopeQuery = { __typename?: 'Query', scope: Array<{ __typename?: 'ScopeRule', url?: any | null }> };
|
||||
|
||||
export type SetScopeMutationVariables = Exact<{
|
||||
scope: Array<ScopeRuleInput> | ScopeRuleInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type SetScopeMutation = { __typename?: 'Mutation', setScope: Array<{ __typename?: 'ScopeRule', url?: any | null }> };
|
||||
|
||||
export type CreateOrUpdateSenderRequestMutationVariables = Exact<{
|
||||
request: SenderRequestInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateOrUpdateSenderRequestMutation = { __typename?: 'Mutation', createOrUpdateSenderRequest: { __typename?: 'SenderRequest', id: string } };
|
||||
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateSenderRequestFromHttpRequestLogMutation = { __typename?: 'Mutation', createSenderRequestFromHttpRequestLog: { __typename?: 'SenderRequest', id: string } };
|
||||
|
||||
export type SendRequestMutationVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SendRequestMutation = { __typename?: 'Mutation', sendRequest: { __typename?: 'SenderRequest', id: string } };
|
||||
|
||||
export type GetSenderRequestQueryVariables = Exact<{
|
||||
id: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetSenderRequestQuery = { __typename?: 'Query', senderRequest?: { __typename?: 'SenderRequest', id: string, sourceRequestLogID?: string | null, url: any, method: HttpMethod, proto: HttpProtocol, body?: string | null, timestamp: any, headers?: Array<{ __typename?: 'HttpHeader', key: string, value: string }> | null, response?: { __typename?: 'HttpResponseLog', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null };
|
||||
|
||||
export type GetSenderRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetSenderRequestsQuery = { __typename?: 'Query', senderRequests: Array<{ __typename?: 'SenderRequest', id: string, url: any, method: HttpMethod, response?: { __typename?: 'HttpResponseLog', id: string, statusCode: number, statusReason: string } | null }> };
|
||||
|
||||
|
||||
export const CloseProjectDocument = gql`
|
||||
mutation CloseProject {
|
||||
closeProject {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CloseProjectMutationFn = Apollo.MutationFunction<CloseProjectMutation, CloseProjectMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCloseProjectMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCloseProjectMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCloseProjectMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [closeProjectMutation, { data, loading, error }] = useCloseProjectMutation({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCloseProjectMutation(baseOptions?: Apollo.MutationHookOptions<CloseProjectMutation, CloseProjectMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CloseProjectMutation, CloseProjectMutationVariables>(CloseProjectDocument, options);
|
||||
}
|
||||
export type CloseProjectMutationHookResult = ReturnType<typeof useCloseProjectMutation>;
|
||||
export type CloseProjectMutationResult = Apollo.MutationResult<CloseProjectMutation>;
|
||||
export type CloseProjectMutationOptions = Apollo.BaseMutationOptions<CloseProjectMutation, CloseProjectMutationVariables>;
|
||||
export const CreateProjectDocument = gql`
|
||||
mutation CreateProject($name: String!) {
|
||||
createProject(name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateProjectMutationFn = Apollo.MutationFunction<CreateProjectMutation, CreateProjectMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateProjectMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateProjectMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateProjectMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [createProjectMutation, { data, loading, error }] = useCreateProjectMutation({
|
||||
* variables: {
|
||||
* name: // value for 'name'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateProjectMutation(baseOptions?: Apollo.MutationHookOptions<CreateProjectMutation, CreateProjectMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateProjectMutation, CreateProjectMutationVariables>(CreateProjectDocument, options);
|
||||
}
|
||||
export type CreateProjectMutationHookResult = ReturnType<typeof useCreateProjectMutation>;
|
||||
export type CreateProjectMutationResult = Apollo.MutationResult<CreateProjectMutation>;
|
||||
export type CreateProjectMutationOptions = Apollo.BaseMutationOptions<CreateProjectMutation, CreateProjectMutationVariables>;
|
||||
export const DeleteProjectDocument = gql`
|
||||
mutation DeleteProject($id: ID!) {
|
||||
deleteProject(id: $id) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DeleteProjectMutationFn = Apollo.MutationFunction<DeleteProjectMutation, DeleteProjectMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteProjectMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteProjectMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteProjectMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [deleteProjectMutation, { data, loading, error }] = useDeleteProjectMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteProjectMutation(baseOptions?: Apollo.MutationHookOptions<DeleteProjectMutation, DeleteProjectMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteProjectMutation, DeleteProjectMutationVariables>(DeleteProjectDocument, options);
|
||||
}
|
||||
export type DeleteProjectMutationHookResult = ReturnType<typeof useDeleteProjectMutation>;
|
||||
export type DeleteProjectMutationResult = Apollo.MutationResult<DeleteProjectMutation>;
|
||||
export type DeleteProjectMutationOptions = Apollo.BaseMutationOptions<DeleteProjectMutation, DeleteProjectMutationVariables>;
|
||||
export const OpenProjectDocument = gql`
|
||||
mutation OpenProject($id: ID!) {
|
||||
openProject(id: $id) {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type OpenProjectMutationFn = Apollo.MutationFunction<OpenProjectMutation, OpenProjectMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useOpenProjectMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useOpenProjectMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useOpenProjectMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [openProjectMutation, { data, loading, error }] = useOpenProjectMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useOpenProjectMutation(baseOptions?: Apollo.MutationHookOptions<OpenProjectMutation, OpenProjectMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<OpenProjectMutation, OpenProjectMutationVariables>(OpenProjectDocument, options);
|
||||
}
|
||||
export type OpenProjectMutationHookResult = ReturnType<typeof useOpenProjectMutation>;
|
||||
export type OpenProjectMutationResult = Apollo.MutationResult<OpenProjectMutation>;
|
||||
export type OpenProjectMutationOptions = Apollo.BaseMutationOptions<OpenProjectMutation, OpenProjectMutationVariables>;
|
||||
export const ProjectsDocument = gql`
|
||||
query Projects {
|
||||
projects {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useProjectsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useProjectsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useProjectsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useProjectsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useProjectsQuery(baseOptions?: Apollo.QueryHookOptions<ProjectsQuery, ProjectsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ProjectsQuery, ProjectsQueryVariables>(ProjectsDocument, options);
|
||||
}
|
||||
export function useProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProjectsQuery, ProjectsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ProjectsQuery, ProjectsQueryVariables>(ProjectsDocument, options);
|
||||
}
|
||||
export type ProjectsQueryHookResult = ReturnType<typeof useProjectsQuery>;
|
||||
export type ProjectsLazyQueryHookResult = ReturnType<typeof useProjectsLazyQuery>;
|
||||
export type ProjectsQueryResult = Apollo.QueryResult<ProjectsQuery, ProjectsQueryVariables>;
|
||||
export const ClearHttpRequestLogDocument = gql`
|
||||
mutation ClearHTTPRequestLog {
|
||||
clearHTTPRequestLog {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type ClearHttpRequestLogMutationFn = Apollo.MutationFunction<ClearHttpRequestLogMutation, ClearHttpRequestLogMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useClearHttpRequestLogMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useClearHttpRequestLogMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useClearHttpRequestLogMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [clearHttpRequestLogMutation, { data, loading, error }] = useClearHttpRequestLogMutation({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useClearHttpRequestLogMutation(baseOptions?: Apollo.MutationHookOptions<ClearHttpRequestLogMutation, ClearHttpRequestLogMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ClearHttpRequestLogMutation, ClearHttpRequestLogMutationVariables>(ClearHttpRequestLogDocument, options);
|
||||
}
|
||||
export type ClearHttpRequestLogMutationHookResult = ReturnType<typeof useClearHttpRequestLogMutation>;
|
||||
export type ClearHttpRequestLogMutationResult = Apollo.MutationResult<ClearHttpRequestLogMutation>;
|
||||
export type ClearHttpRequestLogMutationOptions = Apollo.BaseMutationOptions<ClearHttpRequestLogMutation, ClearHttpRequestLogMutationVariables>;
|
||||
export const HttpRequestLogDocument = gql`
|
||||
query HttpRequestLog($id: ID!) {
|
||||
httpRequestLog(id: $id) {
|
||||
id
|
||||
method
|
||||
url
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
response {
|
||||
id
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
statusCode
|
||||
statusReason
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useHttpRequestLogQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useHttpRequestLogQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useHttpRequestLogQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useHttpRequestLogQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useHttpRequestLogQuery(baseOptions: Apollo.QueryHookOptions<HttpRequestLogQuery, HttpRequestLogQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<HttpRequestLogQuery, HttpRequestLogQueryVariables>(HttpRequestLogDocument, options);
|
||||
}
|
||||
export function useHttpRequestLogLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HttpRequestLogQuery, HttpRequestLogQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<HttpRequestLogQuery, HttpRequestLogQueryVariables>(HttpRequestLogDocument, options);
|
||||
}
|
||||
export type HttpRequestLogQueryHookResult = ReturnType<typeof useHttpRequestLogQuery>;
|
||||
export type HttpRequestLogLazyQueryHookResult = ReturnType<typeof useHttpRequestLogLazyQuery>;
|
||||
export type HttpRequestLogQueryResult = Apollo.QueryResult<HttpRequestLogQuery, HttpRequestLogQueryVariables>;
|
||||
export const HttpRequestLogFilterDocument = gql`
|
||||
query HttpRequestLogFilter {
|
||||
httpRequestLogFilter {
|
||||
onlyInScope
|
||||
searchExpression
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useHttpRequestLogFilterQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useHttpRequestLogFilterQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useHttpRequestLogFilterQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useHttpRequestLogFilterQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useHttpRequestLogFilterQuery(baseOptions?: Apollo.QueryHookOptions<HttpRequestLogFilterQuery, HttpRequestLogFilterQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<HttpRequestLogFilterQuery, HttpRequestLogFilterQueryVariables>(HttpRequestLogFilterDocument, options);
|
||||
}
|
||||
export function useHttpRequestLogFilterLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HttpRequestLogFilterQuery, HttpRequestLogFilterQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<HttpRequestLogFilterQuery, HttpRequestLogFilterQueryVariables>(HttpRequestLogFilterDocument, options);
|
||||
}
|
||||
export type HttpRequestLogFilterQueryHookResult = ReturnType<typeof useHttpRequestLogFilterQuery>;
|
||||
export type HttpRequestLogFilterLazyQueryHookResult = ReturnType<typeof useHttpRequestLogFilterLazyQuery>;
|
||||
export type HttpRequestLogFilterQueryResult = Apollo.QueryResult<HttpRequestLogFilterQuery, HttpRequestLogFilterQueryVariables>;
|
||||
export const HttpRequestLogsDocument = gql`
|
||||
query HttpRequestLogs {
|
||||
httpRequestLogs {
|
||||
id
|
||||
method
|
||||
url
|
||||
timestamp
|
||||
response {
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useHttpRequestLogsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useHttpRequestLogsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useHttpRequestLogsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useHttpRequestLogsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useHttpRequestLogsQuery(baseOptions?: Apollo.QueryHookOptions<HttpRequestLogsQuery, HttpRequestLogsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<HttpRequestLogsQuery, HttpRequestLogsQueryVariables>(HttpRequestLogsDocument, options);
|
||||
}
|
||||
export function useHttpRequestLogsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HttpRequestLogsQuery, HttpRequestLogsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<HttpRequestLogsQuery, HttpRequestLogsQueryVariables>(HttpRequestLogsDocument, options);
|
||||
}
|
||||
export type HttpRequestLogsQueryHookResult = ReturnType<typeof useHttpRequestLogsQuery>;
|
||||
export type HttpRequestLogsLazyQueryHookResult = ReturnType<typeof useHttpRequestLogsLazyQuery>;
|
||||
export type HttpRequestLogsQueryResult = Apollo.QueryResult<HttpRequestLogsQuery, HttpRequestLogsQueryVariables>;
|
||||
export const SetHttpRequestLogFilterDocument = gql`
|
||||
mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) {
|
||||
setHttpRequestLogFilter(filter: $filter) {
|
||||
onlyInScope
|
||||
searchExpression
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SetHttpRequestLogFilterMutationFn = Apollo.MutationFunction<SetHttpRequestLogFilterMutation, SetHttpRequestLogFilterMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSetHttpRequestLogFilterMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSetHttpRequestLogFilterMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSetHttpRequestLogFilterMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [setHttpRequestLogFilterMutation, { data, loading, error }] = useSetHttpRequestLogFilterMutation({
|
||||
* variables: {
|
||||
* filter: // value for 'filter'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSetHttpRequestLogFilterMutation(baseOptions?: Apollo.MutationHookOptions<SetHttpRequestLogFilterMutation, SetHttpRequestLogFilterMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SetHttpRequestLogFilterMutation, SetHttpRequestLogFilterMutationVariables>(SetHttpRequestLogFilterDocument, options);
|
||||
}
|
||||
export type SetHttpRequestLogFilterMutationHookResult = ReturnType<typeof useSetHttpRequestLogFilterMutation>;
|
||||
export type SetHttpRequestLogFilterMutationResult = Apollo.MutationResult<SetHttpRequestLogFilterMutation>;
|
||||
export type SetHttpRequestLogFilterMutationOptions = Apollo.BaseMutationOptions<SetHttpRequestLogFilterMutation, SetHttpRequestLogFilterMutationVariables>;
|
||||
export const ScopeDocument = gql`
|
||||
query Scope {
|
||||
scope {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useScopeQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useScopeQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useScopeQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useScopeQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useScopeQuery(baseOptions?: Apollo.QueryHookOptions<ScopeQuery, ScopeQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ScopeQuery, ScopeQueryVariables>(ScopeDocument, options);
|
||||
}
|
||||
export function useScopeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ScopeQuery, ScopeQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ScopeQuery, ScopeQueryVariables>(ScopeDocument, options);
|
||||
}
|
||||
export type ScopeQueryHookResult = ReturnType<typeof useScopeQuery>;
|
||||
export type ScopeLazyQueryHookResult = ReturnType<typeof useScopeLazyQuery>;
|
||||
export type ScopeQueryResult = Apollo.QueryResult<ScopeQuery, ScopeQueryVariables>;
|
||||
export const SetScopeDocument = gql`
|
||||
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
||||
setScope(scope: $scope) {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SetScopeMutationFn = Apollo.MutationFunction<SetScopeMutation, SetScopeMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSetScopeMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSetScopeMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSetScopeMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [setScopeMutation, { data, loading, error }] = useSetScopeMutation({
|
||||
* variables: {
|
||||
* scope: // value for 'scope'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSetScopeMutation(baseOptions?: Apollo.MutationHookOptions<SetScopeMutation, SetScopeMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SetScopeMutation, SetScopeMutationVariables>(SetScopeDocument, options);
|
||||
}
|
||||
export type SetScopeMutationHookResult = ReturnType<typeof useSetScopeMutation>;
|
||||
export type SetScopeMutationResult = Apollo.MutationResult<SetScopeMutation>;
|
||||
export type SetScopeMutationOptions = Apollo.BaseMutationOptions<SetScopeMutation, SetScopeMutationVariables>;
|
||||
export const CreateOrUpdateSenderRequestDocument = gql`
|
||||
mutation CreateOrUpdateSenderRequest($request: SenderRequestInput!) {
|
||||
createOrUpdateSenderRequest(request: $request) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateOrUpdateSenderRequestMutationFn = Apollo.MutationFunction<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateOrUpdateSenderRequestMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateOrUpdateSenderRequestMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateOrUpdateSenderRequestMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [createOrUpdateSenderRequestMutation, { data, loading, error }] = useCreateOrUpdateSenderRequestMutation({
|
||||
* variables: {
|
||||
* request: // value for 'request'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateOrUpdateSenderRequestMutation(baseOptions?: Apollo.MutationHookOptions<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>(CreateOrUpdateSenderRequestDocument, options);
|
||||
}
|
||||
export type CreateOrUpdateSenderRequestMutationHookResult = ReturnType<typeof useCreateOrUpdateSenderRequestMutation>;
|
||||
export type CreateOrUpdateSenderRequestMutationResult = Apollo.MutationResult<CreateOrUpdateSenderRequestMutation>;
|
||||
export type CreateOrUpdateSenderRequestMutationOptions = Apollo.BaseMutationOptions<CreateOrUpdateSenderRequestMutation, CreateOrUpdateSenderRequestMutationVariables>;
|
||||
export const CreateSenderRequestFromHttpRequestLogDocument = gql`
|
||||
mutation CreateSenderRequestFromHttpRequestLog($id: ID!) {
|
||||
createSenderRequestFromHttpRequestLog(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationFn = Apollo.MutationFunction<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateSenderRequestFromHttpRequestLogMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateSenderRequestFromHttpRequestLogMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateSenderRequestFromHttpRequestLogMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [createSenderRequestFromHttpRequestLogMutation, { data, loading, error }] = useCreateSenderRequestFromHttpRequestLogMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateSenderRequestFromHttpRequestLogMutation(baseOptions?: Apollo.MutationHookOptions<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>(CreateSenderRequestFromHttpRequestLogDocument, options);
|
||||
}
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationHookResult = ReturnType<typeof useCreateSenderRequestFromHttpRequestLogMutation>;
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationResult = Apollo.MutationResult<CreateSenderRequestFromHttpRequestLogMutation>;
|
||||
export type CreateSenderRequestFromHttpRequestLogMutationOptions = Apollo.BaseMutationOptions<CreateSenderRequestFromHttpRequestLogMutation, CreateSenderRequestFromHttpRequestLogMutationVariables>;
|
||||
export const SendRequestDocument = gql`
|
||||
mutation SendRequest($id: ID!) {
|
||||
sendRequest(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SendRequestMutationFn = Apollo.MutationFunction<SendRequestMutation, SendRequestMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSendRequestMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSendRequestMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSendRequestMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [sendRequestMutation, { data, loading, error }] = useSendRequestMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSendRequestMutation(baseOptions?: Apollo.MutationHookOptions<SendRequestMutation, SendRequestMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SendRequestMutation, SendRequestMutationVariables>(SendRequestDocument, options);
|
||||
}
|
||||
export type SendRequestMutationHookResult = ReturnType<typeof useSendRequestMutation>;
|
||||
export type SendRequestMutationResult = Apollo.MutationResult<SendRequestMutation>;
|
||||
export type SendRequestMutationOptions = Apollo.BaseMutationOptions<SendRequestMutation, SendRequestMutationVariables>;
|
||||
export const GetSenderRequestDocument = gql`
|
||||
query GetSenderRequest($id: ID!) {
|
||||
senderRequest(id: $id) {
|
||||
id
|
||||
sourceRequestLogID
|
||||
url
|
||||
method
|
||||
proto
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
body
|
||||
timestamp
|
||||
response {
|
||||
id
|
||||
proto
|
||||
statusCode
|
||||
statusReason
|
||||
body
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetSenderRequestQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetSenderRequestQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetSenderRequestQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetSenderRequestQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetSenderRequestQuery(baseOptions: Apollo.QueryHookOptions<GetSenderRequestQuery, GetSenderRequestQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetSenderRequestQuery, GetSenderRequestQueryVariables>(GetSenderRequestDocument, options);
|
||||
}
|
||||
export function useGetSenderRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetSenderRequestQuery, GetSenderRequestQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetSenderRequestQuery, GetSenderRequestQueryVariables>(GetSenderRequestDocument, options);
|
||||
}
|
||||
export type GetSenderRequestQueryHookResult = ReturnType<typeof useGetSenderRequestQuery>;
|
||||
export type GetSenderRequestLazyQueryHookResult = ReturnType<typeof useGetSenderRequestLazyQuery>;
|
||||
export type GetSenderRequestQueryResult = Apollo.QueryResult<GetSenderRequestQuery, GetSenderRequestQueryVariables>;
|
||||
export const GetSenderRequestsDocument = gql`
|
||||
query GetSenderRequests {
|
||||
senderRequests {
|
||||
id
|
||||
url
|
||||
method
|
||||
response {
|
||||
id
|
||||
statusCode
|
||||
statusReason
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetSenderRequestsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetSenderRequestsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetSenderRequestsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetSenderRequestsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetSenderRequestsQuery(baseOptions?: Apollo.QueryHookOptions<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>(GetSenderRequestsDocument, options);
|
||||
}
|
||||
export function useGetSenderRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>(GetSenderRequestsDocument, options);
|
||||
}
|
||||
export type GetSenderRequestsQueryHookResult = ReturnType<typeof useGetSenderRequestsQuery>;
|
||||
export type GetSenderRequestsLazyQueryHookResult = ReturnType<typeof useGetSenderRequestsLazyQuery>;
|
||||
export type GetSenderRequestsQueryResult = Apollo.QueryResult<GetSenderRequestsQuery, GetSenderRequestsQueryVariables>;
|
7
admin/src/lib/graphql/omitTypename.ts
Normal file
@ -0,0 +1,7 @@
|
||||
function omitTypename<T>(key: string, value: T): T | undefined {
|
||||
return key === "__typename" ? undefined : value;
|
||||
}
|
||||
|
||||
export function withoutTypename<T>(input: T): T {
|
||||
return JSON.parse(JSON.stringify(input), omitTypename);
|
||||
}
|
24
admin/src/lib/graphql/useApollo.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject } from "@apollo/client";
|
||||
|
||||
let apolloClient: ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
function createApolloClient() {
|
||||
return new ApolloClient({
|
||||
ssrMode: typeof window === "undefined",
|
||||
link: new HttpLink({
|
||||
uri: "/api/graphql/",
|
||||
}),
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useApollo() {
|
||||
const _apolloClient = apolloClient ?? createApolloClient();
|
||||
|
||||
// For SSG and SSR always create a new Apollo Client
|
||||
if (typeof window === "undefined") return _apolloClient;
|
||||
// Create the Apollo Client once in the client
|
||||
if (!apolloClient) apolloClient = _apolloClient;
|
||||
|
||||
return _apolloClient;
|
||||
}
|
7
admin/src/lib/mui/createEmotionCache.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import createCache from "@emotion/cache";
|
||||
|
||||
// prepend: true moves MUI styles to the top of the <head> so they're loaded first.
|
||||
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
|
||||
export default function createEmotionCache() {
|
||||
return createCache({ key: "css", prepend: true });
|
||||
}
|
75
admin/src/lib/mui/theme.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import * as colors from "@mui/material/colors";
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
declare module "@mui/material/Paper" {
|
||||
interface PaperPropsVariantOverrides {
|
||||
centered: true;
|
||||
}
|
||||
}
|
||||
|
||||
const heading = {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
let theme = createTheme({
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: {
|
||||
main: colors.teal["A400"],
|
||||
},
|
||||
secondary: {
|
||||
main: colors.grey[900],
|
||||
light: "#333",
|
||||
dark: colors.common.black,
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
h2: heading,
|
||||
h3: heading,
|
||||
h4: heading,
|
||||
h5: heading,
|
||||
h6: heading,
|
||||
},
|
||||
});
|
||||
|
||||
theme = createTheme(theme, {
|
||||
palette: {
|
||||
background: {
|
||||
default: theme.palette.secondary.main,
|
||||
paper: theme.palette.secondary.light,
|
||||
},
|
||||
info: {
|
||||
main: theme.palette.primary.main,
|
||||
},
|
||||
success: {
|
||||
main: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&.Mui-selected, &.Mui-selected:hover": {
|
||||
backgroundColor: theme.palette.grey[700],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
variants: [
|
||||
{
|
||||
props: { variant: "centered" },
|
||||
style: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(4),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
17
admin/src/lib/queryParamsFromURL.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { KeyValuePair } from "./components/KeyValuePair";
|
||||
|
||||
export function queryParamsFromURL(url: string): KeyValuePair[] {
|
||||
const questionMarkIndex = url.indexOf("?");
|
||||
if (questionMarkIndex === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const queryParams: KeyValuePair[] = [];
|
||||
|
||||
const searchParams = new URLSearchParams(url.slice(questionMarkIndex + 1));
|
||||
for (const [key, value] of searchParams) {
|
||||
queryParams.push({ key, value });
|
||||
}
|
||||
|
||||
return queryParams;
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import { createMuiTheme } from "@material-ui/core/styles";
|
||||
import grey from "@material-ui/core/colors/grey";
|
||||
import teal from "@material-ui/core/colors/teal";
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
type: "dark",
|
||||
primary: {
|
||||
main: grey[900],
|
||||
},
|
||||
secondary: {
|
||||
main: teal["A400"],
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
h2: {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontWeight: 600,
|
||||
},
|
||||
h3: {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontWeight: 600,
|
||||
},
|
||||
h4: {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontWeight: 600,
|
||||
},
|
||||
h5: {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
overrides: {
|
||||
MuiTableCell: {
|
||||
stickyHeader: {
|
||||
backgroundColor: grey[900],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
@ -1,41 +1,43 @@
|
||||
import React from "react";
|
||||
import { AppProps } from "next/app";
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import { CacheProvider, EmotionCache } from "@emotion/react";
|
||||
import { ThemeProvider } from "@mui/material";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import { ThemeProvider } from "@material-ui/core/styles";
|
||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||
import React from "react";
|
||||
|
||||
import theme from "../lib/theme";
|
||||
import { useApollo } from "../lib/graphql";
|
||||
import { ActiveProjectProvider } from "lib/ActiveProjectContext";
|
||||
import { useApollo } from "lib/graphql/useApollo";
|
||||
import createEmotionCache from "lib/mui/createEmotionCache";
|
||||
import theme from "lib/mui/theme";
|
||||
|
||||
function App({ Component, pageProps }: AppProps): JSX.Element {
|
||||
const apolloClient = useApollo(pageProps.initialApolloState);
|
||||
import "../styles.css";
|
||||
|
||||
React.useEffect(() => {
|
||||
// Remove the server-side injected CSS.
|
||||
const jssStyles = document.querySelector("#jss-server-side");
|
||||
if (jssStyles) {
|
||||
jssStyles.parentElement.removeChild(jssStyles);
|
||||
}
|
||||
}, []);
|
||||
// Client-side cache, shared for the whole session of the user in the browser.
|
||||
const clientSideEmotionCache = createEmotionCache();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Head>
|
||||
<title>Hetty://</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||
/>
|
||||
</Head>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</ApolloProvider>
|
||||
</React.Fragment>
|
||||
);
|
||||
interface MyAppProps extends AppProps {
|
||||
emotionCache?: EmotionCache;
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default function MyApp(props: MyAppProps) {
|
||||
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
|
||||
const apolloClient = useApollo();
|
||||
|
||||
return (
|
||||
<CacheProvider value={emotionCache}>
|
||||
<Head>
|
||||
<title>Hetty://</title>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
</Head>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<ActiveProjectProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</ActiveProjectProvider>
|
||||
</ApolloProvider>
|
||||
</CacheProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,21 @@
|
||||
import React from "react";
|
||||
import createEmotionServer from "@emotion/server/create-instance";
|
||||
import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||
import { ServerStyleSheets } from "@material-ui/core/styles";
|
||||
import React from "react";
|
||||
|
||||
import theme from "../lib/theme";
|
||||
import createEmotionCache from "lib/mui/createEmotionCache";
|
||||
import theme from "lib/mui/theme";
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
/* eslint-disable */
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<meta name="theme-color" content={theme.palette.primary.main} />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap"
|
||||
/>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" />
|
||||
{(this.props as any).emotionStyleTags}
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
@ -30,25 +27,60 @@ export default class MyDocument extends Document {
|
||||
}
|
||||
|
||||
// `getInitialProps` belongs to `_document` (instead of `_app`),
|
||||
// it's compatible with server-side generation (SSG).
|
||||
// it's compatible with static-site generation (SSG).
|
||||
MyDocument.getInitialProps = async (ctx) => {
|
||||
// Render app and page and get the context of the page with collected side effects.
|
||||
const sheets = new ServerStyleSheets();
|
||||
// Resolution order
|
||||
//
|
||||
// On the server:
|
||||
// 1. app.getInitialProps
|
||||
// 2. page.getInitialProps
|
||||
// 3. document.getInitialProps
|
||||
// 4. app.render
|
||||
// 5. page.render
|
||||
// 6. document.render
|
||||
//
|
||||
// On the server with error:
|
||||
// 1. document.getInitialProps
|
||||
// 2. app.render
|
||||
// 3. page.render
|
||||
// 4. document.render
|
||||
//
|
||||
// On the client
|
||||
// 1. app.getInitialProps
|
||||
// 2. page.getInitialProps
|
||||
// 3. app.render
|
||||
// 4. page.render
|
||||
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
|
||||
// You can consider sharing the same emotion cache between all the SSR requests to speed up performance.
|
||||
// However, be aware that it can have global side effects.
|
||||
const cache = createEmotionCache();
|
||||
const { extractCriticalToChunks } = createEmotionServer(cache);
|
||||
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
|
||||
enhanceApp: (App: any) =>
|
||||
function EnhanceApp(props) {
|
||||
return <App emotionCache={cache} {...props} />;
|
||||
},
|
||||
});
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
// This is important. It prevents emotion to render invalid HTML.
|
||||
// See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153
|
||||
const emotionStyles = extractCriticalToChunks(initialProps.html);
|
||||
const emotionStyleTags = emotionStyles.styles.map((style) => (
|
||||
<style
|
||||
data-emotion={`${style.key} ${style.ids.join(" ")}`}
|
||||
key={style.key}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: style.css }}
|
||||
/>
|
||||
));
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
// Styles fragment is rendered after the app and page rendering finish.
|
||||
styles: [
|
||||
...React.Children.toArray(initialProps.styles),
|
||||
sheets.getStyleElement(),
|
||||
],
|
||||
emotionStyleTags,
|
||||
};
|
||||
};
|
||||
|
@ -1,79 +1,53 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
createStyles,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
|
||||
import SendIcon from "@material-ui/icons/Send";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
|
||||
import Layout, { Page } from "../components/Layout";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
titleHighlight: {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: "1.6rem",
|
||||
width: "60%",
|
||||
lineHeight: 2,
|
||||
marginBottom: theme.spacing(5),
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
})
|
||||
);
|
||||
import { Layout, Page } from "features/Layout";
|
||||
|
||||
function Index(): JSX.Element {
|
||||
const classes = useStyles();
|
||||
const highlightSx = { color: "primary.main" };
|
||||
|
||||
return (
|
||||
<Layout page={Page.Home} title="">
|
||||
<Box p={4}>
|
||||
<Box mb={4} width="60%">
|
||||
<Typography variant="h2">
|
||||
<span className={classes.titleHighlight}>Hetty://</span>
|
||||
<Box component="span" sx={highlightSx}>
|
||||
Hetty://
|
||||
</Box>
|
||||
<br />
|
||||
The simple HTTP toolkit for security research.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography className={classes.subtitle} paragraph>
|
||||
What if security testing was intuitive, powerful, and good looking?
|
||||
What if it was <strong>free</strong>, instead of $400 per year?{" "}
|
||||
<span className={classes.titleHighlight}>Hetty</span> is listening on{" "}
|
||||
<code>:8080</code>…
|
||||
|
||||
<Typography
|
||||
paragraph
|
||||
sx={{
|
||||
fontSize: "1.6rem",
|
||||
width: "60%",
|
||||
lineHeight: 2,
|
||||
mb: 5,
|
||||
}}
|
||||
>
|
||||
Welcome to{" "}
|
||||
<Box component="span" sx={highlightSx}>
|
||||
Hetty
|
||||
</Box>
|
||||
. Get started by creating a project.
|
||||
</Typography>
|
||||
<Box>
|
||||
<Link href="/proxy" passHref>
|
||||
<Button
|
||||
className={classes.button}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
component="a"
|
||||
size="large"
|
||||
startIcon={<SettingsEthernetIcon />}
|
||||
>
|
||||
Setup proxy
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/proxy" passHref>
|
||||
<Button
|
||||
className={classes.button}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component="a"
|
||||
size="large"
|
||||
startIcon={<SendIcon />}
|
||||
>
|
||||
Send HTTP requests
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Link href="/projects" passHref>
|
||||
<Button
|
||||
sx={{ mr: 2 }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component="a"
|
||||
size="large"
|
||||
startIcon={<FolderIcon />}
|
||||
>
|
||||
Manage projects
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
</Layout>
|
||||
);
|
||||
|
33
admin/src/pages/projects/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Box, Divider, Grid, Typography } from "@mui/material";
|
||||
|
||||
import { Layout, Page } from "features/Layout";
|
||||
import NewProject from "features/projects/components/NewProject";
|
||||
import ProjectList from "features/projects/components/ProjectList";
|
||||
|
||||
function Index(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.Projects} title="Projects">
|
||||
<Box p={4}>
|
||||
<Box mb={3}>
|
||||
<Typography variant="h4">Projects</Typography>
|
||||
</Box>
|
||||
<Typography paragraph>
|
||||
Projects contain settings and data generated/processed by Hetty. They are stored in a single database on disk.
|
||||
</Typography>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<Box mb={8}>
|
||||
<NewProject />
|
||||
</Box>
|
||||
<Grid container>
|
||||
<Grid item xs={12} sm={8} md={6} lg={6}>
|
||||
<ProjectList />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
@ -1,22 +1,16 @@
|
||||
import React from "react";
|
||||
import { Button, Typography } from "@material-ui/core";
|
||||
import ListIcon from "@material-ui/icons/List";
|
||||
import ListIcon from "@mui/icons-material/List";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import Layout, { Page } from "../../components/Layout";
|
||||
import { Layout, Page } from "features/Layout";
|
||||
|
||||
function Index(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.ProxySetup} title="Proxy setup">
|
||||
<Typography paragraph>Coming soon…</Typography>
|
||||
<Link href="/proxy/logs" passHref>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
component="a"
|
||||
size="large"
|
||||
startIcon={<ListIcon />}
|
||||
>
|
||||
<Button variant="contained" color="primary" component="a" size="large" startIcon={<ListIcon />}>
|
||||
View logs
|
||||
</Button>
|
||||
</Link>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import LogsOverview from "../../../components/reqlog/LogsOverview";
|
||||
import Layout, { Page } from "../../../components/Layout";
|
||||
import { Layout, Page } from "features/Layout";
|
||||
import RequestLogs from "features/reqlog";
|
||||
|
||||
function ProxyLogs(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.ProxyLogs} title="Proxy logs">
|
||||
<LogsOverview />
|
||||
<RequestLogs />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
37
admin/src/pages/scope/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Box, Divider, Grid, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
import { Layout, Page } from "features/Layout";
|
||||
import AddRule from "features/scope/components/AddRule";
|
||||
import Rules from "features/scope/components/Rules";
|
||||
|
||||
function Index(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.Scope} title="Scope">
|
||||
<Box p={4}>
|
||||
<Box mb={3}>
|
||||
<Typography variant="h4">Scope</Typography>
|
||||
</Box>
|
||||
<Typography paragraph>
|
||||
Scope rules are used by various modules in Hetty and can influence their behavior. For example: the Proxy logs
|
||||
module can match incoming requests against scope rules and decide its behavior (e.g. log or bypass) based on
|
||||
the outcome of the match. All scope configuration is stored per project.
|
||||
</Typography>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<Grid container>
|
||||
<Grid item xs={12} sm={12} md={8} lg={6}>
|
||||
<AddRule />
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<Rules />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
@ -1,11 +1,10 @@
|
||||
import { Box, Typography } from "@material-ui/core";
|
||||
|
||||
import Layout, { Page } from "../../components/Layout";
|
||||
import { Layout, Page } from "features/Layout";
|
||||
import Sender from "features/sender";
|
||||
|
||||
function Index(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.Sender} title="Sender">
|
||||
<Typography paragraph>Coming soon…</Typography>
|
||||
<Sender />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|