Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI #1170

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Add UI #1170

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions http/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ package http
import (
"context"
"crypto/tls"
"crypto/x509"
"embed"
"encoding/json"
"errors"
"expvar"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"mime"
"net"
"net/http"
"net/http/pprof"
Expand Down Expand Up @@ -426,6 +431,8 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.handleExpvar(w, r)
case strings.HasPrefix(r.URL.Path, "/debug/pprof") && s.Pprof:
s.handlePprof(w, r)
case strings.HasPrefix(r.URL.Path, "/ui"):
s.handleUI(w, r)
default:
w.WriteHeader(http.StatusNotFound)
}
Expand Down Expand Up @@ -1524,6 +1531,47 @@ func (s *Service) handlePprof(w http.ResponseWriter, r *http.Request) {
}
}

// Pattern should start with "all:" prefix in order to
// include files and directories beginning with "." or "_".
//go:embed all:ui/out/*
var staticAssets embed.FS
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ui/out directory is embedded as virtual file system at build time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's cool, definitely looking forward to learning more about that.


// handleUI serves UI assets(html, javascript, css) over HTTP.
func (s *Service) handleUI(w http.ResponseWriter, r *http.Request) {
// If the request path is `/ui/menu/query-runner`, one of the two files below will be served.
// 1. `ui/out/query-runner` (normarlizedFilePath)
// 2. `ui/out/query-runner/index.html` (indexFilePath)
normalizedFilePath := strings.Replace(r.URL.Path, "/ui", "ui/out", 1)
normalizedFilePath = strings.TrimRight(normalizedFilePath, "/")
indexFilePath := normalizedFilePath + "/index.html"

targetPaths := []string{normalizedFilePath, indexFilePath}
for _, path := range targetPaths {
fileInfo, err := fs.Stat(staticAssets, path)
if err != nil || fileInfo.IsDir() {
continue
}

file, err := staticAssets.ReadFile(path)
if err != nil {
continue
}

// Mime type has to be set manually.
splittedPath := strings.Split(path, ".")
mimeType := mime.TypeByExtension("." + splittedPath[len(splittedPath)-1])
if mimeType != "" {
w.Header().Add("Content-Type", mimeType)
}

w.Write(file)
return
}

// If no file exists, redirect to the main menu.
http.Redirect(w, r, "/ui/menu/query-runner/", http.StatusMovedPermanently)
}

// Addr returns the address on which the Service is listening
func (s *Service) Addr() net.Addr {
return s.ln.Addr()
Expand Down
26 changes: 26 additions & 0 deletions http/ui/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"overrides": [
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/ban-ts-comment": "warn"
}
}
3 changes: 3 additions & 0 deletions http/ui/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
35 changes: 35 additions & 0 deletions http/ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
20 changes: 20 additions & 0 deletions http/ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# rqlite UI
Rqlite UI provides simple web UI for rqlite. You can execute SQL queries and check status of rqlite cluster via rqman.


## Development
### Prerequisite
Latest [Node.js](https://nodejs.org/ko/download) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable) are needed.

### Running UI in local environment
After executing below command, UI will be running on `localhost:3000`
```bash
yarn dev
```


## Packaging into rqlited
```bash
yarn build && yarn next export
```
The build output can be found in the `out` directory. When you build rqlited, that directory will be included in the binary.
51 changes: 51 additions & 0 deletions http/ui/components/common/AccountFormModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Dialog, DialogTitle, Button, Stack, TextField } from "@mui/material";
import { isEmpty } from "lodash";
import { useEffect, useState } from "react";

export interface IAccountFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (account: { username: string; password: string }) => void;
}

const AccountFormModal = (props: IAccountFormModalProps) => {
const { onClose, open, onSubmit } = props;
const [form, setForm] = useState({ username: "", password: "" });
useEffect(() => {
setForm({ username: "", password: "" });
}, [open]);

return (
<Dialog onClose={onClose} open={open}>
<Stack sx={{ padding: "10px" }}>
<DialogTitle >Enter a <a href="https://rqlite.io/docs/guides/security/#basic-auth" target="_blank" rel="noreferrer">rqlite account</a></DialogTitle>
<TextField
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
label="username"
style={{ marginBottom: "10px" }}
/>
<TextField
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
label="password"
/>
<Button
sx={{ marginTop: "20px" }}
onClick={() => {
if (isEmpty(form.username) || isEmpty(form.password)) {
return;
}

onSubmit(form);
onClose();
}}
>
SUBMIT
</Button>
</Stack>
</Dialog>
);
};

export default AccountFormModal;
Loading