mirror of
https://github.com/cuigh/swirl
synced 2025-06-26 18:16:50 +00:00
Refactor UI with vue3
This commit is contained in:
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,3 +0,0 @@
|
||||
*.js linguist-language=Go
|
||||
*.css linguist-language=Go
|
||||
*.jet linguist-language=Go
|
||||
38
.gitignore
vendored
38
.gitignore
vendored
@@ -1,37 +1,23 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Go template
|
||||
# folders
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
.vscode
|
||||
.idea
|
||||
vendor
|
||||
|
||||
# debug files
|
||||
/debug
|
||||
/swirl
|
||||
/swirl
|
||||
|
||||
# node
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
12
CHANGELOG.md
Normal file
12
CHANGELOG.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.0.0 (2021-12-15)
|
||||
|
||||
> As this version contains some incompatible modifications, it is recommended to redeploy instead of upgrading directly.
|
||||
|
||||
* feat: Refactor UI with vue3.
|
||||
* feat: Switch to official MongoDB driver.
|
||||
* feat: Allow set chart margins.
|
||||
* fix: Some args are incorrect when generating service command line.
|
||||
* break: Optimize role permissions.
|
||||
* break: Adjust system settings.
|
||||
29
Dockerfile
29
Dockerfile
@@ -1,17 +1,24 @@
|
||||
FROM golang:alpine AS build
|
||||
RUN apk add git
|
||||
WORKDIR /go/src/swirl/
|
||||
ADD . .
|
||||
ENV GO111MODULE on
|
||||
# ---- Build UI----
|
||||
FROM node:alpine AS node
|
||||
WORKDIR /app
|
||||
COPY ui .
|
||||
RUN yarn install
|
||||
RUN yarn run build
|
||||
|
||||
# ---- Build Go----
|
||||
FROM golang:1.17-alpine AS golang
|
||||
WORKDIR /app
|
||||
COPY --from=node /app/dist ui/dist
|
||||
COPY . .
|
||||
RUN apk update && apk add git
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w"
|
||||
|
||||
FROM alpine:3.8
|
||||
# ---- Release ----
|
||||
FROM alpine
|
||||
LABEL maintainer="cuigh <noname@live.com>"
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache ca-certificates
|
||||
COPY --from=build /go/src/swirl/swirl .
|
||||
COPY --from=build /go/src/swirl/config ./config/
|
||||
COPY --from=build /go/src/swirl/assets ./assets/
|
||||
COPY --from=build /go/src/swirl/views ./views/
|
||||
COPY --from=golang /app/swirl .
|
||||
COPY --from=golang /app/config config/
|
||||
EXPOSE 8001
|
||||
ENTRYPOINT ["/app/swirl"]
|
||||
ENTRYPOINT ["/app/swirl"]
|
||||
52
README.md
52
README.md
@@ -5,6 +5,8 @@
|
||||
|
||||
**Swirl** is a web management tool for Docker, focused on swarm cluster.
|
||||
|
||||
> Warning: v1.0+ is not fully compatible with previous versions, it is recommended to redeploy instead of upgrading directly.
|
||||
|
||||
## Features
|
||||
|
||||
* Swarm components management
|
||||
@@ -26,7 +28,7 @@
|
||||
|
||||
### Service list
|
||||
|
||||

|
||||

|
||||
|
||||
### Service stats
|
||||
|
||||
@@ -34,11 +36,11 @@
|
||||
|
||||
### Stack list
|
||||
|
||||

|
||||

|
||||
|
||||
### Settings
|
||||
|
||||

|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -51,12 +53,16 @@ name: swirl
|
||||
banner: false
|
||||
|
||||
web:
|
||||
address: ':8001'
|
||||
entries:
|
||||
- address: :8002
|
||||
authorize: '?'
|
||||
|
||||
swirl:
|
||||
db_type: mongo
|
||||
db_address: localhost:27017/swirl
|
||||
db_address: mongodb://localhost:27017/swirl
|
||||
# token_key: 80fe9a6d5c6d5dd39f27cd37a77faf8a
|
||||
# token_expiry: 30m
|
||||
# docker_api_version: '1.41'
|
||||
# docker_endpoint: tcp://docker-proxy:2375
|
||||
|
||||
log:
|
||||
@@ -73,12 +79,14 @@ log:
|
||||
|
||||
Only these options can be set by environment variables for now.
|
||||
|
||||
| Name | Value |
|
||||
| --------------- | -----------------------------|
|
||||
| DB_TYPE | mongo,bolt |
|
||||
| DB_ADDRESS | localhost:27017/swirl |
|
||||
| DOCKER_ENDPOINT | tcp://docker-proxy:2375 |
|
||||
| AUTH_TIMEOUT | 4h |
|
||||
| Name | Value |
|
||||
|--------------------|----------------------------------|
|
||||
| DB_TYPE | mongo(default),bolt |
|
||||
| DB_ADDRESS | mongodb://localhost:27017/swirl |
|
||||
| TOKEN_KEY | 80fe9a6d5c6d5dd39f27cd37a77faf8a |
|
||||
| TOKEN_EXPIRY | 30m |
|
||||
| DOCKER_ENDPOINT | tcp://docker-proxy:2375 |
|
||||
| DOCKER_API_VERSION | 1.41 |
|
||||
|
||||
### With swarm config
|
||||
|
||||
@@ -86,11 +94,11 @@ Docker support mounting configuration file through swarm from v17.06, so you can
|
||||
|
||||
## Deployment
|
||||
|
||||
Swirl support two storage engines now: mongo and bolt. **bolt** is suitable for develepment environment, **Swirl** can only deploy one replica if you use **bolt** storage engine.
|
||||
Swirl support two storage engines now: mongo and bolt. **bolt** is suitable for development environment, **Swirl** can only deploy one replica if you use **bolt** storage engine.
|
||||
|
||||
### Stand alone
|
||||
### Standalone
|
||||
|
||||
Just copy the swirl binary and config/assets/views directories to the host, and run it.
|
||||
Just copy the swirl binary and config directory to the host, and run it.
|
||||
|
||||
```bash
|
||||
./swirl
|
||||
@@ -116,7 +124,7 @@ docker run -d -p 8001:8001 \
|
||||
docker run -d -p 8001:8001 \
|
||||
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
-e DB_TYPE=mongo \
|
||||
-e DB_ADDRESS=localhost:27017/swirl \
|
||||
-e DB_ADDRESS=mongodb://localhost:27017/swirl \
|
||||
--name=swirl \
|
||||
cuigh/swirl
|
||||
```
|
||||
@@ -143,7 +151,7 @@ docker service create \
|
||||
docker service create \
|
||||
--name=swirl \
|
||||
--publish=8001:8001/tcp \
|
||||
--env DB_ADDRESS=localhost:27017/swirl \
|
||||
--env DB_ADDRESS=mongodb://localhost:27017/swirl \
|
||||
--constraint=node.role==manager \
|
||||
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
cuigh/swirl
|
||||
@@ -159,15 +167,19 @@ docker stack deploy -c compose.yml swirl
|
||||
|
||||
**Swirl** use service labels to support some features, the labels in the table below are currently supported.
|
||||
|
||||
Name | Description | Examples
|
||||
--- | --- | ---
|
||||
swirl.scale | Service auto scaling | `swirl.scale=min=1,max=5,cpu=30:50`
|
||||
| Name | Description | Examples |
|
||||
|-------------|----------------------|-------------------------------------|
|
||||
| swirl.scale | Service auto scaling | `swirl.scale=min=1,max=5,cpu=30:50` |
|
||||
|
||||
## Build
|
||||
|
||||
To build **Swirl** from source, you need `go1.11` installed.
|
||||
To build **Swirl** from source, you need `yarn` and `go(v1.16+)` installed.
|
||||
|
||||
```sh
|
||||
$ cd ui
|
||||
$ yarn
|
||||
$ yarn build
|
||||
$ cd ..
|
||||
$ go build
|
||||
```
|
||||
|
||||
|
||||
38
api/api.go
Normal file
38
api/api.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/app/container"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
)
|
||||
|
||||
func ajax(ctx web.Context, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, nil)
|
||||
}
|
||||
|
||||
func success(ctx web.Context, data interface{}) error {
|
||||
return ctx.Result(0, "", data)
|
||||
}
|
||||
|
||||
func init() {
|
||||
container.Put(NewSystem, container.Name("api.system"))
|
||||
container.Put(NewSetting, container.Name("api.setting"))
|
||||
container.Put(NewUser, container.Name("api.user"))
|
||||
container.Put(NewNode, container.Name("api.node"))
|
||||
container.Put(NewRegistry, container.Name("api.registry"))
|
||||
container.Put(NewNetwork, container.Name("api.network"))
|
||||
container.Put(NewService, container.Name("api.service"))
|
||||
container.Put(NewTask, container.Name("api.task"))
|
||||
container.Put(NewConfig, container.Name("api.config"))
|
||||
container.Put(NewSecret, container.Name("api.secret"))
|
||||
container.Put(NewStack, container.Name("api.stack"))
|
||||
container.Put(NewImage, container.Name("api.image"))
|
||||
container.Put(NewContainer, container.Name("api.container"))
|
||||
container.Put(NewVolume, container.Name("api.volume"))
|
||||
container.Put(NewUser, container.Name("api.user"))
|
||||
container.Put(NewRole, container.Name("api.role"))
|
||||
container.Put(NewEvent, container.Name("api.event"))
|
||||
container.Put(NewChart, container.Name("api.chart"))
|
||||
}
|
||||
159
api/chart.go
Normal file
159
api/chart.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/ext/times"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
// ChartHandler encapsulates chart related handlers.
|
||||
type ChartHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"chart.view" desc:"search charts"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"chart.view" desc:"find chart by id"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"chart.edit" desc:"create or update chart"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"chart.delete" desc:"delete chart"`
|
||||
FetchData web.HandlerFunc `path:"/fetch-data" auth:"?" desc:"fetch chart data"`
|
||||
FindDashboard web.HandlerFunc `path:"/find-dashboard" auth:"?" desc:"find dashboard by name and key"`
|
||||
SaveDashboard web.HandlerFunc `path:"/save-dashboard" method:"post" auth:"chart.dashboard" desc:"save dashboard"`
|
||||
}
|
||||
|
||||
// NewChart creates an instance of ChartHandler
|
||||
func NewChart(b biz.ChartBiz) *ChartHandler {
|
||||
return &ChartHandler{
|
||||
Search: chartSearch(b),
|
||||
Find: chartFind(b),
|
||||
Delete: chartDelete(b),
|
||||
Save: chartSave(b),
|
||||
FetchData: chartFetchData(b),
|
||||
FindDashboard: chartFindDashboard(b),
|
||||
SaveDashboard: chartSaveDashboard(b),
|
||||
}
|
||||
}
|
||||
|
||||
func chartSearch(b biz.ChartBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name" bind:"name"`
|
||||
Dashboard string `json:"dashboard" bind:"dashboard"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
charts []*biz.Chart
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
charts, total, err = b.Search(args.Name, args.Dashboard, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": charts,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func chartFind(b biz.ChartBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
chart, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, chart)
|
||||
}
|
||||
}
|
||||
|
||||
func chartDelete(b biz.ChartBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.ID, args.Title, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func chartSave(b biz.ChartBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
r := &model.Chart{}
|
||||
err := ctx.Bind(r, true)
|
||||
if err == nil {
|
||||
if r.ID == "" {
|
||||
err = b.Create(r, ctx.User())
|
||||
} else {
|
||||
err = b.Update(r, ctx.User())
|
||||
}
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func chartFetchData(b biz.ChartBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Key string `json:"key" bind:"key"`
|
||||
Charts string `json:"charts" bind:"charts"`
|
||||
Period int32 `json:"period" bind:"period"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
d data.Map
|
||||
)
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
d, err = b.FetchData(args.Key, strings.Split(args.Charts, ","), times.Minutes(args.Period))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, d)
|
||||
}
|
||||
}
|
||||
|
||||
func chartFindDashboard(b biz.ChartBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
d *biz.Dashboard
|
||||
name = ctx.Query("name")
|
||||
key = ctx.Query("key")
|
||||
)
|
||||
d, err = b.FindDashboard(name, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, d)
|
||||
}
|
||||
}
|
||||
|
||||
func chartSaveDashboard(b biz.ChartBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
dashboard := &model.Dashboard{}
|
||||
err := ctx.Bind(dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch dashboard.Name {
|
||||
case "home", "service":
|
||||
err = b.UpdateDashboard(dashboard, ctx.User())
|
||||
default:
|
||||
err = errors.New("unknown dashboard: " + dashboard.Name)
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
94
api/config.go
Normal file
94
api/config.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// ConfigHandler encapsulates config related handlers.
|
||||
type ConfigHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"config.view" desc:"search configs"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"config.view" desc:"find config by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"config.delete" desc:"delete config"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"config.edit" desc:"create or update config"`
|
||||
}
|
||||
|
||||
// NewConfig creates an instance of ConfigHandler
|
||||
func NewConfig(b biz.ConfigBiz) *ConfigHandler {
|
||||
return &ConfigHandler{
|
||||
Search: configSearch(b),
|
||||
Find: configFind(b),
|
||||
Delete: configDelete(b),
|
||||
Save: configSave(b),
|
||||
}
|
||||
}
|
||||
|
||||
func configSearch(b biz.ConfigBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name" bind:"name"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
configs []*biz.Config
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
configs, total, err = b.Search(args.Name, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": configs,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func configFind(b biz.ConfigBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
config, raw, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"config": config, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func configDelete(b biz.ConfigBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.ID, args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func configSave(b biz.ConfigBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
c := &biz.Config{}
|
||||
err := ctx.Bind(c, true)
|
||||
if err == nil {
|
||||
if c.ID == "" {
|
||||
err = b.Create(c, ctx.User())
|
||||
} else {
|
||||
err = b.Update(c, ctx.User())
|
||||
}
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
211
api/container.go
Normal file
211
api/container.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
)
|
||||
|
||||
// ContainerHandler encapsulates container related handlers.
|
||||
type ContainerHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"container.view" desc:"search containers"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"container.view" desc:"find container by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"container.delete" desc:"delete container"`
|
||||
FetchLogs web.HandlerFunc `path:"/fetch-logs" auth:"container.logs" desc:"fetch logs of container"`
|
||||
Connect web.HandlerFunc `path:"/connect" auth:"*" desc:"connect to a running container"`
|
||||
}
|
||||
|
||||
// NewContainer creates an instance of ContainerHandler
|
||||
func NewContainer(b biz.ContainerBiz) *ContainerHandler {
|
||||
return &ContainerHandler{
|
||||
Search: containerSearch(b),
|
||||
Find: containerFind(b),
|
||||
Delete: containerDelete(b),
|
||||
FetchLogs: containerFetchLogs(b),
|
||||
Connect: containerConnect(b),
|
||||
}
|
||||
}
|
||||
|
||||
func containerSearch(b biz.ContainerBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name" bind:"name"`
|
||||
Status string `json:"status" bind:"status"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
containers []*biz.Container
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
containers, total, err = b.Search(args.Name, args.Status, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": containers,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func containerFind(b biz.ContainerBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
container, raw, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"container": container, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func containerDelete(b biz.ContainerBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.ID, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func containerFetchLogs(b biz.ContainerBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id" bind:"id"`
|
||||
Lines int `json:"lines" bind:"lines"`
|
||||
Timestamps bool `json:"timestamps" bind:"timestamps"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
stdout, stderr string
|
||||
)
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
stdout, stderr, err = b.FetchLogs(args.ID, args.Lines, args.Timestamps)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"stdout": stdout, "stderr": stderr})
|
||||
}
|
||||
}
|
||||
|
||||
func containerConnect(b biz.ContainerBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
var (
|
||||
id = ctx.Query("id")
|
||||
cmd = ctx.Query("cmd")
|
||||
)
|
||||
|
||||
_, _, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, _, _, err := ws.UpgradeHTTP(ctx.Request(), ctx.Response())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idResp, err := b.ExecCreate(id, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := b.ExecAttach(idResp.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.ExecStart(idResp.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
closed = false
|
||||
logger = log.Get("container")
|
||||
disposer = func() {
|
||||
if !closed {
|
||||
closed = true
|
||||
_ = conn.Close()
|
||||
resp.Close()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// input
|
||||
go func() {
|
||||
defer disposer()
|
||||
|
||||
var (
|
||||
msg []byte
|
||||
op ws.OpCode
|
||||
)
|
||||
|
||||
for {
|
||||
msg, op, err = wsutil.ReadClientData(conn)
|
||||
if err != nil {
|
||||
if !closed {
|
||||
logger.Error("failed to read data from client: ", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if op == ws.OpClose {
|
||||
break
|
||||
}
|
||||
|
||||
_, err = resp.Conn.Write(msg)
|
||||
if err != nil {
|
||||
logger.Error("failed to write data to container: ", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// output
|
||||
go func() {
|
||||
defer disposer()
|
||||
|
||||
var (
|
||||
n int
|
||||
buf = make([]byte, 1024)
|
||||
)
|
||||
|
||||
for {
|
||||
n, err = resp.Reader.Read(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
logger.Error("failed to read data from container: ", err)
|
||||
break
|
||||
}
|
||||
|
||||
err = wsutil.WriteServerMessage(conn, ws.OpText, buf[:n])
|
||||
if err != nil {
|
||||
logger.Error("failed to write data to client: ", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
62
api/event.go
Normal file
62
api/event.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
// EventHandler encapsulates event related handlers.
|
||||
type EventHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"event.view" desc:"search events"`
|
||||
Prune web.HandlerFunc `path:"/prune" method:"post" auth:"event.delete" desc:"delete events"`
|
||||
}
|
||||
|
||||
// NewEvent creates an instance of EventHandler
|
||||
func NewEvent(b biz.EventBiz) *EventHandler {
|
||||
return &EventHandler{
|
||||
Search: eventSearch(b),
|
||||
Prune: eventPrune(b),
|
||||
}
|
||||
}
|
||||
|
||||
func eventSearch(b biz.EventBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &model.EventListArgs{}
|
||||
events []*biz.Event
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
events, total, err = b.Search(args)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": events,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func eventPrune(b biz.EventBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Date string `json:"date"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var args = &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
// TODO
|
||||
//err = b.Prune(args.Date)
|
||||
err = errors.NotImplemented
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
76
api/image.go
Normal file
76
api/image.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// ImageHandler encapsulates image related handlers.
|
||||
type ImageHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"image.view" desc:"search images"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"image.view" desc:"find image by id"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"image.delete" desc:"delete image"`
|
||||
}
|
||||
|
||||
// NewImage creates an instance of ImageHandler
|
||||
func NewImage(b biz.ImageBiz) *ImageHandler {
|
||||
return &ImageHandler{
|
||||
Search: imageSearch(b),
|
||||
Find: imageFind(b),
|
||||
Delete: imageDelete(b),
|
||||
}
|
||||
}
|
||||
|
||||
func imageSearch(b biz.ImageBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name" bind:"name"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
images []*biz.Image
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
images, total, err = b.Search(args.Name, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": images,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func imageFind(b biz.ImageBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
image, raw, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"image": image, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func imageDelete(b biz.ImageBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.ID, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
89
api/network.go
Normal file
89
api/network.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// NetworkHandler encapsulates network related handlers.
|
||||
type NetworkHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"network.view" desc:"search networks"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"network.view" desc:"find network by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"network.delete" desc:"delete network"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"network.edit" desc:"create or update network"`
|
||||
Disconnect web.HandlerFunc `path:"/disconnect" method:"post" auth:"network.disconnect" desc:"disconnect container from network"`
|
||||
}
|
||||
|
||||
// NewNetwork creates an instance of NetworkHandler
|
||||
func NewNetwork(nb biz.NetworkBiz) *NetworkHandler {
|
||||
return &NetworkHandler{
|
||||
Search: networkSearch(nb),
|
||||
Find: networkFind(nb),
|
||||
Delete: networkDelete(nb),
|
||||
Save: networkSave(nb),
|
||||
Disconnect: networkDisconnect(nb),
|
||||
}
|
||||
}
|
||||
|
||||
func networkSearch(nb biz.NetworkBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
networks, err := nb.Search()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, networks)
|
||||
}
|
||||
}
|
||||
|
||||
func networkFind(nb biz.NetworkBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
name := ctx.Query("name")
|
||||
network, raw, err := nb.Find(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"network": network, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func networkDelete(nb biz.NetworkBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = nb.Delete(args.ID, args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func networkSave(nb biz.NetworkBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
n := &biz.Network{}
|
||||
err := ctx.Bind(n, true)
|
||||
if err == nil {
|
||||
err = nb.Create(n, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func networkDisconnect(nb biz.NetworkBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
NetworkID string `json:"networkId"`
|
||||
NetworkName string `json:"networkName"`
|
||||
Container string `json:"container"`
|
||||
}
|
||||
return func(ctx web.Context) error {
|
||||
args := &Args{}
|
||||
err := ctx.Bind(args, true)
|
||||
if err == nil {
|
||||
err = nb.Disconnect(args.NetworkID, args.NetworkName, args.Container, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
72
api/node.go
Normal file
72
api/node.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// NodeHandler encapsulates node related handlers.
|
||||
type NodeHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"node.view" desc:"search nodes"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"node.view" desc:"find node by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"node.delete" desc:"delete node"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"node.edit" desc:"create or update node"`
|
||||
}
|
||||
|
||||
// NewNode creates an instance of NodeHandler
|
||||
func NewNode(nb biz.NodeBiz) *NodeHandler {
|
||||
return &NodeHandler{
|
||||
Search: nodeSearch(nb),
|
||||
Find: nodeFind(nb),
|
||||
Delete: nodeDelete(nb),
|
||||
Save: nodeSave(nb),
|
||||
}
|
||||
}
|
||||
|
||||
func nodeSearch(nb biz.NodeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
nodes, err := nb.Search()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return success(ctx, nodes)
|
||||
}
|
||||
}
|
||||
|
||||
func nodeFind(nb biz.NodeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
node, raw, err := nb.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"node": node, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func nodeDelete(nb biz.NodeBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = nb.Delete(args.ID, args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func nodeSave(nb biz.NodeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
n := &biz.Node{}
|
||||
err := ctx.Bind(n, true)
|
||||
if err == nil {
|
||||
err = nb.Update(n, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
74
api/registry.go
Normal file
74
api/registry.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// RegistryHandler encapsulates registry related handlers.
|
||||
type RegistryHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"registry.view" desc:"search registries"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"registry.view" desc:"find registry by id"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"registry.delete" desc:"delete registry"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"registry.edit" desc:"create or update registry"`
|
||||
}
|
||||
|
||||
// NewRegistry creates an instance of RegistryHandler
|
||||
func NewRegistry(b biz.RegistryBiz) *RegistryHandler {
|
||||
return &RegistryHandler{
|
||||
Search: registrySearch(b),
|
||||
Find: registryFind(b),
|
||||
Delete: registryDelete(b),
|
||||
Save: registrySave(b),
|
||||
}
|
||||
}
|
||||
|
||||
func registrySearch(b biz.RegistryBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
registries, err := b.Search()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, registries)
|
||||
}
|
||||
}
|
||||
|
||||
func registryFind(b biz.RegistryBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
node, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, node)
|
||||
}
|
||||
}
|
||||
|
||||
func registryDelete(b biz.RegistryBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.ID, args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func registrySave(b biz.RegistryBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
r := &biz.Registry{}
|
||||
err := ctx.Bind(r, true)
|
||||
if err == nil {
|
||||
if r.ID == "" {
|
||||
err = b.Create(r, ctx.User())
|
||||
} else {
|
||||
err = b.Update(r, ctx.User())
|
||||
}
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
75
api/role.go
Normal file
75
api/role.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// RoleHandler encapsulates role related handlers.
|
||||
type RoleHandler struct {
|
||||
Find web.HandlerFunc `path:"/find" auth:"role.view" desc:"find role by id"`
|
||||
Search web.HandlerFunc `path:"/search" auth:"role.view" desc:"search roles"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"role.edit" desc:"create or update role"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"role.delete" desc:"delete role"`
|
||||
}
|
||||
|
||||
// NewRole creates an instance of RoleHandler
|
||||
func NewRole(b biz.RoleBiz) *RoleHandler {
|
||||
return &RoleHandler{
|
||||
Search: roleSearch(b),
|
||||
Find: roleFind(b),
|
||||
Delete: roleDelete(b),
|
||||
Save: roleSave(b),
|
||||
}
|
||||
}
|
||||
|
||||
func roleSearch(b biz.RoleBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
name := ctx.Query("name")
|
||||
roles, err := b.Search(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, roles)
|
||||
}
|
||||
}
|
||||
|
||||
func roleFind(b biz.RoleBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
role, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, role)
|
||||
}
|
||||
}
|
||||
|
||||
func roleDelete(b biz.RoleBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.ID, args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func roleSave(b biz.RoleBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
r := &biz.Role{}
|
||||
err := ctx.Bind(r, true)
|
||||
if err == nil {
|
||||
if r.ID == "" {
|
||||
err = b.Create(r, ctx.User())
|
||||
} else {
|
||||
err = b.Update(r, ctx.User())
|
||||
}
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
94
api/secret.go
Normal file
94
api/secret.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// SecretHandler encapsulates secret related handlers.
|
||||
type SecretHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"secret.view" desc:"search secrets"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"secret.view" desc:"find secret by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"secret.delete" desc:"delete secret"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"secret.edit" desc:"create or update secret"`
|
||||
}
|
||||
|
||||
// NewSecret creates an instance of SecretHandler
|
||||
func NewSecret(b biz.SecretBiz) *SecretHandler {
|
||||
return &SecretHandler{
|
||||
Search: secretSearch(b),
|
||||
Find: secretFind(b),
|
||||
Delete: secretDelete(b),
|
||||
Save: secretSave(b),
|
||||
}
|
||||
}
|
||||
|
||||
func secretSearch(b biz.SecretBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name" bind:"name"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
secrets []*biz.Secret
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
secrets, total, err = b.Search(args.Name, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": secrets,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func secretFind(b biz.SecretBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
secret, raw, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"secret": secret, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func secretDelete(b biz.SecretBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.ID, args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func secretSave(b biz.SecretBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
c := &biz.Secret{}
|
||||
err := ctx.Bind(c, true)
|
||||
if err == nil {
|
||||
if c.ID == "" {
|
||||
err = b.Create(c, ctx.User())
|
||||
} else {
|
||||
err = b.Update(c, ctx.User())
|
||||
}
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
190
api/service.go
Normal file
190
api/service.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// ServiceHandler encapsulates service related handlers.
|
||||
type ServiceHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"service.view" desc:"search services"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"service.view" desc:"find service by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"service.delete" desc:"delete service"`
|
||||
Restart web.HandlerFunc `path:"/restart" method:"post" auth:"service.restart" desc:"restart service"`
|
||||
Rollback web.HandlerFunc `path:"/rollback" method:"post" auth:"service.rollback" desc:"rollback service"`
|
||||
Scale web.HandlerFunc `path:"/scale" method:"post" auth:"service.edit" desc:"scale service"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"service.edit" desc:"create or update service"`
|
||||
Deploy web.HandlerFunc `path:"/deploy" method:"post" auth:"service.deploy" desc:"deploy service"`
|
||||
FetchLogs web.HandlerFunc `path:"/fetch-logs" auth:"service.logs" desc:"fetch logs of service"`
|
||||
}
|
||||
|
||||
// NewService creates an instance of ServiceHandler
|
||||
func NewService(b biz.ServiceBiz) *ServiceHandler {
|
||||
return &ServiceHandler{
|
||||
Search: serviceSearch(b),
|
||||
Find: serviceFind(b),
|
||||
Delete: serviceDelete(b),
|
||||
Restart: serviceRestart(b),
|
||||
Rollback: serviceRollback(b),
|
||||
Scale: serviceScale(b),
|
||||
Save: serviceSave(b),
|
||||
Deploy: serviceDeploy(b),
|
||||
FetchLogs: serviceFetchLogs(b),
|
||||
}
|
||||
}
|
||||
|
||||
func serviceSearch(b biz.ServiceBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name" bind:"name"`
|
||||
Mode string `json:"mode" bind:"mode"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
services []*biz.ServiceBase
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
services, total, err = b.Search(args.Name, args.Mode, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": services,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serviceFind(b biz.ServiceBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
name := ctx.Query("name")
|
||||
status := ctx.Query("status") == "true"
|
||||
service, raw, err := b.Find(name, status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"service": service, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func serviceDelete(b biz.ServiceBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func serviceRestart(b biz.ServiceBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Restart(args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func serviceRollback(b biz.ServiceBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Rollback(args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func serviceScale(b biz.ServiceBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name"`
|
||||
Count uint64 `json:"count"`
|
||||
Version uint64 `json:"version"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Scale(args.Name, args.Count, args.Version, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func serviceSave(b biz.ServiceBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
s := &biz.Service{}
|
||||
err := ctx.Bind(s, true)
|
||||
if err == nil {
|
||||
if s.ID == "" {
|
||||
err = b.Create(s, ctx.User())
|
||||
} else {
|
||||
err = b.Update(s, ctx.User())
|
||||
}
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func serviceDeploy(b biz.ServiceBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
service := &biz.Service{}
|
||||
err := ctx.Bind(service, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var s *biz.Service
|
||||
if s, _, err = b.Find(service.Name, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
err = b.Create(service, ctx.User())
|
||||
} else {
|
||||
err = b.Update(service, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func serviceFetchLogs(b biz.ServiceBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id" bind:"id"`
|
||||
Lines int `json:"lines" bind:"lines"`
|
||||
Timestamps bool `json:"timestamps" bind:"timestamps"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
stdout, stderr string
|
||||
)
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
stdout, stderr, err = b.FetchLogs(args.ID, args.Lines, args.Timestamps)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"stdout": stdout, "stderr": stderr})
|
||||
}
|
||||
}
|
||||
57
api/setting.go
Normal file
57
api/setting.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// SettingHandler encapsulates setting related handlers.
|
||||
type SettingHandler struct {
|
||||
Load web.HandlerFunc `path:"/load" auth:"setting.view" desc:"load setting"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"setting.edit" desc:"save setting"`
|
||||
}
|
||||
|
||||
// NewSetting creates an instance of SettingHandler
|
||||
func NewSetting(b biz.SettingBiz) *SettingHandler {
|
||||
return &SettingHandler{
|
||||
Load: settingLoad(b),
|
||||
Save: settingSave(b),
|
||||
}
|
||||
}
|
||||
|
||||
func settingLoad(b biz.SettingBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
options, err := b.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, options)
|
||||
}
|
||||
}
|
||||
|
||||
func settingSave(b biz.SettingBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Options json.RawMessage `json:"options"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
err = ctx.Bind(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
options := data.Map{}
|
||||
d := json.NewDecoder(bytes.NewBuffer(args.Options))
|
||||
d.UseNumber()
|
||||
if err = d.Decode(&options); err == nil {
|
||||
err = b.Save(args.ID, options, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
142
api/stack.go
Normal file
142
api/stack.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/docker/compose"
|
||||
)
|
||||
|
||||
// StackHandler encapsulates stack related handlers.
|
||||
type StackHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"stack.view" desc:"search stacks"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"stack.view" desc:"find stack by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"stack.delete" desc:"delete stack"`
|
||||
Shutdown web.HandlerFunc `path:"/shutdown" method:"post" auth:"stack.shutdown" desc:"shutdown stack"`
|
||||
Deploy web.HandlerFunc `path:"/deploy" method:"post" auth:"stack.deploy" desc:"deploy stack"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"stack.edit" desc:"create or update stack"`
|
||||
Upload web.HandlerFunc `path:"/upload" method:"post" auth:"*" desc:"upload compose file"`
|
||||
}
|
||||
|
||||
// NewStack creates an instance of StackHandler
|
||||
func NewStack(b biz.StackBiz) *StackHandler {
|
||||
return &StackHandler{
|
||||
Search: stackSearch(b),
|
||||
Find: stackFind(b),
|
||||
Delete: stackDelete(b),
|
||||
Shutdown: stackShutdown(b),
|
||||
Deploy: stackDeploy(b),
|
||||
Save: stackSave(b),
|
||||
Upload: stackUpload,
|
||||
}
|
||||
}
|
||||
|
||||
func stackUpload(ctx web.Context) (err error) {
|
||||
file, _, err := ctx.File("content")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Data(b)
|
||||
}
|
||||
|
||||
func stackSearch(b biz.StackBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name" bind:"name"`
|
||||
Filter string `json:"filter" bind:"filter"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
stacks []*biz.Stack
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
stacks, err = b.Search(args.Name, args.Filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, stacks)
|
||||
}
|
||||
}
|
||||
|
||||
func stackFind(b biz.StackBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
name := ctx.Query("name")
|
||||
stack, err := b.Find(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, stack)
|
||||
}
|
||||
}
|
||||
|
||||
func stackDelete(b biz.StackBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func stackShutdown(b biz.StackBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Shutdown(args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func stackDeploy(b biz.StackBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Deploy(args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func stackSave(b biz.StackBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
stack := &biz.Stack{}
|
||||
err := ctx.Bind(stack, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate format
|
||||
_, err = compose.Parse(stack.Name, stack.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stack.ID == "" {
|
||||
err = b.Create(stack, ctx.User())
|
||||
} else {
|
||||
err = b.Update(stack, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
85
api/system.go
Normal file
85
api/system.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/cuigh/auxo/app"
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/docker"
|
||||
)
|
||||
|
||||
//var ErrSystemInitialized = errors.New("system was already initialized")
|
||||
|
||||
// SystemHandler encapsulates system related handlers.
|
||||
type SystemHandler struct {
|
||||
CheckState web.HandlerFunc `path:"/check-state" auth:"*" desc:"check system state"`
|
||||
CreateAdmin web.HandlerFunc `path:"/create-admin" method:"post" auth:"*" desc:"initialize administrator account"`
|
||||
Version web.HandlerFunc `path:"/version" auth:"*" desc:"fetch app version"`
|
||||
Summarize web.HandlerFunc `path:"/summarize" auth:"?" desc:"fetch statistics data"`
|
||||
}
|
||||
|
||||
// NewSystem creates an instance of SystemHandler
|
||||
func NewSystem(d *docker.Docker, b biz.SystemBiz) *SystemHandler {
|
||||
return &SystemHandler{
|
||||
CheckState: systemCheckState(b),
|
||||
CreateAdmin: systemCreateAdmin(b),
|
||||
Version: systemVersion,
|
||||
Summarize: systemSummarize(d),
|
||||
}
|
||||
}
|
||||
|
||||
func systemCheckState(b biz.SystemBiz) web.HandlerFunc {
|
||||
return func(c web.Context) (err error) {
|
||||
state, err := b.CheckState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(c, state)
|
||||
}
|
||||
}
|
||||
|
||||
func systemVersion(c web.Context) (err error) {
|
||||
return success(c, data.Map{
|
||||
"version": app.Version,
|
||||
"goVersion": runtime.Version(),
|
||||
})
|
||||
}
|
||||
|
||||
func systemSummarize(d *docker.Docker) web.HandlerFunc {
|
||||
return func(c web.Context) (err error) {
|
||||
summary := struct {
|
||||
NodeCount int `json:"nodeCount"`
|
||||
NetworkCount int `json:"networkCount"`
|
||||
ServiceCount int `json:"serviceCount"`
|
||||
StackCount int `json:"stackCount"`
|
||||
}{}
|
||||
|
||||
if summary.NodeCount, err = d.NodeCount(context.TODO()); err != nil {
|
||||
return
|
||||
}
|
||||
if summary.NetworkCount, err = d.NetworkCount(context.TODO()); err != nil {
|
||||
return
|
||||
}
|
||||
if summary.ServiceCount, err = d.ServiceCount(context.TODO()); err != nil {
|
||||
return
|
||||
}
|
||||
if summary.StackCount, err = d.StackCount(context.TODO()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(c, summary)
|
||||
}
|
||||
}
|
||||
|
||||
func systemCreateAdmin(b biz.SystemBiz) web.HandlerFunc {
|
||||
return func(c web.Context) (err error) {
|
||||
user := &biz.User{}
|
||||
if err = c.Bind(user, true); err == nil {
|
||||
err = b.CreateAdmin(user)
|
||||
}
|
||||
return ajax(c, err)
|
||||
}
|
||||
}
|
||||
86
api/task.go
Normal file
86
api/task.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// TaskHandler encapsulates node related handlers.
|
||||
type TaskHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"task.view" desc:"search tasks"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"task.view" desc:"find task by id"`
|
||||
FetchLogs web.HandlerFunc `path:"/fetch-logs" auth:"task.logs" desc:"fetch logs of task"`
|
||||
}
|
||||
|
||||
// NewTask creates an instance of TaskHandler
|
||||
func NewTask(b biz.TaskBiz) *TaskHandler {
|
||||
return &TaskHandler{
|
||||
Search: taskSearch(b),
|
||||
Find: taskFind(b),
|
||||
FetchLogs: taskFetchLogs(b),
|
||||
}
|
||||
}
|
||||
|
||||
func taskSearch(b biz.TaskBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Service string `json:"service" bind:"service"`
|
||||
State string `json:"state" bind:"state"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
tasks []*biz.Task
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
tasks, total, err = b.Search("", args.Service, args.State, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": tasks,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func taskFind(b biz.TaskBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
task, raw, err := b.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"task": task, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func taskFetchLogs(b biz.TaskBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id" bind:"id"`
|
||||
Lines int `json:"lines" bind:"lines"`
|
||||
Timestamps bool `json:"timestamps" bind:"timestamps"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
stdout, stderr string
|
||||
)
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
stdout, stderr, err = b.FetchLogs(args.ID, args.Lines, args.Timestamps)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"stdout": stdout, "stderr": stderr})
|
||||
}
|
||||
}
|
||||
182
api/user.go
Normal file
182
api/user.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/app/container"
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/security"
|
||||
)
|
||||
|
||||
// UserHandler encapsulates user related handlers.
|
||||
type UserHandler struct {
|
||||
SignIn web.HandlerFunc `path:"/sign-in" method:"post" auth:"*" desc:"user sign in"`
|
||||
Search web.HandlerFunc `path:"/search" auth:"user.view" desc:"search users"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"user.edit" desc:"create or update user"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"user.view" desc:"find user by id"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"user.delete" desc:"delete user"`
|
||||
SetStatus web.HandlerFunc `path:"/set-status" method:"post" auth:"user.edit" desc:"set user status"`
|
||||
ModifyPassword web.HandlerFunc `path:"/modify-password" method:"post" auth:"?" desc:"modify password"`
|
||||
ModifyProfile web.HandlerFunc `path:"/modify-profile" method:"post" auth:"?" desc:"modify profile"`
|
||||
}
|
||||
|
||||
// NewUser creates an instance of UserHandler
|
||||
func NewUser(b biz.UserBiz, eb biz.EventBiz, auth *security.Authenticator) *UserHandler {
|
||||
return &UserHandler{
|
||||
SignIn: userSignIn(auth, eb),
|
||||
Search: userSearch(b),
|
||||
Save: userSave(b),
|
||||
Find: userFind(b),
|
||||
Delete: userDelete(b),
|
||||
SetStatus: userSetStatus(b),
|
||||
ModifyPassword: userModifyPassword(b),
|
||||
ModifyProfile: userModifyProfile(b),
|
||||
}
|
||||
}
|
||||
|
||||
func userSignIn(auth *security.Authenticator, eb biz.EventBiz) web.HandlerFunc {
|
||||
type SignInArgs struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &SignInArgs{}
|
||||
user web.User
|
||||
token string
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
if user, err = auth.Login(args.Name, args.Password); err == nil {
|
||||
jwt := container.Find("identifier").(*security.JWT)
|
||||
token, err = jwt.CreateToken(user.ID(), user.Name())
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eb.CreateUser(biz.EventActionLogin, user.ID(), user.Name(), user)
|
||||
return success(ctx, data.Map{
|
||||
"token": token,
|
||||
"id": user.ID(),
|
||||
"name": user.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func userSave(b biz.UserBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
user := &biz.User{}
|
||||
err := ctx.Bind(user, true)
|
||||
if err == nil {
|
||||
user.Type = biz.UserTypeInternal
|
||||
if user.ID == "" {
|
||||
_, err = b.Create(user, ctx.User())
|
||||
} else {
|
||||
err = b.Update(user, ctx.User())
|
||||
}
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func userSearch(b biz.UserBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Filter string `bind:"filter"` // admins, active, blocked
|
||||
Name string `bind:"name"`
|
||||
LoginName string `bind:"loginName"`
|
||||
PageIndex int `bind:"pageIndex"`
|
||||
PageSize int `bind:"pageSize"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) error {
|
||||
args := &Args{}
|
||||
err := ctx.Bind(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users, total, err := b.Search(args.Name, args.LoginName, args.Filter, args.PageIndex, args.PageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"items": users, "total": total})
|
||||
}
|
||||
}
|
||||
|
||||
func userFind(b biz.UserBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
id := ctx.Query("id")
|
||||
if id == "" {
|
||||
id = ctx.User().ID()
|
||||
}
|
||||
user, err := b.FindByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, user)
|
||||
}
|
||||
}
|
||||
|
||||
func userDelete(b biz.UserBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) error {
|
||||
args := &Args{}
|
||||
err := ctx.Bind(args)
|
||||
if err == nil {
|
||||
err = b.Delete(args.ID, args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func userSetStatus(b biz.UserBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
ID string `json:"id"`
|
||||
Status int32 `json:"status"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) error {
|
||||
args := &Args{}
|
||||
err := ctx.Bind(args)
|
||||
if err == nil {
|
||||
err = b.SetStatus(args.ID, args.Status)
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func userModifyPassword(b biz.UserBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
OldPassword string `json:"oldPwd"`
|
||||
NewPassword string `json:"newPwd"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) error {
|
||||
args := &Args{}
|
||||
err := ctx.Bind(args)
|
||||
if err == nil {
|
||||
err = b.ModifyPassword(ctx.User().ID(), args.OldPassword, args.NewPassword)
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func userModifyProfile(b biz.UserBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
u := &biz.User{}
|
||||
err := ctx.Bind(u, true)
|
||||
if err == nil {
|
||||
u.ID = ctx.User().ID()
|
||||
err = b.ModifyProfile(u)
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
104
api/volume.go
Normal file
104
api/volume.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
// VolumeHandler encapsulates volume related handlers.
|
||||
type VolumeHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"volume.view" desc:"search volumes"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"volume.view" desc:"find volume by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"volume.delete" desc:"delete volume"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"volume.edit" desc:"create or update volume"`
|
||||
Prune web.HandlerFunc `path:"/prune" method:"post" auth:"volume.delete" desc:"delete unused volumes"`
|
||||
}
|
||||
|
||||
// NewVolume creates an instance of VolumeHandler
|
||||
func NewVolume(b biz.VolumeBiz) *VolumeHandler {
|
||||
return &VolumeHandler{
|
||||
Search: volumeSearch(b),
|
||||
Find: volumeFind(b),
|
||||
Delete: volumeDelete(b),
|
||||
Save: volumeSave(b),
|
||||
Prune: volumePrune(b),
|
||||
}
|
||||
}
|
||||
|
||||
func volumeSearch(b biz.VolumeBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name" bind:"name"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
}
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
volumes []*biz.Volume
|
||||
total int
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
volumes, total, err = b.Search(args.Name, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"items": volumes,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func volumeFind(b biz.VolumeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
name := ctx.Query("name")
|
||||
volume, raw, err := b.Find(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{"volume": volume, "raw": raw})
|
||||
}
|
||||
}
|
||||
|
||||
func volumeDelete(b biz.VolumeBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func volumeSave(b biz.VolumeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
v := &biz.Volume{}
|
||||
err := ctx.Bind(v, true)
|
||||
if err == nil {
|
||||
err = b.Create(v, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func volumePrune(b biz.VolumeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
deletedVolumes, reclaimedSpace, err := b.Prune(ctx.User())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, data.Map{
|
||||
"deletedVolumes": deletedVolumes,
|
||||
"reclaimedSpace": reclaimedSpace,
|
||||
})
|
||||
}
|
||||
}
|
||||
14484
assets/bulma/bulma.css
vendored
14484
assets/bulma/bulma.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
213
assets/codemirror/addon/comment.js
vendored
213
assets/codemirror/addon/comment.js
vendored
@@ -1,213 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var noOptions = {};
|
||||
var nonWS = /[^\s\u00a0]/;
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function firstNonWS(str) {
|
||||
var found = str.search(nonWS);
|
||||
return found == -1 ? 0 : found;
|
||||
}
|
||||
|
||||
CodeMirror.commands.toggleComment = function(cm) {
|
||||
cm.toggleComment();
|
||||
};
|
||||
|
||||
CodeMirror.defineExtension("toggleComment", function(options) {
|
||||
if (!options) options = noOptions;
|
||||
var cm = this;
|
||||
var minLine = Infinity, ranges = this.listSelections(), mode = null;
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var from = ranges[i].from(), to = ranges[i].to();
|
||||
if (from.line >= minLine) continue;
|
||||
if (to.line >= minLine) to = Pos(minLine, 0);
|
||||
minLine = from.line;
|
||||
if (mode == null) {
|
||||
if (cm.uncomment(from, to, options)) mode = "un";
|
||||
else { cm.lineComment(from, to, options); mode = "line"; }
|
||||
} else if (mode == "un") {
|
||||
cm.uncomment(from, to, options);
|
||||
} else {
|
||||
cm.lineComment(from, to, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Rough heuristic to try and detect lines that are part of multi-line string
|
||||
function probablyInsideString(cm, pos, line) {
|
||||
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line)
|
||||
}
|
||||
|
||||
function getMode(cm, pos) {
|
||||
var mode = cm.getMode()
|
||||
return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos)
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("lineComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var firstLine = self.getLine(from.line);
|
||||
if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
|
||||
|
||||
var commentString = options.lineComment || mode.lineComment;
|
||||
if (!commentString) {
|
||||
if (options.blockCommentStart || mode.blockCommentStart) {
|
||||
options.fullLines = true;
|
||||
self.blockComment(from, to, options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
|
||||
var pad = options.padding == null ? " " : options.padding;
|
||||
var blankLines = options.commentBlankLines || from.line == to.line;
|
||||
|
||||
self.operation(function() {
|
||||
if (options.indent) {
|
||||
var baseString = null;
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var whitespace = line.slice(0, firstNonWS(line));
|
||||
if (baseString == null || baseString.length > whitespace.length) {
|
||||
baseString = whitespace;
|
||||
}
|
||||
}
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i), cut = baseString.length;
|
||||
if (!blankLines && !nonWS.test(line)) continue;
|
||||
if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
|
||||
self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
|
||||
}
|
||||
} else {
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
if (blankLines || nonWS.test(self.getLine(i)))
|
||||
self.replaceRange(commentString + pad, Pos(i, 0));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("blockComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) {
|
||||
if ((options.lineComment || mode.lineComment) && options.fullLines != false)
|
||||
self.lineComment(from, to, options);
|
||||
return;
|
||||
}
|
||||
if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return
|
||||
|
||||
var end = Math.min(to.line, self.lastLine());
|
||||
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
|
||||
|
||||
var pad = options.padding == null ? " " : options.padding;
|
||||
if (from.line > end) return;
|
||||
|
||||
self.operation(function() {
|
||||
if (options.fullLines != false) {
|
||||
var lastLineHasText = nonWS.test(self.getLine(end));
|
||||
self.replaceRange(pad + endString, Pos(end));
|
||||
self.replaceRange(startString + pad, Pos(from.line, 0));
|
||||
var lead = options.blockCommentLead || mode.blockCommentLead;
|
||||
if (lead != null) for (var i = from.line + 1; i <= end; ++i)
|
||||
if (i != end || lastLineHasText)
|
||||
self.replaceRange(lead + pad, Pos(i, 0));
|
||||
} else {
|
||||
self.replaceRange(endString, to);
|
||||
self.replaceRange(startString, from);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("uncomment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
|
||||
|
||||
// Try finding line comments
|
||||
var lineString = options.lineComment || mode.lineComment, lines = [];
|
||||
var pad = options.padding == null ? " " : options.padding, didSomething;
|
||||
lineComment: {
|
||||
if (!lineString) break lineComment;
|
||||
for (var i = start; i <= end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var found = line.indexOf(lineString);
|
||||
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
|
||||
if (found == -1 && nonWS.test(line)) break lineComment;
|
||||
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
|
||||
lines.push(line);
|
||||
}
|
||||
self.operation(function() {
|
||||
for (var i = start; i <= end; ++i) {
|
||||
var line = lines[i - start];
|
||||
var pos = line.indexOf(lineString), endPos = pos + lineString.length;
|
||||
if (pos < 0) continue;
|
||||
if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
|
||||
didSomething = true;
|
||||
self.replaceRange("", Pos(i, pos), Pos(i, endPos));
|
||||
}
|
||||
});
|
||||
if (didSomething) return true;
|
||||
}
|
||||
|
||||
// Try block comments
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) return false;
|
||||
var lead = options.blockCommentLead || mode.blockCommentLead;
|
||||
var startLine = self.getLine(start), open = startLine.indexOf(startString)
|
||||
if (open == -1) return false
|
||||
var endLine = end == start ? startLine : self.getLine(end)
|
||||
var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
|
||||
if (close == -1 && start != end) {
|
||||
endLine = self.getLine(--end);
|
||||
close = endLine.indexOf(endString);
|
||||
}
|
||||
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
|
||||
if (close == -1 ||
|
||||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
|
||||
!/comment/.test(self.getTokenTypeAt(insideEnd)) ||
|
||||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
|
||||
return false;
|
||||
|
||||
// Avoid killing block comments completely outside the selection.
|
||||
// Positions of the last startString before the start of the selection, and the first endString after it.
|
||||
var lastStart = startLine.lastIndexOf(startString, from.ch);
|
||||
var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
|
||||
if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
|
||||
// Positions of the first endString after the end of the selection, and the last startString before it.
|
||||
firstEnd = endLine.indexOf(endString, to.ch);
|
||||
var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
|
||||
lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
|
||||
if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
|
||||
|
||||
self.operation(function() {
|
||||
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
|
||||
Pos(end, close + endString.length));
|
||||
var openEnd = open + startString.length;
|
||||
if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
|
||||
self.replaceRange("", Pos(start, open), Pos(start, openEnd));
|
||||
if (lead) for (var i = start + 1; i <= end; ++i) {
|
||||
var line = self.getLine(i), found = line.indexOf(lead);
|
||||
if (found == -1 || nonWS.test(line.slice(0, found))) continue;
|
||||
var foundEnd = found + lead.length;
|
||||
if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
|
||||
self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -1,341 +0,0 @@
|
||||
/* BASICS */
|
||||
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
color: black;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
/* PADDING */
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0; /* Vertical padding around content */
|
||||
}
|
||||
.CodeMirror pre {
|
||||
padding: 0 4px; /* Horizontal padding of content */
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
background-color: white; /* The little square between H and V scrollbars */
|
||||
}
|
||||
|
||||
/* GUTTER */
|
||||
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f7f7f7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.CodeMirror-linenumbers {}
|
||||
.CodeMirror-linenumber {
|
||||
padding: 0 3px 0 5px;
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.CodeMirror-guttermarker { color: black; }
|
||||
.CodeMirror-guttermarker-subtle { color: #999; }
|
||||
|
||||
/* CURSOR */
|
||||
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
}
|
||||
/* Shown when moving in bi-directional text */
|
||||
.CodeMirror div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
.cm-fat-cursor .CodeMirror-cursor {
|
||||
width: auto;
|
||||
border: 0 !important;
|
||||
background: #7e7;
|
||||
}
|
||||
.cm-fat-cursor div.CodeMirror-cursors {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cm-animate-fat-cursor {
|
||||
width: auto;
|
||||
border: 0;
|
||||
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||
-moz-animation: blink 1.06s steps(1) infinite;
|
||||
animation: blink 1.06s steps(1) infinite;
|
||||
background-color: #7e7;
|
||||
}
|
||||
@-moz-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
@-webkit-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
|
||||
/* Can style cursor different in overwrite (non-insert) mode */
|
||||
.CodeMirror-overwrite .CodeMirror-cursor {}
|
||||
|
||||
.cm-tab { display: inline-block; text-decoration: inherit; }
|
||||
|
||||
.CodeMirror-rulers {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: -50px; bottom: -20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.CodeMirror-ruler {
|
||||
border-left: 1px solid #ccc;
|
||||
top: 0; bottom: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* DEFAULT THEME */
|
||||
|
||||
.cm-s-default .cm-header {color: blue;}
|
||||
.cm-s-default .cm-quote {color: #090;}
|
||||
.cm-negative {color: #d44;}
|
||||
.cm-positive {color: #292;}
|
||||
.cm-header, .cm-strong {font-weight: bold;}
|
||||
.cm-em {font-style: italic;}
|
||||
.cm-link {text-decoration: underline;}
|
||||
.cm-strikethrough {text-decoration: line-through;}
|
||||
|
||||
.cm-s-default .cm-keyword {color: #708;}
|
||||
.cm-s-default .cm-atom {color: #219;}
|
||||
.cm-s-default .cm-number {color: #164;}
|
||||
.cm-s-default .cm-def {color: #00f;}
|
||||
.cm-s-default .cm-variable,
|
||||
.cm-s-default .cm-punctuation,
|
||||
.cm-s-default .cm-property,
|
||||
.cm-s-default .cm-operator {}
|
||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
|
||||
.cm-s-default .cm-comment {color: #a50;}
|
||||
.cm-s-default .cm-string {color: #a11;}
|
||||
.cm-s-default .cm-string-2 {color: #f50;}
|
||||
.cm-s-default .cm-meta {color: #555;}
|
||||
.cm-s-default .cm-qualifier {color: #555;}
|
||||
.cm-s-default .cm-builtin {color: #30a;}
|
||||
.cm-s-default .cm-bracket {color: #997;}
|
||||
.cm-s-default .cm-tag {color: #170;}
|
||||
.cm-s-default .cm-attribute {color: #00c;}
|
||||
.cm-s-default .cm-hr {color: #999;}
|
||||
.cm-s-default .cm-link {color: #00c;}
|
||||
|
||||
.cm-s-default .cm-error {color: #f00;}
|
||||
.cm-invalidchar {color: #f00;}
|
||||
|
||||
.CodeMirror-composing { border-bottom: 2px solid; }
|
||||
|
||||
/* Default styles for common addons */
|
||||
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||
|
||||
/* STOP */
|
||||
|
||||
/* The rest of this file contains styles related to the mechanics of
|
||||
the editor. You probably shouldn't touch them. */
|
||||
|
||||
.CodeMirror {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: scroll !important; /* Things will break if this is overridden */
|
||||
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||
/* See overflow: hidden in .CodeMirror */
|
||||
margin-bottom: -30px; margin-right: -30px;
|
||||
padding-bottom: 30px;
|
||||
height: 100%;
|
||||
outline: none; /* Prevent dragging from highlighting the element */
|
||||
position: relative;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
border-right: 30px solid transparent;
|
||||
}
|
||||
|
||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||
before actual scrolling happens, thus preventing shaking and
|
||||
flickering artifacts. */
|
||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
right: 0; top: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 0; left: 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
right: 0; bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutter-filler {
|
||||
left: 0; bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
position: absolute; left: 0; top: 0;
|
||||
min-height: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
.CodeMirror-gutter {
|
||||
white-space: normal;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: -30px;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.CodeMirror-gutter-background {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-elt {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
|
||||
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: text;
|
||||
min-height: 1px; /* prevents collapsing before first draw */
|
||||
}
|
||||
.CodeMirror pre {
|
||||
/* Reset some styles that the rest of the page might have set */
|
||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-font-variant-ligatures: contextual;
|
||||
font-variant-ligatures: contextual;
|
||||
}
|
||||
.CodeMirror-wrap pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.CodeMirror-linebackground {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-linewidget {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.CodeMirror-widget {}
|
||||
|
||||
.CodeMirror-rtl pre { direction: rtl; }
|
||||
|
||||
.CodeMirror-code {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Force content-box sizing for the elements where we expect it */
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-sizer,
|
||||
.CodeMirror-gutter,
|
||||
.CodeMirror-gutters,
|
||||
.CodeMirror-linenumber {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.CodeMirror-measure pre { position: static; }
|
||||
|
||||
div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
div.CodeMirror-dragcursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-focused div.CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-selected { background: #d9d9d9; }
|
||||
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||
.CodeMirror-crosshair { cursor: crosshair; }
|
||||
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
|
||||
|
||||
.cm-searching {
|
||||
background-color: #ffa;
|
||||
background-color: rgba(255, 255, 0, .4);
|
||||
}
|
||||
|
||||
/* Used to force a border model for a node */
|
||||
.cm-force-border { padding-right: .1px; }
|
||||
|
||||
@media print {
|
||||
/* Hide the cursor when printing */
|
||||
.CodeMirror div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* See issue #2901 */
|
||||
.cm-tab-wrap-hack:after { content: ''; }
|
||||
|
||||
/* Help users use markselection to safely style text background */
|
||||
span.CodeMirror-selectedtext { background: none; }
|
||||
File diff suppressed because it is too large
Load Diff
613
assets/codemirror/keymap/sublime.js
vendored
613
assets/codemirror/keymap/sublime.js
vendored
@@ -1,613 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// A rough approximation of Sublime Text's keybindings
|
||||
// Depends on addon/search/searchcursor.js and optionally addon/dialog/dialogs.js
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/edit/matchbrackets"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/edit/matchbrackets"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var map = CodeMirror.keyMap.sublime = {fallthrough: "default"};
|
||||
var cmds = CodeMirror.commands;
|
||||
var Pos = CodeMirror.Pos;
|
||||
var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
|
||||
var ctrl = mac ? "Cmd-" : "Ctrl-";
|
||||
|
||||
// This is not exactly Sublime's algorithm. I couldn't make heads or tails of that.
|
||||
function findPosSubword(doc, start, dir) {
|
||||
if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1));
|
||||
var line = doc.getLine(start.line);
|
||||
if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0));
|
||||
var state = "start", type;
|
||||
for (var pos = start.ch, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) {
|
||||
var next = line.charAt(dir < 0 ? pos - 1 : pos);
|
||||
var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o";
|
||||
if (cat == "w" && next.toUpperCase() == next) cat = "W";
|
||||
if (state == "start") {
|
||||
if (cat != "o") { state = "in"; type = cat; }
|
||||
} else if (state == "in") {
|
||||
if (type != cat) {
|
||||
if (type == "w" && cat == "W" && dir < 0) pos--;
|
||||
if (type == "W" && cat == "w" && dir > 0) { type = "w"; continue; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pos(start.line, pos);
|
||||
}
|
||||
|
||||
function moveSubword(cm, dir) {
|
||||
cm.extendSelectionsBy(function(range) {
|
||||
if (cm.display.shift || cm.doc.extend || range.empty())
|
||||
return findPosSubword(cm.doc, range.head, dir);
|
||||
else
|
||||
return dir < 0 ? range.from() : range.to();
|
||||
});
|
||||
}
|
||||
|
||||
var goSubwordCombo = mac ? "Ctrl-" : "Alt-";
|
||||
|
||||
cmds[map[goSubwordCombo + "Left"] = "goSubwordLeft"] = function(cm) { moveSubword(cm, -1); };
|
||||
cmds[map[goSubwordCombo + "Right"] = "goSubwordRight"] = function(cm) { moveSubword(cm, 1); };
|
||||
|
||||
if (mac) map["Cmd-Left"] = "goLineStartSmart";
|
||||
|
||||
var scrollLineCombo = mac ? "Ctrl-Alt-" : "Ctrl-";
|
||||
|
||||
cmds[map[scrollLineCombo + "Up"] = "scrollLineUp"] = function(cm) {
|
||||
var info = cm.getScrollInfo();
|
||||
if (!cm.somethingSelected()) {
|
||||
var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local");
|
||||
if (cm.getCursor().line >= visibleBottomLine)
|
||||
cm.execCommand("goLineUp");
|
||||
}
|
||||
cm.scrollTo(null, info.top - cm.defaultTextHeight());
|
||||
};
|
||||
cmds[map[scrollLineCombo + "Down"] = "scrollLineDown"] = function(cm) {
|
||||
var info = cm.getScrollInfo();
|
||||
if (!cm.somethingSelected()) {
|
||||
var visibleTopLine = cm.lineAtHeight(info.top, "local")+1;
|
||||
if (cm.getCursor().line <= visibleTopLine)
|
||||
cm.execCommand("goLineDown");
|
||||
}
|
||||
cm.scrollTo(null, info.top + cm.defaultTextHeight());
|
||||
};
|
||||
|
||||
cmds[map["Shift-" + ctrl + "L"] = "splitSelectionByLine"] = function(cm) {
|
||||
var ranges = cm.listSelections(), lineRanges = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var from = ranges[i].from(), to = ranges[i].to();
|
||||
for (var line = from.line; line <= to.line; ++line)
|
||||
if (!(to.line > from.line && line == to.line && to.ch == 0))
|
||||
lineRanges.push({anchor: line == from.line ? from : Pos(line, 0),
|
||||
head: line == to.line ? to : Pos(line)});
|
||||
}
|
||||
cm.setSelections(lineRanges, 0);
|
||||
};
|
||||
|
||||
map["Shift-Tab"] = "indentLess";
|
||||
|
||||
cmds[map["Esc"] = "singleSelectionTop"] = function(cm) {
|
||||
var range = cm.listSelections()[0];
|
||||
cm.setSelection(range.anchor, range.head, {scroll: false});
|
||||
};
|
||||
|
||||
cmds[map[ctrl + "L"] = "selectLine"] = function(cm) {
|
||||
var ranges = cm.listSelections(), extended = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i];
|
||||
extended.push({anchor: Pos(range.from().line, 0),
|
||||
head: Pos(range.to().line + 1, 0)});
|
||||
}
|
||||
cm.setSelections(extended);
|
||||
};
|
||||
|
||||
map["Shift-Ctrl-K"] = "deleteLine";
|
||||
|
||||
function insertLine(cm, above) {
|
||||
if (cm.isReadOnly()) return CodeMirror.Pass
|
||||
cm.operation(function() {
|
||||
var len = cm.listSelections().length, newSelection = [], last = -1;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var head = cm.listSelections()[i].head;
|
||||
if (head.line <= last) continue;
|
||||
var at = Pos(head.line + (above ? 0 : 1), 0);
|
||||
cm.replaceRange("\n", at, null, "+insertLine");
|
||||
cm.indentLine(at.line, null, true);
|
||||
newSelection.push({head: at, anchor: at});
|
||||
last = head.line + 1;
|
||||
}
|
||||
cm.setSelections(newSelection);
|
||||
});
|
||||
cm.execCommand("indentAuto");
|
||||
}
|
||||
|
||||
cmds[map[ctrl + "Enter"] = "insertLineAfter"] = function(cm) { return insertLine(cm, false); };
|
||||
|
||||
cmds[map["Shift-" + ctrl + "Enter"] = "insertLineBefore"] = function(cm) { return insertLine(cm, true); };
|
||||
|
||||
function wordAt(cm, pos) {
|
||||
var start = pos.ch, end = start, line = cm.getLine(pos.line);
|
||||
while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start;
|
||||
while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end;
|
||||
return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)};
|
||||
}
|
||||
|
||||
cmds[map[ctrl + "D"] = "selectNextOccurrence"] = function(cm) {
|
||||
var from = cm.getCursor("from"), to = cm.getCursor("to");
|
||||
var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel;
|
||||
if (CodeMirror.cmpPos(from, to) == 0) {
|
||||
var word = wordAt(cm, from);
|
||||
if (!word.word) return;
|
||||
cm.setSelection(word.from, word.to);
|
||||
fullWord = true;
|
||||
} else {
|
||||
var text = cm.getRange(from, to);
|
||||
var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
|
||||
var cur = cm.getSearchCursor(query, to);
|
||||
var found = cur.findNext();
|
||||
if (!found) {
|
||||
cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
|
||||
found = cur.findNext();
|
||||
}
|
||||
if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to()))
|
||||
return CodeMirror.Pass
|
||||
cm.addSelection(cur.from(), cur.to());
|
||||
}
|
||||
if (fullWord)
|
||||
cm.state.sublimeFindFullWord = cm.doc.sel;
|
||||
};
|
||||
|
||||
function addCursorToSelection(cm, dir) {
|
||||
var ranges = cm.listSelections(), newRanges = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i];
|
||||
var newAnchor = cm.findPosV(range.anchor, dir, "line");
|
||||
var newHead = cm.findPosV(range.head, dir, "line");
|
||||
var newRange = {anchor: newAnchor, head: newHead};
|
||||
newRanges.push(range);
|
||||
newRanges.push(newRange);
|
||||
}
|
||||
cm.setSelections(newRanges);
|
||||
}
|
||||
|
||||
var addCursorToLineCombo = mac ? "Shift-Cmd" : 'Alt-Ctrl';
|
||||
cmds[map[addCursorToLineCombo + "Up"] = "addCursorToPrevLine"] = function(cm) { addCursorToSelection(cm, -1); };
|
||||
cmds[map[addCursorToLineCombo + "Down"] = "addCursorToNextLine"] = function(cm) { addCursorToSelection(cm, 1); };
|
||||
|
||||
function isSelectedRange(ranges, from, to) {
|
||||
for (var i = 0; i < ranges.length; i++)
|
||||
if (ranges[i].from() == from && ranges[i].to() == to) return true
|
||||
return false
|
||||
}
|
||||
|
||||
var mirror = "(){}[]";
|
||||
function selectBetweenBrackets(cm) {
|
||||
var ranges = cm.listSelections(), newRanges = []
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i], pos = range.head, opening = cm.scanForBracket(pos, -1);
|
||||
if (!opening) return false;
|
||||
for (;;) {
|
||||
var closing = cm.scanForBracket(pos, 1);
|
||||
if (!closing) return false;
|
||||
if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) {
|
||||
newRanges.push({anchor: Pos(opening.pos.line, opening.pos.ch + 1),
|
||||
head: closing.pos});
|
||||
break;
|
||||
}
|
||||
pos = Pos(closing.pos.line, closing.pos.ch + 1);
|
||||
}
|
||||
}
|
||||
cm.setSelections(newRanges);
|
||||
return true;
|
||||
}
|
||||
|
||||
cmds[map["Shift-" + ctrl + "Space"] = "selectScope"] = function(cm) {
|
||||
selectBetweenBrackets(cm) || cm.execCommand("selectAll");
|
||||
};
|
||||
cmds[map["Shift-" + ctrl + "M"] = "selectBetweenBrackets"] = function(cm) {
|
||||
if (!selectBetweenBrackets(cm)) return CodeMirror.Pass;
|
||||
};
|
||||
|
||||
cmds[map[ctrl + "M"] = "goToBracket"] = function(cm) {
|
||||
cm.extendSelectionsBy(function(range) {
|
||||
var next = cm.scanForBracket(range.head, 1);
|
||||
if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos;
|
||||
var prev = cm.scanForBracket(range.head, -1);
|
||||
return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head;
|
||||
});
|
||||
};
|
||||
|
||||
var swapLineCombo = mac ? "Cmd-Ctrl-" : "Shift-Ctrl-";
|
||||
|
||||
cmds[map[swapLineCombo + "Up"] = "swapLineUp"] = function(cm) {
|
||||
if (cm.isReadOnly()) return CodeMirror.Pass
|
||||
var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i], from = range.from().line - 1, to = range.to().line;
|
||||
newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch),
|
||||
head: Pos(range.head.line - 1, range.head.ch)});
|
||||
if (range.to().ch == 0 && !range.empty()) --to;
|
||||
if (from > at) linesToMove.push(from, to);
|
||||
else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
|
||||
at = to;
|
||||
}
|
||||
cm.operation(function() {
|
||||
for (var i = 0; i < linesToMove.length; i += 2) {
|
||||
var from = linesToMove[i], to = linesToMove[i + 1];
|
||||
var line = cm.getLine(from);
|
||||
cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
|
||||
if (to > cm.lastLine())
|
||||
cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine");
|
||||
else
|
||||
cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
|
||||
}
|
||||
cm.setSelections(newSels);
|
||||
cm.scrollIntoView();
|
||||
});
|
||||
};
|
||||
|
||||
cmds[map[swapLineCombo + "Down"] = "swapLineDown"] = function(cm) {
|
||||
if (cm.isReadOnly()) return CodeMirror.Pass
|
||||
var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1;
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var range = ranges[i], from = range.to().line + 1, to = range.from().line;
|
||||
if (range.to().ch == 0 && !range.empty()) from--;
|
||||
if (from < at) linesToMove.push(from, to);
|
||||
else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
|
||||
at = to;
|
||||
}
|
||||
cm.operation(function() {
|
||||
for (var i = linesToMove.length - 2; i >= 0; i -= 2) {
|
||||
var from = linesToMove[i], to = linesToMove[i + 1];
|
||||
var line = cm.getLine(from);
|
||||
if (from == cm.lastLine())
|
||||
cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine");
|
||||
else
|
||||
cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
|
||||
cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
|
||||
}
|
||||
cm.scrollIntoView();
|
||||
});
|
||||
};
|
||||
|
||||
cmds[map[ctrl + "/"] = "toggleCommentIndented"] = function(cm) {
|
||||
cm.toggleComment({ indent: true });
|
||||
}
|
||||
|
||||
cmds[map[ctrl + "J"] = "joinLines"] = function(cm) {
|
||||
var ranges = cm.listSelections(), joined = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i], from = range.from();
|
||||
var start = from.line, end = range.to().line;
|
||||
while (i < ranges.length - 1 && ranges[i + 1].from().line == end)
|
||||
end = ranges[++i].to().line;
|
||||
joined.push({start: start, end: end, anchor: !range.empty() && from});
|
||||
}
|
||||
cm.operation(function() {
|
||||
var offset = 0, ranges = [];
|
||||
for (var i = 0; i < joined.length; i++) {
|
||||
var obj = joined[i];
|
||||
var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head;
|
||||
for (var line = obj.start; line <= obj.end; line++) {
|
||||
var actual = line - offset;
|
||||
if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1);
|
||||
if (actual < cm.lastLine()) {
|
||||
cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length));
|
||||
++offset;
|
||||
}
|
||||
}
|
||||
ranges.push({anchor: anchor || head, head: head});
|
||||
}
|
||||
cm.setSelections(ranges, 0);
|
||||
});
|
||||
};
|
||||
|
||||
cmds[map["Shift-" + ctrl + "D"] = "duplicateLine"] = function(cm) {
|
||||
cm.operation(function() {
|
||||
var rangeCount = cm.listSelections().length;
|
||||
for (var i = 0; i < rangeCount; i++) {
|
||||
var range = cm.listSelections()[i];
|
||||
if (range.empty())
|
||||
cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0));
|
||||
else
|
||||
cm.replaceRange(cm.getRange(range.from(), range.to()), range.from());
|
||||
}
|
||||
cm.scrollIntoView();
|
||||
});
|
||||
};
|
||||
|
||||
if (!mac) map[ctrl + "T"] = "transposeChars";
|
||||
|
||||
function sortLines(cm, caseSensitive) {
|
||||
if (cm.isReadOnly()) return CodeMirror.Pass
|
||||
var ranges = cm.listSelections(), toSort = [], selected;
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i];
|
||||
if (range.empty()) continue;
|
||||
var from = range.from().line, to = range.to().line;
|
||||
while (i < ranges.length - 1 && ranges[i + 1].from().line == to)
|
||||
to = ranges[++i].to().line;
|
||||
if (!ranges[i].to().ch) to--;
|
||||
toSort.push(from, to);
|
||||
}
|
||||
if (toSort.length) selected = true;
|
||||
else toSort.push(cm.firstLine(), cm.lastLine());
|
||||
|
||||
cm.operation(function() {
|
||||
var ranges = [];
|
||||
for (var i = 0; i < toSort.length; i += 2) {
|
||||
var from = toSort[i], to = toSort[i + 1];
|
||||
var start = Pos(from, 0), end = Pos(to);
|
||||
var lines = cm.getRange(start, end, false);
|
||||
if (caseSensitive)
|
||||
lines.sort();
|
||||
else
|
||||
lines.sort(function(a, b) {
|
||||
var au = a.toUpperCase(), bu = b.toUpperCase();
|
||||
if (au != bu) { a = au; b = bu; }
|
||||
return a < b ? -1 : a == b ? 0 : 1;
|
||||
});
|
||||
cm.replaceRange(lines, start, end);
|
||||
if (selected) ranges.push({anchor: start, head: Pos(to + 1, 0)});
|
||||
}
|
||||
if (selected) cm.setSelections(ranges, 0);
|
||||
});
|
||||
}
|
||||
|
||||
cmds[map["F9"] = "sortLines"] = function(cm) { sortLines(cm, true); };
|
||||
cmds[map[ctrl + "F9"] = "sortLinesInsensitive"] = function(cm) { sortLines(cm, false); };
|
||||
|
||||
cmds[map["F2"] = "nextBookmark"] = function(cm) {
|
||||
var marks = cm.state.sublimeBookmarks;
|
||||
if (marks) while (marks.length) {
|
||||
var current = marks.shift();
|
||||
var found = current.find();
|
||||
if (found) {
|
||||
marks.push(current);
|
||||
return cm.setSelection(found.from, found.to);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cmds[map["Shift-F2"] = "prevBookmark"] = function(cm) {
|
||||
var marks = cm.state.sublimeBookmarks;
|
||||
if (marks) while (marks.length) {
|
||||
marks.unshift(marks.pop());
|
||||
var found = marks[marks.length - 1].find();
|
||||
if (!found)
|
||||
marks.pop();
|
||||
else
|
||||
return cm.setSelection(found.from, found.to);
|
||||
}
|
||||
};
|
||||
|
||||
cmds[map[ctrl + "F2"] = "toggleBookmark"] = function(cm) {
|
||||
var ranges = cm.listSelections();
|
||||
var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []);
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var from = ranges[i].from(), to = ranges[i].to();
|
||||
var found = cm.findMarks(from, to);
|
||||
for (var j = 0; j < found.length; j++) {
|
||||
if (found[j].sublimeBookmark) {
|
||||
found[j].clear();
|
||||
for (var k = 0; k < marks.length; k++)
|
||||
if (marks[k] == found[j])
|
||||
marks.splice(k--, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j == found.length)
|
||||
marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false}));
|
||||
}
|
||||
};
|
||||
|
||||
cmds[map["Shift-" + ctrl + "F2"] = "clearBookmarks"] = function(cm) {
|
||||
var marks = cm.state.sublimeBookmarks;
|
||||
if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear();
|
||||
marks.length = 0;
|
||||
};
|
||||
|
||||
cmds[map["Alt-F2"] = "selectBookmarks"] = function(cm) {
|
||||
var marks = cm.state.sublimeBookmarks, ranges = [];
|
||||
if (marks) for (var i = 0; i < marks.length; i++) {
|
||||
var found = marks[i].find();
|
||||
if (!found)
|
||||
marks.splice(i--, 0);
|
||||
else
|
||||
ranges.push({anchor: found.from, head: found.to});
|
||||
}
|
||||
if (ranges.length)
|
||||
cm.setSelections(ranges, 0);
|
||||
};
|
||||
|
||||
map["Alt-Q"] = "wrapLines";
|
||||
|
||||
var cK = ctrl + "K ";
|
||||
|
||||
function modifyWordOrSelection(cm, mod) {
|
||||
cm.operation(function() {
|
||||
var ranges = cm.listSelections(), indices = [], replacements = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i];
|
||||
if (range.empty()) { indices.push(i); replacements.push(""); }
|
||||
else replacements.push(mod(cm.getRange(range.from(), range.to())));
|
||||
}
|
||||
cm.replaceSelections(replacements, "around", "case");
|
||||
for (var i = indices.length - 1, at; i >= 0; i--) {
|
||||
var range = ranges[indices[i]];
|
||||
if (at && CodeMirror.cmpPos(range.head, at) > 0) continue;
|
||||
var word = wordAt(cm, range.head);
|
||||
at = word.from;
|
||||
cm.replaceRange(mod(word.word), word.from, word.to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
map[cK + ctrl + "Backspace"] = "delLineLeft";
|
||||
|
||||
cmds[map["Backspace"] = "smartBackspace"] = function(cm) {
|
||||
if (cm.somethingSelected()) return CodeMirror.Pass;
|
||||
|
||||
cm.operation(function() {
|
||||
var cursors = cm.listSelections();
|
||||
var indentUnit = cm.getOption("indentUnit");
|
||||
|
||||
for (var i = cursors.length - 1; i >= 0; i--) {
|
||||
var cursor = cursors[i].head;
|
||||
var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor);
|
||||
var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize"));
|
||||
|
||||
// Delete by one character by default
|
||||
var deletePos = cm.findPosH(cursor, -1, "char", false);
|
||||
|
||||
if (toStartOfLine && !/\S/.test(toStartOfLine) && column % indentUnit == 0) {
|
||||
var prevIndent = new Pos(cursor.line,
|
||||
CodeMirror.findColumn(toStartOfLine, column - indentUnit, indentUnit));
|
||||
|
||||
// Smart delete only if we found a valid prevIndent location
|
||||
if (prevIndent.ch != cursor.ch) deletePos = prevIndent;
|
||||
}
|
||||
|
||||
cm.replaceRange("", deletePos, cursor, "+delete");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cmds[map[cK + ctrl + "K"] = "delLineRight"] = function(cm) {
|
||||
cm.operation(function() {
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = ranges.length - 1; i >= 0; i--)
|
||||
cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete");
|
||||
cm.scrollIntoView();
|
||||
});
|
||||
};
|
||||
|
||||
cmds[map[cK + ctrl + "U"] = "upcaseAtCursor"] = function(cm) {
|
||||
modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); });
|
||||
};
|
||||
cmds[map[cK + ctrl + "L"] = "downcaseAtCursor"] = function(cm) {
|
||||
modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); });
|
||||
};
|
||||
|
||||
cmds[map[cK + ctrl + "Space"] = "setSublimeMark"] = function(cm) {
|
||||
if (cm.state.sublimeMark) cm.state.sublimeMark.clear();
|
||||
cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
|
||||
};
|
||||
cmds[map[cK + ctrl + "A"] = "selectToSublimeMark"] = function(cm) {
|
||||
var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
|
||||
if (found) cm.setSelection(cm.getCursor(), found);
|
||||
};
|
||||
cmds[map[cK + ctrl + "W"] = "deleteToSublimeMark"] = function(cm) {
|
||||
var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
|
||||
if (found) {
|
||||
var from = cm.getCursor(), to = found;
|
||||
if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; }
|
||||
cm.state.sublimeKilled = cm.getRange(from, to);
|
||||
cm.replaceRange("", from, to);
|
||||
}
|
||||
};
|
||||
cmds[map[cK + ctrl + "X"] = "swapWithSublimeMark"] = function(cm) {
|
||||
var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
|
||||
if (found) {
|
||||
cm.state.sublimeMark.clear();
|
||||
cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
|
||||
cm.setCursor(found);
|
||||
}
|
||||
};
|
||||
cmds[map[cK + ctrl + "Y"] = "sublimeYank"] = function(cm) {
|
||||
if (cm.state.sublimeKilled != null)
|
||||
cm.replaceSelection(cm.state.sublimeKilled, null, "paste");
|
||||
};
|
||||
|
||||
map[cK + ctrl + "G"] = "clearBookmarks";
|
||||
cmds[map[cK + ctrl + "C"] = "showInCenter"] = function(cm) {
|
||||
var pos = cm.cursorCoords(null, "local");
|
||||
cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2);
|
||||
};
|
||||
|
||||
var selectLinesCombo = mac ? "Ctrl-Shift-" : "Ctrl-Alt-";
|
||||
cmds[map[selectLinesCombo + "Up"] = "selectLinesUpward"] = function(cm) {
|
||||
cm.operation(function() {
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i];
|
||||
if (range.head.line > cm.firstLine())
|
||||
cm.addSelection(Pos(range.head.line - 1, range.head.ch));
|
||||
}
|
||||
});
|
||||
};
|
||||
cmds[map[selectLinesCombo + "Down"] = "selectLinesDownward"] = function(cm) {
|
||||
cm.operation(function() {
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i];
|
||||
if (range.head.line < cm.lastLine())
|
||||
cm.addSelection(Pos(range.head.line + 1, range.head.ch));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function getTarget(cm) {
|
||||
var from = cm.getCursor("from"), to = cm.getCursor("to");
|
||||
if (CodeMirror.cmpPos(from, to) == 0) {
|
||||
var word = wordAt(cm, from);
|
||||
if (!word.word) return;
|
||||
from = word.from;
|
||||
to = word.to;
|
||||
}
|
||||
return {from: from, to: to, query: cm.getRange(from, to), word: word};
|
||||
}
|
||||
|
||||
function findAndGoTo(cm, forward) {
|
||||
var target = getTarget(cm);
|
||||
if (!target) return;
|
||||
var query = target.query;
|
||||
var cur = cm.getSearchCursor(query, forward ? target.to : target.from);
|
||||
|
||||
if (forward ? cur.findNext() : cur.findPrevious()) {
|
||||
cm.setSelection(cur.from(), cur.to());
|
||||
} else {
|
||||
cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0)
|
||||
: cm.clipPos(Pos(cm.lastLine())));
|
||||
if (forward ? cur.findNext() : cur.findPrevious())
|
||||
cm.setSelection(cur.from(), cur.to());
|
||||
else if (target.word)
|
||||
cm.setSelection(target.from, target.to);
|
||||
}
|
||||
};
|
||||
cmds[map[ctrl + "F3"] = "findUnder"] = function(cm) { findAndGoTo(cm, true); };
|
||||
cmds[map["Shift-" + ctrl + "F3"] = "findUnderPrevious"] = function(cm) { findAndGoTo(cm,false); };
|
||||
cmds[map["Alt-F3"] = "findAllUnder"] = function(cm) {
|
||||
var target = getTarget(cm);
|
||||
if (!target) return;
|
||||
var cur = cm.getSearchCursor(target.query);
|
||||
var matches = [];
|
||||
var primaryIndex = -1;
|
||||
while (cur.findNext()) {
|
||||
matches.push({anchor: cur.from(), head: cur.to()});
|
||||
if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch)
|
||||
primaryIndex++;
|
||||
}
|
||||
cm.setSelections(matches, primaryIndex);
|
||||
};
|
||||
|
||||
map["Shift-" + ctrl + "["] = "fold";
|
||||
map["Shift-" + ctrl + "]"] = "unfold";
|
||||
map[cK + ctrl + "0"] = map[cK + ctrl + "J"] = "unfoldAll";
|
||||
|
||||
map[ctrl + "I"] = "findIncremental";
|
||||
map["Shift-" + ctrl + "I"] = "findIncrementalReverse";
|
||||
map[ctrl + "H"] = "replace";
|
||||
map["F3"] = "findNext";
|
||||
map["Shift-F3"] = "findPrev";
|
||||
|
||||
CodeMirror.normalizeKeyMap(map);
|
||||
});
|
||||
118
assets/codemirror/mode/yaml.js
vendored
118
assets/codemirror/mode/yaml.js
vendored
@@ -1,118 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineMode("yaml", function() {
|
||||
|
||||
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
|
||||
var keywordRegex = new RegExp("\\b(("+cons.join(")|(")+"))$", 'i');
|
||||
|
||||
return {
|
||||
token: function(stream, state) {
|
||||
var ch = stream.peek();
|
||||
var esc = state.escaped;
|
||||
state.escaped = false;
|
||||
/* comments */
|
||||
if (ch == "#" && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
|
||||
stream.skipToEnd();
|
||||
return "comment";
|
||||
}
|
||||
|
||||
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))
|
||||
return "string";
|
||||
|
||||
if (state.literal && stream.indentation() > state.keyCol) {
|
||||
stream.skipToEnd(); return "string";
|
||||
} else if (state.literal) { state.literal = false; }
|
||||
if (stream.sol()) {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
/* document start */
|
||||
if(stream.match(/---/)) { return "def"; }
|
||||
/* document end */
|
||||
if (stream.match(/\.\.\./)) { return "def"; }
|
||||
/* array list item */
|
||||
if (stream.match(/\s*-\s+/)) { return 'meta'; }
|
||||
}
|
||||
/* inline pairs/lists */
|
||||
if (stream.match(/^(\{|\}|\[|\])/)) {
|
||||
if (ch == '{')
|
||||
state.inlinePairs++;
|
||||
else if (ch == '}')
|
||||
state.inlinePairs--;
|
||||
else if (ch == '[')
|
||||
state.inlineList++;
|
||||
else
|
||||
state.inlineList--;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* list seperator */
|
||||
if (state.inlineList > 0 && !esc && ch == ',') {
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
/* pairs seperator */
|
||||
if (state.inlinePairs > 0 && !esc && ch == ',') {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* start of value of a pair */
|
||||
if (state.pairStart) {
|
||||
/* block literals */
|
||||
if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; };
|
||||
/* references */
|
||||
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; }
|
||||
/* numbers */
|
||||
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; }
|
||||
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; }
|
||||
/* keywords */
|
||||
if (stream.match(keywordRegex)) { return 'keyword'; }
|
||||
}
|
||||
|
||||
/* pairs (associative arrays) -> key */
|
||||
if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) {
|
||||
state.pair = true;
|
||||
state.keyCol = stream.indentation();
|
||||
return "atom";
|
||||
}
|
||||
if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; }
|
||||
|
||||
/* nothing found, continue */
|
||||
state.pairStart = false;
|
||||
state.escaped = (ch == '\\');
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
startState: function() {
|
||||
return {
|
||||
pair: false,
|
||||
pairStart: false,
|
||||
keyCol: 0,
|
||||
inlinePairs: 0,
|
||||
inlineList: 0,
|
||||
literal: false,
|
||||
escaped: false
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/x-yaml", "yaml");
|
||||
CodeMirror.defineMIME("text/yaml", "yaml");
|
||||
|
||||
});
|
||||
1
assets/dragula/dragula.min.css
vendored
1
assets/dragula/dragula.min.css
vendored
@@ -1 +0,0 @@
|
||||
.gu-mirror{position:fixed!important;margin:0!important;z-index:9999!important;opacity:.8;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";filter:alpha(opacity=80)}.gu-hide{display:none!important}.gu-unselectable{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.gu-transit{opacity:.2;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";filter:alpha(opacity=20)}
|
||||
1
assets/dragula/dragula.min.js
vendored
1
assets/dragula/dragula.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/echarts/echarts.min.js
vendored
1
assets/echarts/echarts.min.js
vendored
File diff suppressed because one or more lines are too long
343
assets/fontawesome/fa-svg-with-js.css
vendored
343
assets/fontawesome/fa-svg-with-js.css
vendored
@@ -1,343 +0,0 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com
|
||||
* License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
svg:not(:root).svg-inline--fa {
|
||||
overflow: visible; }
|
||||
|
||||
.svg-inline--fa {
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
height: 1em;
|
||||
overflow: visible;
|
||||
vertical-align: -.125em; }
|
||||
.svg-inline--fa.fa-lg {
|
||||
vertical-align: -.225em; }
|
||||
.svg-inline--fa.fa-w-1 {
|
||||
width: 0.0625em; }
|
||||
.svg-inline--fa.fa-w-2 {
|
||||
width: 0.125em; }
|
||||
.svg-inline--fa.fa-w-3 {
|
||||
width: 0.1875em; }
|
||||
.svg-inline--fa.fa-w-4 {
|
||||
width: 0.25em; }
|
||||
.svg-inline--fa.fa-w-5 {
|
||||
width: 0.3125em; }
|
||||
.svg-inline--fa.fa-w-6 {
|
||||
width: 0.375em; }
|
||||
.svg-inline--fa.fa-w-7 {
|
||||
width: 0.4375em; }
|
||||
.svg-inline--fa.fa-w-8 {
|
||||
width: 0.5em; }
|
||||
.svg-inline--fa.fa-w-9 {
|
||||
width: 0.5625em; }
|
||||
.svg-inline--fa.fa-w-10 {
|
||||
width: 0.625em; }
|
||||
.svg-inline--fa.fa-w-11 {
|
||||
width: 0.6875em; }
|
||||
.svg-inline--fa.fa-w-12 {
|
||||
width: 0.75em; }
|
||||
.svg-inline--fa.fa-w-13 {
|
||||
width: 0.8125em; }
|
||||
.svg-inline--fa.fa-w-14 {
|
||||
width: 0.875em; }
|
||||
.svg-inline--fa.fa-w-15 {
|
||||
width: 0.9375em; }
|
||||
.svg-inline--fa.fa-w-16 {
|
||||
width: 1em; }
|
||||
.svg-inline--fa.fa-w-17 {
|
||||
width: 1.0625em; }
|
||||
.svg-inline--fa.fa-w-18 {
|
||||
width: 1.125em; }
|
||||
.svg-inline--fa.fa-w-19 {
|
||||
width: 1.1875em; }
|
||||
.svg-inline--fa.fa-w-20 {
|
||||
width: 1.25em; }
|
||||
.svg-inline--fa.fa-pull-left {
|
||||
margin-right: .3em;
|
||||
width: auto; }
|
||||
.svg-inline--fa.fa-pull-right {
|
||||
margin-left: .3em;
|
||||
width: auto; }
|
||||
.svg-inline--fa.fa-border {
|
||||
height: 1.5em; }
|
||||
.svg-inline--fa.fa-li {
|
||||
width: 2em; }
|
||||
.svg-inline--fa.fa-fw {
|
||||
width: 1.25em; }
|
||||
|
||||
.fa-layers svg.svg-inline--fa {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0; }
|
||||
|
||||
.fa-layers {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: -.125em;
|
||||
width: 1em; }
|
||||
.fa-layers svg.svg-inline--fa {
|
||||
-webkit-transform-origin: center center;
|
||||
transform-origin: center center; }
|
||||
|
||||
.fa-layers-text, .fa-layers-counter {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
text-align: center; }
|
||||
|
||||
.fa-layers-text {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-transform-origin: center center;
|
||||
transform-origin: center center; }
|
||||
|
||||
.fa-layers-counter {
|
||||
background-color: #ff253a;
|
||||
border-radius: 1em;
|
||||
color: #fff;
|
||||
height: 1.5em;
|
||||
line-height: 1;
|
||||
max-width: 5em;
|
||||
min-width: 1.5em;
|
||||
overflow: hidden;
|
||||
padding: .25em;
|
||||
right: 0;
|
||||
text-overflow: ellipsis;
|
||||
top: 0;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: top right;
|
||||
transform-origin: top right; }
|
||||
|
||||
.fa-layers-bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: bottom right;
|
||||
transform-origin: bottom right; }
|
||||
|
||||
.fa-layers-bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: auto;
|
||||
top: auto;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: bottom left;
|
||||
transform-origin: bottom left; }
|
||||
|
||||
.fa-layers-top-right {
|
||||
right: 0;
|
||||
top: 0;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: top right;
|
||||
transform-origin: top right; }
|
||||
|
||||
.fa-layers-top-left {
|
||||
left: 0;
|
||||
right: auto;
|
||||
top: 0;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: top left;
|
||||
transform-origin: top left; }
|
||||
|
||||
.fa-lg {
|
||||
font-size: 1.33333em;
|
||||
line-height: 0.75em;
|
||||
vertical-align: -.0667em; }
|
||||
|
||||
.fa-xs {
|
||||
font-size: .75em; }
|
||||
|
||||
.fa-sm {
|
||||
font-size: .875em; }
|
||||
|
||||
.fa-1x {
|
||||
font-size: 1em; }
|
||||
|
||||
.fa-2x {
|
||||
font-size: 2em; }
|
||||
|
||||
.fa-3x {
|
||||
font-size: 3em; }
|
||||
|
||||
.fa-4x {
|
||||
font-size: 4em; }
|
||||
|
||||
.fa-5x {
|
||||
font-size: 5em; }
|
||||
|
||||
.fa-6x {
|
||||
font-size: 6em; }
|
||||
|
||||
.fa-7x {
|
||||
font-size: 7em; }
|
||||
|
||||
.fa-8x {
|
||||
font-size: 8em; }
|
||||
|
||||
.fa-9x {
|
||||
font-size: 9em; }
|
||||
|
||||
.fa-10x {
|
||||
font-size: 10em; }
|
||||
|
||||
.fa-fw {
|
||||
text-align: center;
|
||||
width: 1.25em; }
|
||||
|
||||
.fa-ul {
|
||||
list-style-type: none;
|
||||
margin-left: 2.5em;
|
||||
padding-left: 0; }
|
||||
.fa-ul > li {
|
||||
position: relative; }
|
||||
|
||||
.fa-li {
|
||||
left: -2em;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
line-height: inherit; }
|
||||
|
||||
.fa-border {
|
||||
border: solid 0.08em #eee;
|
||||
border-radius: .1em;
|
||||
padding: .2em .25em .15em; }
|
||||
|
||||
.fa-pull-left {
|
||||
float: left; }
|
||||
|
||||
.fa-pull-right {
|
||||
float: right; }
|
||||
|
||||
.fa.fa-pull-left,
|
||||
.fas.fa-pull-left,
|
||||
.far.fa-pull-left,
|
||||
.fal.fa-pull-left,
|
||||
.fab.fa-pull-left {
|
||||
margin-right: .3em; }
|
||||
|
||||
.fa.fa-pull-right,
|
||||
.fas.fa-pull-right,
|
||||
.far.fa-pull-right,
|
||||
.fal.fa-pull-right,
|
||||
.fab.fa-pull-right {
|
||||
margin-left: .3em; }
|
||||
|
||||
.fa-spin {
|
||||
-webkit-animation: fa-spin 2s infinite linear;
|
||||
animation: fa-spin 2s infinite linear; }
|
||||
|
||||
.fa-pulse {
|
||||
-webkit-animation: fa-spin 1s infinite steps(8);
|
||||
animation: fa-spin 1s infinite steps(8); }
|
||||
|
||||
@-webkit-keyframes fa-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
@keyframes fa-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
.fa-rotate-90 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg); }
|
||||
|
||||
.fa-rotate-180 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg); }
|
||||
|
||||
.fa-rotate-270 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg); }
|
||||
|
||||
.fa-flip-horizontal {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
|
||||
-webkit-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1); }
|
||||
|
||||
.fa-flip-vertical {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
|
||||
-webkit-transform: scale(1, -1);
|
||||
transform: scale(1, -1); }
|
||||
|
||||
.fa-flip-horizontal.fa-flip-vertical {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
|
||||
-webkit-transform: scale(-1, -1);
|
||||
transform: scale(-1, -1); }
|
||||
|
||||
:root .fa-rotate-90,
|
||||
:root .fa-rotate-180,
|
||||
:root .fa-rotate-270,
|
||||
:root .fa-flip-horizontal,
|
||||
:root .fa-flip-vertical {
|
||||
-webkit-filter: none;
|
||||
filter: none; }
|
||||
|
||||
.fa-stack {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
width: 2em; }
|
||||
|
||||
.fa-stack-1x,
|
||||
.fa-stack-2x {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0; }
|
||||
|
||||
.svg-inline--fa.fa-stack-1x {
|
||||
height: 1em;
|
||||
width: 1em; }
|
||||
|
||||
.svg-inline--fa.fa-stack-2x {
|
||||
height: 2em;
|
||||
width: 2em; }
|
||||
|
||||
.fa-inverse {
|
||||
color: #fff; }
|
||||
|
||||
.sr-only {
|
||||
border: 0;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px; }
|
||||
|
||||
.sr-only-focusable:active, .sr-only-focusable:focus {
|
||||
clip: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
width: auto; }
|
||||
5
assets/fontawesome/fontawesome-all.min.js
vendored
5
assets/fontawesome/fontawesome-all.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #333;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-subst {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-literal,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag .hljs-attr {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-doctag {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-selector-id {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-type,
|
||||
.hljs-class .hljs-title {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-attribute {
|
||||
color: #000080;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-link {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-meta {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
10253
assets/jquery/jquery-3.2.1.js
vendored
10253
assets/jquery/jquery-3.2.1.js
vendored
File diff suppressed because it is too large
Load Diff
4
assets/jquery/jquery-3.2.1.min.js
vendored
4
assets/jquery/jquery-3.2.1.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,285 +0,0 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.page-wrap {
|
||||
min-height: 100%;
|
||||
margin-bottom: -120px;
|
||||
}
|
||||
|
||||
.page-push,
|
||||
.footer {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.icon.infobox {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.notification.is-top {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.modal-card-error {
|
||||
background-color: #ff3860;
|
||||
color: #fff;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
.is-vertical-marginless {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.is-vertical-paddingless {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-item.is-narrow {
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem
|
||||
}
|
||||
|
||||
.progress.is-narrow:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.is-divider-vertical {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hero.is-mini .hero-body {
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
fieldset:not(:last-child) {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.lead {
|
||||
width: 100%;
|
||||
margin-bottom: 10px !important;
|
||||
font-style: italic;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.lead.is-bordered {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.lead.is-1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.lead.is-2 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.lead.is-3 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.lead.is-4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.lead.is-5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.lead.is-6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lead.is-7 {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
dt,
|
||||
dd {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.is-horizontal dt {
|
||||
float: left;
|
||||
width: 160px;
|
||||
clear: left;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.is-horizontal dd {
|
||||
margin-left: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.is-vertical-middle {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.has-bg-white-ter {
|
||||
background-color: whitesmoke !important;
|
||||
}
|
||||
|
||||
.is-bottom-marginless {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.is-bottom-paddingless {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.tag:not(body).is-grey {
|
||||
background-color: #7a7a7a;
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
.card, .box {
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.card-header-title {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header-title, .card-header-icon, .card-footer-item {
|
||||
padding: 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.level.is-mobile .level-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.block {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.block:not(:last-child) {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.block .block-header {
|
||||
background-color: whitesmoke;
|
||||
border: 1px solid lightgray;
|
||||
border-bottom: none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
padding: 0.5em 0.75em;
|
||||
}
|
||||
|
||||
.block .block-body {
|
||||
padding: 1em 1.25em;
|
||||
}
|
||||
|
||||
.block .block-body.is-bordered {
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
||||
.columns.is-tight {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.columns.is-tight > .column {
|
||||
margin: 0;
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
.columns.is-tight:not(:last-child) {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.columns.is-tight:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 5px;
|
||||
background-color: whitesmoke;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tabs-content:not(:last-child) {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.has-no-border {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.has-no-top-border {
|
||||
border-top: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
textarea.code {
|
||||
padding: 0.75em;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 0.8em;
|
||||
border: 1px solid #ddd;
|
||||
height: 450px !important;
|
||||
}
|
||||
|
||||
.drag {
|
||||
cursor: move;
|
||||
padding: 0.5rem 0.25rem;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.6 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,30 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Metric {
|
||||
import EditTable = Swirl.Core.EditTable;
|
||||
|
||||
class MetricTable extends EditTable {
|
||||
protected render(): string {
|
||||
return `<tr>
|
||||
<td>
|
||||
<input name="metrics[${this.index}].legend" class="input is-small" placeholder="Legend expression for dataset, e.g. ${name}">
|
||||
</td>
|
||||
<td>
|
||||
<input name="metrics[${this.index}].query" class="input is-small" placeholder="Prometheus query expression, for service dashboard, you can use '$\{service\}' variable">
|
||||
</td>
|
||||
<td>
|
||||
<a class="button is-small is-outlined is-danger" data-action="delete-metric">
|
||||
<span class="icon is-small">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class EditPage {
|
||||
constructor() {
|
||||
new MetricTable("#table-metrics");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Metric {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
import FilterBox = Swirl.Core.FilterBox;
|
||||
|
||||
export class ListPage {
|
||||
private fb: FilterBox;
|
||||
private $charts: JQuery;
|
||||
|
||||
constructor() {
|
||||
this.$charts = $("#div-charts").children();
|
||||
this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
|
||||
|
||||
// bind events
|
||||
$("#btn-import").click(this.importChart);
|
||||
Dispatcher.bind("#div-charts")
|
||||
.on("export-chart", this.exportChart.bind(this))
|
||||
.on("delete-chart", this.deleteChart.bind(this));
|
||||
}
|
||||
|
||||
private deleteChart(e: JQueryEventObject) {
|
||||
let $container = $(e.target).closest("div.column");
|
||||
let name = $container.data("name");
|
||||
Modal.confirm(`Are you sure to delete chart: <strong>${name}</strong>?`, "Delete chart", (dlg, e) => {
|
||||
$ajax.post(name + "/delete").trigger(e.target).json<AjaxResult>(r => {
|
||||
$container.remove();
|
||||
dlg.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private filterCharts(text: string) {
|
||||
if (!text) {
|
||||
this.$charts.show();
|
||||
return;
|
||||
}
|
||||
|
||||
this.$charts.each((i, elem) => {
|
||||
let $elem = $(elem),
|
||||
texts: string[] = [
|
||||
$elem.data("name").toLowerCase(),
|
||||
$elem.data("title").toLowerCase(),
|
||||
$elem.data("desc").toLowerCase(),
|
||||
];
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
let index = texts[i].indexOf(text);
|
||||
if (index >= 0) {
|
||||
$elem.show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
$elem.hide();
|
||||
})
|
||||
}
|
||||
|
||||
private exportChart(e: JQueryEventObject) {
|
||||
let $container = $(e.target).closest("div.column");
|
||||
let name = $container.data("name");
|
||||
$ajax.get(name + "/detail").text(r => {
|
||||
Modal.alert(`<textarea class="textarea" rows="8" readonly>${r}</textarea>`, "Export chart");
|
||||
});
|
||||
}
|
||||
|
||||
private importChart(e: JQueryEventObject) {
|
||||
Modal.confirm(`<textarea class="textarea" rows="8"></textarea>`, "Import chart", (dlg, e) => {
|
||||
try {
|
||||
let chart = JSON.parse(dlg.find('textarea').val());
|
||||
$ajax.post("new", chart).trigger(e.target).json<AjaxResult>(r => {
|
||||
if (r.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
dlg.error(r.message);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
dlg.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Config {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Table = Swirl.Core.ListTable;
|
||||
|
||||
export class ListPage {
|
||||
private table: Table;
|
||||
|
||||
constructor() {
|
||||
this.table = new Table("#table-items");
|
||||
|
||||
// bind events
|
||||
this.table.on("delete-config", this.deleteConfig.bind(this));
|
||||
$("#btn-delete").click(this.deleteConfigs.bind(this));
|
||||
}
|
||||
|
||||
private deleteConfig(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:eq(1)").text().trim();
|
||||
let id = $tr.find(":checkbox:first").val();
|
||||
Modal.confirm(`Are you sure to remove config: <strong>${name}</strong>?`, "Delete config", (dlg, e) => {
|
||||
$ajax.post("delete", {ids: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private deleteConfigs(e: JQueryEventObject) {
|
||||
let ids = this.table.selectedKeys();
|
||||
if (ids.length == 0) {
|
||||
Modal.alert("Please select one or more items.");
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm(`Are you sure to remove ${ids.length} configs?`, "Delete configs", (dlg, e) => {
|
||||
$ajax.post("delete", {ids: ids.join(",")}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
this.table.selectedRows().remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Config {
|
||||
import OptionTable = Swirl.Core.OptionTable;
|
||||
|
||||
export class NewPage {
|
||||
constructor() {
|
||||
new OptionTable("#table-labels");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Container {
|
||||
export class ExecPage {
|
||||
private $cmd: JQuery;
|
||||
private $connect: JQuery;
|
||||
private $disconnect: JQuery;
|
||||
private ws: WebSocket;
|
||||
private term: Terminal;
|
||||
|
||||
constructor() {
|
||||
this.$cmd = $("#txt-cmd");
|
||||
this.$connect = $("#btn-connect");
|
||||
this.$disconnect = $("#btn-disconnect");
|
||||
|
||||
this.$connect.click(this.connect.bind(this));
|
||||
this.$disconnect.click(this.disconnect.bind(this));
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
}
|
||||
|
||||
private connect(e: JQueryEventObject) {
|
||||
this.$connect.hide();
|
||||
this.$disconnect.show();
|
||||
|
||||
let url = location.host + location.pathname.substring(0, location.pathname.lastIndexOf("/")) + "/connect?cmd=" + encodeURIComponent(this.$cmd.val());
|
||||
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
||||
let ws = new WebSocket(protocol + url);
|
||||
ws.onopen = () => {
|
||||
this.term = new Terminal();
|
||||
this.term.on('data', (data: any) => {
|
||||
if (ws.readyState == WebSocket.OPEN) {
|
||||
ws.send(data);
|
||||
}
|
||||
});
|
||||
this.term.open(document.getElementById('terminal-container'));
|
||||
this.term.focus();
|
||||
let width = Math.floor(($('#terminal-container').width() - 20) / 8.39);
|
||||
let height = 30;
|
||||
this.term.resize(width, height);
|
||||
this.term.setOption('cursorBlink', true);
|
||||
this.term.fit();
|
||||
|
||||
window.onresize = () => {
|
||||
this.term.fit();
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
this.term.write(e.data);
|
||||
};
|
||||
ws.onerror = function (error) {
|
||||
console.log("error: " + error);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.log("close");
|
||||
};
|
||||
};
|
||||
this.ws = ws;
|
||||
}
|
||||
|
||||
private disconnect(e: JQueryEventObject) {
|
||||
if (this.ws && this.ws.readyState != WebSocket.CLOSED) {
|
||||
this.ws.close();
|
||||
}
|
||||
if (this.term) {
|
||||
this.term.destroy();
|
||||
this.term = null;
|
||||
}
|
||||
this.$connect.show();
|
||||
this.$disconnect.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Container {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Table = Swirl.Core.ListTable;
|
||||
|
||||
export class ListPage {
|
||||
private table: Table;
|
||||
|
||||
constructor() {
|
||||
this.table = new Table("#table-items");
|
||||
|
||||
// bind events
|
||||
this.table.on("delete-container", this.deleteContainer.bind(this));
|
||||
$("#btn-delete").click(this.deleteContainers.bind(this));
|
||||
}
|
||||
|
||||
private deleteContainer(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:eq(1)").text().trim();
|
||||
let id = $tr.find(":checkbox:first").val();
|
||||
Modal.confirm(`Are you sure to remove container: <strong>${name}</strong>?`, "Delete container", (dlg, e) => {
|
||||
$ajax.post("delete", { ids: id }).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private deleteContainers() {
|
||||
let ids = this.table.selectedKeys();
|
||||
if (ids.length == 0) {
|
||||
Modal.alert("Please select one or more items.");
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm(`Are you sure to remove ${ids.length} containers?`, "Delete containers", (dlg, e) => {
|
||||
$ajax.post("delete", { ids: ids.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
this.table.selectedRows().remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
/*!
|
||||
* Swirl Ajax Library v1.0.0
|
||||
* Copyright 2017 cuigh. All rights reserved.
|
||||
*
|
||||
* @author cuigh(noname@live.com)
|
||||
*/
|
||||
///<reference path="bulma.ts"/>
|
||||
namespace Swirl.Core {
|
||||
export type AjaxErrorHandler = (xhr: JQueryXHR, textStatus: string, error: string) => void;
|
||||
|
||||
/**
|
||||
* AjaxResult
|
||||
*/
|
||||
export class AjaxResult {
|
||||
success: boolean;
|
||||
code?: number;
|
||||
message?: string;
|
||||
data?: any;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AjaxOptions
|
||||
*/
|
||||
export class AjaxOptions {
|
||||
private static defaultOptions: AjaxOptions = new AjaxOptions();
|
||||
/**
|
||||
* request url
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* request method, GET/POST...
|
||||
*/
|
||||
method: AjaxMethod;
|
||||
/**
|
||||
* request data
|
||||
*/
|
||||
data?: Object;
|
||||
/**
|
||||
* request timeout time(ms)
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
timeout?: number = 30000;
|
||||
/**
|
||||
* send request by asynchronous
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
async?: boolean = true;
|
||||
/**
|
||||
* response data type
|
||||
*/
|
||||
dataType?: "text" | "html" | "json" | "jsonp" | "xml" | "script" | string;
|
||||
/**
|
||||
* AJAX trigger element for indicator
|
||||
*
|
||||
* @type {(Element | JQuery)}
|
||||
*/
|
||||
trigger?: Element | JQuery;
|
||||
/**
|
||||
* data encoder for POST request
|
||||
*/
|
||||
encoder: "none" | "form" | "json" = "json";
|
||||
/**
|
||||
* previous filter
|
||||
*/
|
||||
preHandler: (options: AjaxOptions) => void;
|
||||
/**
|
||||
* post filter
|
||||
*/
|
||||
postHandler: (options: AjaxOptions) => void;
|
||||
/**
|
||||
* error handler
|
||||
*/
|
||||
errorHandler: AjaxErrorHandler;
|
||||
|
||||
/**
|
||||
* get default options
|
||||
*
|
||||
* @returns {AjaxOptions}
|
||||
*/
|
||||
static getDefaultOptions(): AjaxOptions {
|
||||
return AjaxOptions.defaultOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* set default options
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
static setDefaultOptions(options: AjaxOptions) {
|
||||
if (options) {
|
||||
AjaxOptions.defaultOptions = options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX request method
|
||||
*/
|
||||
export enum AjaxMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
HEAD,
|
||||
TRACE,
|
||||
OPTIONS,
|
||||
CONNECT,
|
||||
PATCH
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX Request
|
||||
*/
|
||||
export class AjaxRequest {
|
||||
static preHandler: (options: AjaxOptions) => void = options => {
|
||||
options.trigger && $(options.trigger).prop("disabled", true);
|
||||
};
|
||||
static postHandler: (options: AjaxOptions) => void = options => {
|
||||
options.trigger && $(options.trigger).prop("disabled", false);
|
||||
};
|
||||
static errorHandler: (xhr: JQueryXHR, textStatus: string, error: string) => void = (xhr, status, err) => {
|
||||
let msg: string;
|
||||
if (xhr.responseJSON) {
|
||||
// auxo web framework return: {code: 0, message: "xxx"}
|
||||
let err = xhr.responseJSON;
|
||||
msg = err.message;
|
||||
if (err.code) {
|
||||
msg += `(${err.code})`
|
||||
}
|
||||
} else if (xhr.status >= 400) {
|
||||
msg = xhr.responseText || err || status;
|
||||
} else {
|
||||
return
|
||||
}
|
||||
Notification.show("danger", `AJAX: ${msg}`)
|
||||
};
|
||||
protected options: AjaxOptions;
|
||||
|
||||
constructor(url: string, method: AjaxMethod, data?: any) {
|
||||
this.options = $.extend({
|
||||
url: url,
|
||||
method: method,
|
||||
data: data,
|
||||
preHandler: AjaxRequest.preHandler,
|
||||
postHandler: AjaxRequest.postHandler,
|
||||
errorHandler: AjaxRequest.errorHandler,
|
||||
}, AjaxOptions.getDefaultOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* set pre handler
|
||||
*
|
||||
* @param handler
|
||||
* @return {AjaxRequest}
|
||||
*/
|
||||
preHandler(handler: (options: AjaxOptions) => void): this {
|
||||
this.options.preHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set post handler
|
||||
*
|
||||
* @param handler
|
||||
* @return {AjaxRequest}
|
||||
*/
|
||||
postHandler(handler: (options: AjaxOptions) => void): this {
|
||||
this.options.postHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set error handler
|
||||
*
|
||||
* @param handler
|
||||
* @return {AjaxRequest}
|
||||
*/
|
||||
errorHandler(handler: AjaxErrorHandler): this {
|
||||
this.options.errorHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set request timeout
|
||||
*
|
||||
* @param timeout
|
||||
* @returns {AjaxRequest}
|
||||
*/
|
||||
timeout(timeout: number): this {
|
||||
this.options.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set async option
|
||||
*
|
||||
* @param async
|
||||
* @returns {AjaxRequest}
|
||||
*/
|
||||
async(async: boolean): this {
|
||||
this.options.async = async;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set trigger element
|
||||
*
|
||||
* @param {(Element | JQuery)} elem
|
||||
* @returns {AjaxRequest}
|
||||
*/
|
||||
trigger(elem: Element | JQuery): this {
|
||||
this.options.trigger = elem;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* get response as JSON
|
||||
*
|
||||
* @template T JSON data type
|
||||
* @param {(r: T) => void} [callback] callback function
|
||||
*/
|
||||
json<T>(callback?: (r: T) => void): void | Promise<T> {
|
||||
return this.result<T>("json", callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* get response as text
|
||||
*
|
||||
* @param {(r: string) => void} [callback] callback function
|
||||
*/
|
||||
text(callback?: (r: string) => void): void | Promise<string> {
|
||||
return this.result<string>("text", callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* get response as HTML
|
||||
*
|
||||
* @param {(r: string) => void} [callback] callback function
|
||||
*/
|
||||
html(callback?: (r: string) => void): void | Promise<string> {
|
||||
return this.result<string>("html", callback);
|
||||
}
|
||||
|
||||
protected result<T>(dataType: string, callback?: (r: T) => void): void | Promise<T> {
|
||||
this.options.dataType = dataType;
|
||||
this.options.preHandler && this.options.preHandler(this.options);
|
||||
let settings = this.buildSettings();
|
||||
if (callback) {
|
||||
$.ajax(settings).done(callback).always(() => {
|
||||
this.options.postHandler && this.options.postHandler(this.options);
|
||||
}).fail((xhr: JQueryXHR, textStatus: string, error: string) => {
|
||||
this.options.errorHandler && this.options.errorHandler(xhr, textStatus, error);
|
||||
});
|
||||
} else {
|
||||
return new Promise<T>((resolve, _) => {
|
||||
$.ajax(settings).done((r: T) => {
|
||||
resolve(r);
|
||||
}).always(() => {
|
||||
AjaxRequest.postHandler && AjaxRequest.postHandler(this.options);
|
||||
}).fail((xhr: JQueryXHR, textStatus: string, error: string) => {
|
||||
AjaxRequest.errorHandler && AjaxRequest.errorHandler(xhr, textStatus, error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected buildSettings(): JQueryAjaxSettings {
|
||||
return {
|
||||
url: this.options.url,
|
||||
method: AjaxMethod[this.options.method],
|
||||
data: this.options.data,
|
||||
dataType: this.options.dataType,
|
||||
timeout: this.options.timeout,
|
||||
async: this.options.async,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX GET Request
|
||||
*/
|
||||
export class AjaxGetRequest extends AjaxRequest {
|
||||
/**
|
||||
* get JSON response by jsonp
|
||||
*
|
||||
* @template T JSON data type
|
||||
* @param {(r: T) => void} [callback] callback function
|
||||
*/
|
||||
jsonp<T>(callback?: (r: T) => void): void | Promise<T> {
|
||||
return this.result<T>("jsonp", callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX POST Request
|
||||
*/
|
||||
export class AjaxPostRequest extends AjaxRequest {
|
||||
/**
|
||||
* set encoder
|
||||
*
|
||||
* @param encoder
|
||||
* @returns {AjaxPostRequest}
|
||||
*/
|
||||
encoder(encoder: "none" | "form" | "json"): this {
|
||||
this.options.encoder = encoder;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected buildSettings(): JQueryAjaxSettings {
|
||||
let settings = super.buildSettings();
|
||||
switch (this.options.encoder) {
|
||||
case "none":
|
||||
settings.contentType = false;
|
||||
settings.processData = false;
|
||||
break;
|
||||
case "json":
|
||||
settings.contentType = "application/json; charset=UTF-8";
|
||||
settings.data = JSON.stringify(this.options.data);
|
||||
break;
|
||||
case "form":
|
||||
settings.contentType = "application/x-www-form-urlencoded; charset=UTF-8";
|
||||
break;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX static entry class
|
||||
*/
|
||||
export class Ajax {
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send GET request
|
||||
*
|
||||
* @static
|
||||
* @param {string} url request url
|
||||
* @param {Object} [args] request data
|
||||
* @returns {Ajax} Ajax request instance
|
||||
*/
|
||||
static get(url: string, args?: Object): AjaxGetRequest {
|
||||
return new AjaxGetRequest(url, AjaxMethod.GET, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send POST request
|
||||
*
|
||||
* @static
|
||||
* @param {string} url request url
|
||||
* @param {(string | Object)} [data] request data
|
||||
* @returns {Ajax} Ajax request instance
|
||||
*/
|
||||
static post(url: string, data?: string | Object): AjaxPostRequest {
|
||||
return new AjaxPostRequest(url, AjaxMethod.POST, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX interface(仅用于实现 $ajax 快捷对象)
|
||||
*/
|
||||
export interface AjaxStatic {
|
||||
/**
|
||||
* Send GET request
|
||||
*
|
||||
* @static
|
||||
* @param {string} url request url
|
||||
* @param {Object} [args] request data
|
||||
* @returns {Ajax} Ajax request instance
|
||||
*/
|
||||
get(url: string, args?: Object): AjaxGetRequest;
|
||||
/**
|
||||
* Send POST request
|
||||
*
|
||||
* @static
|
||||
* @param {string} url request url
|
||||
* @param {(string | Object)} [data] request data
|
||||
* @returns {Ajax} Ajax request instance
|
||||
*/
|
||||
post(url: string, data?: string | Object): AjaxPostRequest;
|
||||
}
|
||||
}
|
||||
|
||||
let $ajax: Swirl.Core.AjaxStatic = Swirl.Core.Ajax;
|
||||
@@ -1,227 +0,0 @@
|
||||
namespace Swirl.Core {
|
||||
export class Modal {
|
||||
private static initialized: boolean;
|
||||
private static active: Modal;
|
||||
private $modal: JQuery;
|
||||
private deletable: boolean;
|
||||
|
||||
constructor(modal: string | Element | JQuery) {
|
||||
this.$modal = $(modal);
|
||||
this.find(".modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .dismiss").click(e => this.close());
|
||||
}
|
||||
|
||||
static initialize() {
|
||||
if (!Modal.initialized) {
|
||||
$('.modal-trigger').click(function () {
|
||||
let target = $(this).data('target');
|
||||
let dlg = new Modal("#" + target)
|
||||
dlg.show();
|
||||
});
|
||||
$(document).on('keyup', function (e) {
|
||||
// ESC Key
|
||||
if (e.keyCode === 27) {
|
||||
Modal.active && Modal.active.close();
|
||||
}
|
||||
});
|
||||
Modal.initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
static close() {
|
||||
Modal.active && Modal.active.close();
|
||||
}
|
||||
|
||||
static current(): Modal {
|
||||
return Modal.active;
|
||||
}
|
||||
|
||||
static alert(content: string, title?: string, callback?: (dlg: Modal, e: JQueryEventObject) => void): Modal {
|
||||
title = title || "Prompt";
|
||||
let $elem = $(`<div class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">${title}</p>
|
||||
<button class="delete"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">${content}</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-primary">OK</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>`).appendTo("body");
|
||||
let dlg = new Modal($elem);
|
||||
callback = callback || function (dlg: Modal, e: JQueryEventObject) { dlg.close(); };
|
||||
$elem.find(".modal-card-foot>button:first").click(e => callback(dlg, e));
|
||||
dlg.deletable = true;
|
||||
dlg.show();
|
||||
return dlg;
|
||||
}
|
||||
|
||||
static confirm(content: string, title?: string, callback?: (dlg: Modal, e: JQueryEventObject) => void): Modal {
|
||||
title = title || "Confirm";
|
||||
let $elem = $(`<div class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">${title}</p>
|
||||
<button class="delete"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">${content}</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-primary">Confirm</button>
|
||||
<button class="button dismiss">Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>`).appendTo("body");
|
||||
let dlg = new Modal($elem);
|
||||
if (callback) {
|
||||
$elem.find(".modal-card-foot>button:first").click(e => callback(dlg, e));
|
||||
}
|
||||
dlg.deletable = true;
|
||||
dlg.show();
|
||||
return dlg;
|
||||
}
|
||||
|
||||
show() {
|
||||
Modal.active && Modal.active.close();
|
||||
|
||||
$('html').addClass('is-clipped');
|
||||
this.$modal.addClass('is-active').focus();
|
||||
this.error();
|
||||
|
||||
Modal.active = this;
|
||||
}
|
||||
|
||||
close() {
|
||||
$('html').removeClass('is-clipped');
|
||||
if (this.deletable) {
|
||||
this.$modal.remove();
|
||||
} else {
|
||||
this.$modal.removeClass('is-active');
|
||||
}
|
||||
Modal.active = null;
|
||||
}
|
||||
|
||||
error(msg?: string) {
|
||||
let $error = this.find(".modal-card-error");
|
||||
if (msg) {
|
||||
if (!$error.length) {
|
||||
$error = $('<section class="modal-card-error"></section>').insertAfter(this.find(".modal-card-body"));
|
||||
}
|
||||
$error.html(msg).show();
|
||||
} else {
|
||||
$error.hide();
|
||||
}
|
||||
}
|
||||
|
||||
find(selector: string | Element | JQuery): JQuery {
|
||||
if (typeof selector === "string") {
|
||||
return this.$modal.find(selector);
|
||||
} else if (selector instanceof Element) {
|
||||
return this.$modal.find(selector);
|
||||
}
|
||||
return this.$modal.find(selector);
|
||||
}
|
||||
}
|
||||
|
||||
export type NotificationType = "primary" | "info" | "success" | "warning" | "danger";
|
||||
|
||||
export interface NotificationOptions {
|
||||
type: NotificationType;
|
||||
time: number; // display time(in seconds), 0 is always hide
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class Notification {
|
||||
private $elem: JQuery;
|
||||
private options: NotificationOptions;
|
||||
|
||||
constructor(options: NotificationOptions) {
|
||||
this.options = $.extend({}, {
|
||||
type: "primary",
|
||||
time: 5,
|
||||
}, options);
|
||||
}
|
||||
|
||||
static show(type: NotificationType, msg: string, time?: number): Notification {
|
||||
let n = new Notification({
|
||||
type: type,
|
||||
message: msg,
|
||||
time: time,
|
||||
});
|
||||
n.show();
|
||||
return n;
|
||||
}
|
||||
|
||||
private show() {
|
||||
this.$elem = $(`<div class="notification is-${this.options.type} has-text-centered is-marginless is-radiusless is-top" style="display:none">${this.options.message}</div>`).appendTo("body").fadeIn(250);
|
||||
if (this.options.time > 0) {
|
||||
setTimeout(() => this.hide(), this.options.time * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$elem.fadeTo("slow", 0.01, () => this.$elem.remove());
|
||||
}
|
||||
}
|
||||
|
||||
export class Tab {
|
||||
private static initialized: boolean;
|
||||
private $tab: JQuery;
|
||||
private $content: JQuery;
|
||||
private active: number;
|
||||
|
||||
constructor(tab: string | Element | JQuery, content: string | Element | JQuery) {
|
||||
this.$tab = $(tab);
|
||||
this.$content = $(content);
|
||||
this.active = this.$tab.find("li.is-active").index();
|
||||
if (this.active == -1) {
|
||||
this.select(0);
|
||||
}
|
||||
|
||||
this.$tab.find("li").click(e => {
|
||||
let $li = $(e.target).closest("li");
|
||||
if (!$li.hasClass("is-active")) {
|
||||
this.select($li.index());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static initialize() {
|
||||
if (!Tab.initialized) {
|
||||
$('.tabs').each((i, elem) => {
|
||||
let target = $(elem).data("target");
|
||||
new Tab(elem, "#" + target);
|
||||
});
|
||||
Tab.initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
select(index: number) {
|
||||
if (this.active != index) {
|
||||
this.$tab.find(`li.is-active`).removeClass("is-active");
|
||||
this.$tab.find(`li:eq(${index})`).addClass("is-active");
|
||||
this.$content.children(":visible").hide();
|
||||
$(this.$content.children()[index]).show();
|
||||
this.active = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FilterBox {
|
||||
private $elem: JQuery;
|
||||
private timer: number;
|
||||
|
||||
constructor(elem: string | Element | JQuery, callback: (text: string) => void, timeout?: number) {
|
||||
this.$elem = $(elem);
|
||||
this.$elem.keyup(() => {
|
||||
if (this.timer > 0) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
let text: string = this.$elem.val().toLowerCase();
|
||||
this.timer = setTimeout(() => callback(text), timeout || 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,612 +0,0 @@
|
||||
namespace Swirl.Core {
|
||||
export class ChartOptions {
|
||||
name: string;
|
||||
title: string;
|
||||
type?: string = "line";
|
||||
unit?: string;
|
||||
width?: number = 12;
|
||||
height?: number = 200;
|
||||
colors?: string[];
|
||||
}
|
||||
|
||||
export abstract class Chart {
|
||||
protected $elem: JQuery;
|
||||
protected chart: echarts.ECharts;
|
||||
protected opts?: ChartOptions;
|
||||
|
||||
protected constructor(opts: ChartOptions) {
|
||||
this.opts = $.extend(new ChartOptions(), opts);
|
||||
this.createElem();
|
||||
}
|
||||
|
||||
private createElem() {
|
||||
this.$elem = $(`<div class="column is-${this.opts.width}" data-name="${this.opts.name}">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<a class="card-header-icon drag">
|
||||
<span class="icon">
|
||||
<i class="fas fa-bars has-text-grey-light" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
<p class="card-header-title is-paddingless">${this.opts.title}</p>
|
||||
<a data-action="remove-chart" class="card-header-icon" aria-label="remove chart">
|
||||
<span class="icon">
|
||||
<i class="far fa-trash-alt has-text-danger" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</header>
|
||||
<div class="card-content" style="height: ${this.opts.height}px"></div>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
init() {
|
||||
let opt = {
|
||||
legend: {
|
||||
x: 'right',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
// formatter: function (params) {
|
||||
// params = params[0];
|
||||
// var date = new Date(params.name);
|
||||
// return date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear() + ' : ' + params.value[1];
|
||||
// },
|
||||
axisPointer: {
|
||||
animation: false
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
splitNumber: 10,
|
||||
splitLine: {show: false},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
// boundaryGap: [0, '100%'],
|
||||
splitLine: {
|
||||
// show: false,
|
||||
lineStyle: {
|
||||
type: "dashed",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: this.formatValue.bind(this),
|
||||
},
|
||||
},
|
||||
color: this.getColors(),
|
||||
};
|
||||
this.config(opt);
|
||||
this.chart = echarts.init(<HTMLDivElement>this.$elem.find("div.card-content").get(0));
|
||||
this.chart.setOption(opt, true);
|
||||
}
|
||||
|
||||
getElem(): JQuery {
|
||||
return this.$elem;
|
||||
}
|
||||
|
||||
getOptions(): ChartOptions {
|
||||
return this.opts;
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.chart.resize();
|
||||
}
|
||||
|
||||
abstract setData(d: any): void;
|
||||
|
||||
protected config(opt: echarts.EChartOption) {
|
||||
}
|
||||
|
||||
protected formatValue(value: number): string {
|
||||
if (value >= 0) {
|
||||
return this.formatPositiveValue(value)
|
||||
} else {
|
||||
return "-" + this.formatPositiveValue(-value);
|
||||
}
|
||||
}
|
||||
|
||||
private formatPositiveValue(value: number): string {
|
||||
switch (this.opts.unit) {
|
||||
case "percent:100":
|
||||
return value.toFixed(1) + "%";
|
||||
case "percent:1":
|
||||
return (value * 100).toFixed(1) + "%";
|
||||
case "time:ns":
|
||||
return value + 'ns';
|
||||
case "time:µs":
|
||||
return value.toFixed(2) + 'µs';
|
||||
case "time:ms":
|
||||
return value.toFixed(2) + 'ms';
|
||||
case "time:s":
|
||||
if (value < 1) { // 1
|
||||
return (value * 1000).toFixed(0) + 'ms';
|
||||
} else {
|
||||
return value.toFixed(2) + 's';
|
||||
}
|
||||
case "time:m":
|
||||
return value.toFixed(2) + 'm';
|
||||
case "time:h":
|
||||
return value.toFixed(2) + 'h';
|
||||
case "time:d":
|
||||
return value.toFixed(2) + 'd';
|
||||
case "size:bits":
|
||||
value = value / 8; // fall-through
|
||||
case "size:bytes":
|
||||
if (value < 1024) { // 1K
|
||||
return value.toString() + 'B';
|
||||
} else if (value < 1048576) { // 1M
|
||||
return (value / 1024).toFixed(2) + 'K';
|
||||
} else if (value < 1073741824) { // 1G
|
||||
return (value / 1048576).toFixed(2) + 'M';
|
||||
} else {
|
||||
return (value / 1073741824).toFixed(2) + 'G';
|
||||
}
|
||||
case "size:kilobytes":
|
||||
return value.toFixed(2) + 'K';
|
||||
case "size:megabytes":
|
||||
return value.toFixed(2) + 'M';
|
||||
case "size:gigabytes":
|
||||
return value.toFixed(2) + 'G';
|
||||
default:
|
||||
return value % 1 === 0 ? value.toString() : value.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
private getColors(): string[] {
|
||||
// ECharts default colors
|
||||
// let colors = ['#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae', '#749f83', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'];
|
||||
// let colors = [
|
||||
// '#C1232B', '#B5C334', '#FCCE10', '#E87C25', '#27727B',
|
||||
// '#FE8463', '#9BCA63', '#FAD860', '#F3A43B', '#60C0DD',
|
||||
// '#D7504B', '#C6E579', '#F4E001', '#F0805A', '#26C0C0',
|
||||
// ];
|
||||
let colors = [
|
||||
'#45aaf2',
|
||||
'#6574cd',
|
||||
'#a55eea',
|
||||
'#f66d9b',
|
||||
'#cd201f',
|
||||
'#fd9644',
|
||||
'#f1c40f',
|
||||
'#7bd235',
|
||||
'#5eba00',
|
||||
'#2bcbba',
|
||||
];
|
||||
this.shuffle(colors);
|
||||
return colors;
|
||||
}
|
||||
|
||||
private shuffle(a: any) {
|
||||
let len = a.length;
|
||||
for (let i = 0; i < len - 1; i++) {
|
||||
let index = Math.floor(Math.random() * (len - i));
|
||||
let temp = a[index];
|
||||
a[index] = a[len - i - 1];
|
||||
a[len - i - 1] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gauge chart
|
||||
*/
|
||||
export class GaugeChart extends Chart {
|
||||
constructor(opts: ChartOptions) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
protected config(opt: echarts.EChartOption) {
|
||||
$.extend(true, opt, {
|
||||
grid: {
|
||||
left: 0,
|
||||
top: 20,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
show: false,
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
show: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
setData(d: any): void {
|
||||
this.chart.setOption({
|
||||
series: [
|
||||
{
|
||||
// name: d.name,
|
||||
type: 'gauge',
|
||||
radius: '100%',
|
||||
center: ["50%", "50%"], // 仪表位置
|
||||
max: d.value,
|
||||
axisLine: {
|
||||
show: false,
|
||||
lineStyle: {width: 0, opacity: 0, shadowBlur: 0},
|
||||
},
|
||||
axisLabel: {show: false},
|
||||
axisTick: {show: false},
|
||||
splitLine: {show: false},
|
||||
pointer: {show: false},
|
||||
detail: {
|
||||
formatter: this.formatValue.bind(this),
|
||||
offsetCenter: [0, 0],
|
||||
fontSize: 64,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
data: [{value: d.value}]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pie chart.
|
||||
*/
|
||||
export class VectorChart extends Chart {
|
||||
constructor(opts: ChartOptions) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
protected config(opt: echarts.EChartOption) {
|
||||
$.extend(true, opt, {
|
||||
grid: {
|
||||
left: 20,
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any, index: number): string => {
|
||||
return params.name + ": " + this.formatValue(params.value);
|
||||
},
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
show: false,
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
show: false,
|
||||
},
|
||||
],
|
||||
series: [{
|
||||
type: this.opts.type,
|
||||
radius: '80%',
|
||||
center: ['40%', '50%'],
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
setData(d: any): void {
|
||||
this.chart.setOption({
|
||||
legend: {
|
||||
data: d.legend,
|
||||
},
|
||||
series: [{
|
||||
data: d.data,
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Line/Bar etc.
|
||||
*/
|
||||
export class MatrixChart extends Chart {
|
||||
constructor(opts: ChartOptions) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
protected config(opt: echarts.EChartOption) {
|
||||
$.extend(true, opt, {
|
||||
grid: {
|
||||
left: 60,
|
||||
top: 30,
|
||||
right: 20,
|
||||
bottom: 30,
|
||||
},
|
||||
tooltip: {
|
||||
formatter: (params: any) => {
|
||||
let html = params[0].axisValueLabel + '<br/>';
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
html += params[i].marker + params[i].seriesName + ': ' + this.formatValue(params[i].value[1]) + '<br/>';
|
||||
}
|
||||
return html;
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
// min: 'dataMin',
|
||||
max: 'dataMax',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setData(d: any): void {
|
||||
if (!d.series) {
|
||||
return;
|
||||
}
|
||||
|
||||
d.series.forEach((s: any) => {
|
||||
s.type = this.opts.type;
|
||||
s.areaStyle = {
|
||||
opacity: 0.3,
|
||||
};
|
||||
s.smooth = true;
|
||||
s.showSymbol = false;
|
||||
s.lineStyle = {
|
||||
normal: {
|
||||
width: 1,
|
||||
}
|
||||
};
|
||||
});
|
||||
this.chart.setOption({
|
||||
legend: {
|
||||
data: d.legend,
|
||||
},
|
||||
series: d.series,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ChartFactory {
|
||||
static create(opts: ChartOptions): Chart {
|
||||
switch (opts.type) {
|
||||
case "gauge":
|
||||
return new GaugeChart(opts);
|
||||
case "line":
|
||||
case "bar":
|
||||
return new MatrixChart(opts);
|
||||
case "pie":
|
||||
return new VectorChart(opts);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChartDashboardOptions {
|
||||
name: string;
|
||||
key?: string;
|
||||
period?: number = 30;
|
||||
refreshInterval?: number = 15; // ms
|
||||
}
|
||||
|
||||
export class ChartDashboard {
|
||||
private $panel: JQuery;
|
||||
private readonly opts: ChartDashboardOptions;
|
||||
private charts: Chart[] = [];
|
||||
private timer: number;
|
||||
private dlg: ChartDialog;
|
||||
|
||||
constructor(elem: string | Element | JQuery, charts: ChartOptions[], opts?: ChartDashboardOptions) {
|
||||
this.opts = $.extend(new ChartDashboardOptions(), opts);
|
||||
this.$panel = $(elem);
|
||||
this.dlg = new ChartDialog(this);
|
||||
|
||||
charts.forEach(opts => this.createGraph(opts));
|
||||
|
||||
// events
|
||||
Dispatcher.bind(this.$panel).on("remove-chart", e => {
|
||||
let name = $(e.target).closest("div.column").data("name");
|
||||
Modal.confirm(`Are you sure to delete chart: <strong>${name}</strong>?`, "Delete chart", dlg => {
|
||||
this.removeGraph(name);
|
||||
dlg.close();
|
||||
});
|
||||
});
|
||||
$(window).resize(e => {
|
||||
$.each(this.charts, (i: number, g: Chart) => {
|
||||
g.resize();
|
||||
});
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (!this.timer) {
|
||||
this.loadData();
|
||||
if (this.opts.refreshInterval > 0) {
|
||||
this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private refreshData() {
|
||||
this.loadData();
|
||||
if (this.opts.refreshInterval > 0) {
|
||||
this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
}
|
||||
|
||||
setPeriod(period: number) {
|
||||
this.opts.period = period;
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
addGraph(opts: ChartOptions) {
|
||||
this.createGraph(opts);
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
private createGraph(opts: ChartOptions) {
|
||||
for (let i = 0; i < this.charts.length; i++) {
|
||||
let chart = this.charts[i];
|
||||
if (chart.getOptions().name === opts.name) {
|
||||
// chart already added.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let chart = ChartFactory.create(opts);
|
||||
if (chart != null) {
|
||||
this.$panel.append(chart.getElem());
|
||||
chart.init();
|
||||
this.charts.push(chart);
|
||||
}
|
||||
}
|
||||
|
||||
removeGraph(name: string) {
|
||||
let index = -1;
|
||||
for (let i = 0; i < this.charts.length; i++) {
|
||||
let c = this.charts[i];
|
||||
if (c.getOptions().name === name) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
let $elem = this.charts[index].getElem();
|
||||
this.charts.splice(index, 1);
|
||||
$elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
save(asDefault: boolean = false) {
|
||||
let charts: any = [];
|
||||
this.$panel.children().each((index: number, elem: Element) => {
|
||||
let name = $(elem).data("name");
|
||||
for (let i = 0; i < this.charts.length; i++) {
|
||||
let c = this.charts[i];
|
||||
if (c.getOptions().name === name) {
|
||||
charts.push({
|
||||
name: c.getOptions().name,
|
||||
width: c.getOptions().width,
|
||||
height: c.getOptions().height,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
let args = {
|
||||
name: this.opts.name,
|
||||
key: asDefault ? '' : (this.opts.key || ''),
|
||||
charts: charts,
|
||||
};
|
||||
$ajax.post(`/system/chart/save_dashboard`, args).json<AjaxResult>((r: AjaxResult) => {
|
||||
if (r.success) {
|
||||
Notification.show("success", "Successfully saved.");
|
||||
} else {
|
||||
Notification.show("danger", r.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getOptions(): ChartDashboardOptions {
|
||||
return this.opts;
|
||||
}
|
||||
|
||||
private loadData() {
|
||||
if (this.charts.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let args: any = {
|
||||
charts: this.charts.map(c => c.getOptions().name).join(","),
|
||||
period: this.opts.period,
|
||||
};
|
||||
if (this.opts.key) {
|
||||
args.key = this.opts.key;
|
||||
}
|
||||
$ajax.get(`/system/chart/data`, args).json((d: { [index: string]: any[] }) => {
|
||||
$.each(this.charts, (i: number, g: Chart) => {
|
||||
if (d[g.getOptions().name]) {
|
||||
g.setData(d[g.getOptions().name]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ChartDialog {
|
||||
private dashboard: ChartDashboard;
|
||||
private fb: FilterBox;
|
||||
private charts: any;
|
||||
private $charts: JQuery;
|
||||
|
||||
constructor(dashboard: ChartDashboard) {
|
||||
this.dashboard = dashboard;
|
||||
this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
|
||||
$("#btn-add").click(this.showAddDlg.bind(this));
|
||||
$("#btn-add-chart").click(this.addChart.bind(this));
|
||||
$("#btn-save").click(() => {
|
||||
this.dashboard.save();
|
||||
});
|
||||
$("#btn-save-as-default").click(() => {
|
||||
this.dashboard.save(true);
|
||||
});
|
||||
}
|
||||
|
||||
private showAddDlg() {
|
||||
let $panel = $("#nav-charts");
|
||||
$panel.find("label.panel-block").remove();
|
||||
|
||||
// load charts
|
||||
$ajax.get(`/system/chart/query`, {dashboard: this.dashboard.getOptions().name}).json((charts: any) => {
|
||||
for (let i = 0; i < charts.length; i++) {
|
||||
let c = charts[i];
|
||||
$panel.append(`<label class="panel-block">
|
||||
<input type="checkbox" value="${c.name}" data-index="${i}">${c.name}: ${c.title}
|
||||
</label>`);
|
||||
}
|
||||
this.charts = charts;
|
||||
this.$charts = $panel.find("label.panel-block");
|
||||
});
|
||||
|
||||
let dlg = new Modal("#dlg-add-chart");
|
||||
dlg.show();
|
||||
}
|
||||
|
||||
private filterCharts(text: string) {
|
||||
if (!text) {
|
||||
this.$charts.show();
|
||||
return;
|
||||
}
|
||||
|
||||
this.$charts.each((i, elem) => {
|
||||
let $elem = $(elem);
|
||||
let texts: string[] = [
|
||||
this.charts[i].name.toLowerCase(),
|
||||
this.charts[i].title.toLowerCase(),
|
||||
this.charts[i].desc.toLowerCase(),
|
||||
];
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
let index = texts[i].indexOf(text);
|
||||
if (index >= 0) {
|
||||
$elem.show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
$elem.hide();
|
||||
})
|
||||
}
|
||||
|
||||
private addChart() {
|
||||
this.$charts.each((i, e) => {
|
||||
if ($(e).find(":checked").length > 0) {
|
||||
let c = this.charts[i];
|
||||
this.dashboard.addGraph(c);
|
||||
}
|
||||
});
|
||||
Modal.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
///<reference path="bulma.ts"/>
|
||||
///<reference path="ajax.ts"/>
|
||||
///<reference path="validator.ts"/>
|
||||
///<reference path="form.ts"/>
|
||||
///<reference path="dispatcher.ts"/>
|
||||
///<reference path="table.ts"/>
|
||||
namespace Swirl.Core {
|
||||
class BulmaMarker implements ValidationMarker {
|
||||
setError($input: JQuery, errors: string[]) {
|
||||
let $field = this.getField($input);
|
||||
|
||||
// update input state
|
||||
$input.removeClass('is-success').addClass('is-danger');
|
||||
|
||||
// set errors into errors block
|
||||
let $errors = $field.find('div.errors');
|
||||
if (!$errors.length) {
|
||||
$errors = $('<div class="errors"/>').appendTo($field);
|
||||
}
|
||||
$errors.empty().append($.map(errors, (err: string) => `<p class="help is-danger">${err}</p>`)).show();
|
||||
}
|
||||
|
||||
clearError($input: JQuery) {
|
||||
let $field = this.getField($input);
|
||||
|
||||
// update input state
|
||||
$input.removeClass('is-danger').addClass('is-success');
|
||||
|
||||
// clear errors
|
||||
let $errors = $field.find("div.errors");
|
||||
$errors.empty().hide();
|
||||
}
|
||||
|
||||
reset($input: JQuery): void {
|
||||
let $field = this.getField($input);
|
||||
|
||||
// update input state
|
||||
$input.removeClass('is-danger is-success');
|
||||
|
||||
// clear errors
|
||||
let $errors = $field.find("div.errors");
|
||||
$errors.empty().hide();
|
||||
}
|
||||
|
||||
private getField($input: JQuery): JQuery {
|
||||
let $field = $input.closest(".field");
|
||||
if ($field.hasClass("has-addons") || $field.hasClass("is-grouped")) {
|
||||
$field = $field.parent();
|
||||
}
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize bulma adapters
|
||||
*/
|
||||
$(() => {
|
||||
// menu burger
|
||||
$('.navbar-burger').click(function () {
|
||||
let $el = $(this);
|
||||
let $target = $('#' + $el.data('target'));
|
||||
$el.toggleClass('is-active');
|
||||
$target.toggleClass('is-active');
|
||||
});
|
||||
|
||||
// Modal
|
||||
Modal.initialize();
|
||||
|
||||
// Tab
|
||||
Tab.initialize();
|
||||
|
||||
// AJAX
|
||||
AjaxRequest.preHandler = opts => opts.trigger && $(opts.trigger).addClass("is-loading");
|
||||
AjaxRequest.postHandler = opts => opts.trigger && $(opts.trigger).removeClass("is-loading");
|
||||
|
||||
// Validator
|
||||
Validator.setMarker(new BulmaMarker());
|
||||
|
||||
// Form
|
||||
Form.automate();
|
||||
});
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
namespace Swirl.Core {
|
||||
/**
|
||||
* Dispatcher
|
||||
*/
|
||||
export class Dispatcher {
|
||||
private attr: string;
|
||||
|
||||
private events: { [index: string]: (e: JQueryEventObject) => any } = {};
|
||||
|
||||
constructor(attr?: string) {
|
||||
this.attr = attr || "action";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Dispatcher instance
|
||||
*
|
||||
* @param elem
|
||||
* @param event
|
||||
* @returns {Dispatcher}
|
||||
*/
|
||||
static bind(elem: string | JQuery | Element | Document, event: string = "click"): Dispatcher {
|
||||
return new Dispatcher().bind(elem, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register event
|
||||
*
|
||||
* @param action
|
||||
* @param handler
|
||||
* @returns {Swirl.Core.Dispatcher}
|
||||
*/
|
||||
on(action: string, handler: (e: JQueryEventObject) => any): Dispatcher {
|
||||
this.events[action] = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister event
|
||||
*
|
||||
* @param action
|
||||
* @returns {Swirl.Core.Dispatcher}
|
||||
*/
|
||||
off(action: string): Dispatcher {
|
||||
delete this.events[action];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind events to element
|
||||
*
|
||||
* @param elem
|
||||
* @param event
|
||||
* @returns {Swirl.Core.Dispatcher}
|
||||
*/
|
||||
bind(elem: string | JQuery | Element | Document, event: string = "click"): Dispatcher {
|
||||
$(elem).on(event, this.handle.bind(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
private handle(e: JQueryEventObject): any {
|
||||
let action = $(e.target).closest("[data-" + this.attr + "]").data(this.attr);
|
||||
if (action) {
|
||||
let handler = this.events[action];
|
||||
if (handler) {
|
||||
e.stopPropagation();
|
||||
handler(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
/*!
|
||||
* Swirl Form Library v1.0.0
|
||||
* Copyright 2017 cuigh. All rights reserved.
|
||||
* see also: https://github.com/A1rPun/transForm.js
|
||||
*
|
||||
* @author cuigh(noname@live.com)
|
||||
*/
|
||||
///<reference path="validator.ts"/>
|
||||
namespace Swirl.Core {
|
||||
/**
|
||||
* Form options
|
||||
*/
|
||||
export class FormOptions {
|
||||
delimiter: string = ".";
|
||||
skipDisabled: boolean = true;
|
||||
skipReadOnly: boolean = false;
|
||||
skipEmpty: boolean = false;
|
||||
useIdOnEmptyName: boolean = true;
|
||||
triggerChange: boolean = false;
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
name: string;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form
|
||||
*/
|
||||
export class Form {
|
||||
private form: JQuery;
|
||||
private options: FormOptions;
|
||||
private validator: Validator;
|
||||
|
||||
/**
|
||||
* Creates an instance of Form.
|
||||
*
|
||||
* @param {(string | Element | JQuery)} elem Form html element
|
||||
* @param {FormOptions} options Form options
|
||||
*
|
||||
* @memberOf Form
|
||||
*/
|
||||
constructor(elem: string | Element | JQuery, options?: FormOptions) {
|
||||
this.form = $(elem);
|
||||
this.options = $.extend(new FormOptions(), options);
|
||||
this.validator = Validator.bind(this.form);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset form
|
||||
*
|
||||
* @memberOf Form
|
||||
*/
|
||||
reset() {
|
||||
(<HTMLFormElement>this.form.get(0)).reset();
|
||||
if (this.validator) {
|
||||
this.validator.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear form data
|
||||
*
|
||||
* @memberOf Form
|
||||
*/
|
||||
clear() {
|
||||
let inputs = this.getFields();
|
||||
inputs.each((i, input) => {
|
||||
this.clearInput(input);
|
||||
});
|
||||
if (this.validator) {
|
||||
this.validator.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit form by AJAX
|
||||
*
|
||||
* @param {string} url submit url
|
||||
* @returns {Swirl.Core.AjaxPostRequest}
|
||||
*
|
||||
* @memberOf Form
|
||||
*/
|
||||
submit(url?: string): AjaxPostRequest {
|
||||
let data = this.serialize();
|
||||
return Ajax.post(url || this.form.attr("action"), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate form
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validate(): boolean {
|
||||
if (!this.validator) {
|
||||
return true;
|
||||
}
|
||||
return this.validator.validate().length == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize form data to JSON
|
||||
*
|
||||
* @param {Function} [nodeCallback] custom callback for parsing input value
|
||||
* @returns {Object}
|
||||
*
|
||||
* @memberOf Form
|
||||
*/
|
||||
serialize(nodeCallback?: Function): Object {
|
||||
let result = {},
|
||||
inputs = this.getFields();
|
||||
|
||||
for (let i = 0, l = inputs.length; i < l; i++) {
|
||||
let input: any = inputs[i],
|
||||
key = input.name || this.options.useIdOnEmptyName && input.id;
|
||||
|
||||
if (!key) continue;
|
||||
|
||||
let entry: Entry = null;
|
||||
if (nodeCallback) entry = nodeCallback(input);
|
||||
if (!entry) entry = this.getEntryFromInput(input, key);
|
||||
|
||||
if (typeof entry.value === 'undefined' || entry.value === null
|
||||
|| (this.options.skipEmpty && (!entry.value || (this.isArray(entry.value) && !entry.value.length))))
|
||||
continue;
|
||||
this.saveEntryToResult(result, entry, input, this.options.delimiter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill data to form
|
||||
*
|
||||
* @param {Object} data JSON data
|
||||
* @param {Function} [nodeCallback] custom callback for processing input value
|
||||
*
|
||||
* @memberOf Form
|
||||
*/
|
||||
deserialize(data: Object, nodeCallback?: Function) {
|
||||
let inputs = this.getFields();
|
||||
for (let i = 0, l = inputs.length; i < l; i++) {
|
||||
let input: any = inputs[i],
|
||||
key = input.name || this.options.useIdOnEmptyName && input.id,
|
||||
value = this.getFieldValue(key, data);
|
||||
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
this.clearInput(input);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mutated = nodeCallback && nodeCallback(input, value);
|
||||
if (!mutated) this.setValueToInput(input, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* automatic initialize Form components using data attributes.
|
||||
*/
|
||||
static automate() {
|
||||
$('form[data-form]').each(function (i, elem) {
|
||||
let $form = $(elem);
|
||||
let form = new Form($form);
|
||||
let type = $form.data("form");
|
||||
|
||||
if (type == "form") {
|
||||
$form.submit(e => { return form.validate() });
|
||||
return;
|
||||
}
|
||||
|
||||
// ajax-json | ajax-form
|
||||
$form.submit(function () {
|
||||
if (!form.validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let request = form.submit($form.attr("action")).trigger($form.find('button[type="submit"]'));
|
||||
if (type == "ajax-form") {
|
||||
request.encoder("form");
|
||||
}
|
||||
request.json((r: AjaxResult) => {
|
||||
if (r.success) {
|
||||
let url = r.url || $form.data("url");
|
||||
if (url) {
|
||||
if (url === "-") {
|
||||
location.reload();
|
||||
} else {
|
||||
location.href = url;
|
||||
}
|
||||
} else {
|
||||
let msg = r.message || $form.data("message");
|
||||
Notification.show("info", `SUCCESS: ${msg}`, 3);
|
||||
}
|
||||
} else {
|
||||
let msg = r.message;
|
||||
if (r.code) {
|
||||
msg += `({r.code})`
|
||||
}
|
||||
Notification.show("danger", `FAILED: ${msg}`);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getEntryFromInput(input: any, key: string): Entry {
|
||||
let nodeType = input.type && input.type.toLowerCase(),
|
||||
entry: Entry = { name: key },
|
||||
dataType = $(input).data("type");
|
||||
|
||||
switch (nodeType) {
|
||||
case 'radio':
|
||||
if (input.checked)
|
||||
entry.value = input.value === 'on' ? true : input.value;
|
||||
break;
|
||||
case 'checkbox':
|
||||
entry.value = input.checked ? (input.value === 'on' ? true : input.value) : false;
|
||||
break;
|
||||
case 'select-multiple':
|
||||
entry.value = [];
|
||||
for (let i = 0, l = input.options.length; i < l; i++)
|
||||
if (input.options[i].selected) entry.value.push(input.options[i].value);
|
||||
break;
|
||||
case 'file':
|
||||
//Only interested in the filename (Chrome adds C:\fakepath\ for security anyway)
|
||||
entry.value = input.value.split('\\').pop();
|
||||
break;
|
||||
case 'button':
|
||||
case 'submit':
|
||||
case 'reset':
|
||||
break;
|
||||
default:
|
||||
entry.value = input.value;
|
||||
}
|
||||
|
||||
if (entry.value != null) {
|
||||
switch (dataType) {
|
||||
case "integer":
|
||||
entry.value = parseInt(entry.value);
|
||||
break;
|
||||
case "float":
|
||||
entry.value = parseFloat(entry.value);
|
||||
break;
|
||||
case "bool":
|
||||
entry.value = (entry.value === "true") || (entry.value === "1");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private saveEntryToResult(parent: { [index: string]: any }, entry: Entry, input: HTMLElement, delimiter: string) {
|
||||
//not not accept empty values in array collections
|
||||
if (/\[]$/.test(entry.name) && !entry.value) return;
|
||||
|
||||
let parts = this.parseString(entry.name, delimiter);
|
||||
for (let i = 0, l = parts.length; i < l; i++) {
|
||||
let part = parts[i];
|
||||
//if last
|
||||
if (i === l - 1) {
|
||||
parent[part] = entry.value;
|
||||
} else {
|
||||
let index = parts[i + 1];
|
||||
if (!index || $.isNumeric(index)) {
|
||||
if (!this.isArray(parent[part]))
|
||||
parent[part] = [];
|
||||
//if second last
|
||||
if (i === l - 2) {
|
||||
parent[part].push(entry.value);
|
||||
} else {
|
||||
if (!this.isObject(parent[part][index]))
|
||||
parent[part][index] = {};
|
||||
parent = parent[part][index];
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
if (!this.isObject(parent[part]))
|
||||
parent[part] = {};
|
||||
parent = parent[part];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private clearInput(input: any) {
|
||||
let nodeType = input.type && input.type.toLowerCase();
|
||||
switch (nodeType) {
|
||||
case 'select-one':
|
||||
input.selectedIndex = 0;
|
||||
break;
|
||||
case 'select-multiple':
|
||||
for (let i = input.options.length; i--;)
|
||||
input.options[i].selected = false;
|
||||
break;
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
if (input.checked) input.checked = false;
|
||||
break;
|
||||
case 'button':
|
||||
case 'submit':
|
||||
case 'reset':
|
||||
case 'file':
|
||||
break;
|
||||
default:
|
||||
input.value = '';
|
||||
}
|
||||
if (this.options.triggerChange) {
|
||||
$(input).change();
|
||||
}
|
||||
}
|
||||
|
||||
private parseString(str: string, delimiter: string): string[] {
|
||||
let result: string[] = [],
|
||||
split = str.split(delimiter),
|
||||
len = split.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
let s = split[i].split('['),
|
||||
l = s.length;
|
||||
for (let j = 0; j < l; j++) {
|
||||
let key = s[j];
|
||||
if (!key) {
|
||||
//if the first one is empty, continue
|
||||
if (j === 0) continue;
|
||||
//if the undefined key is not the last part of the string, throw error
|
||||
if (j !== l - 1)
|
||||
throw new Error(`Undefined key is not the last part of the name > ${str}`);
|
||||
}
|
||||
//strip "]" if its there
|
||||
if (key && key[key.length - 1] === ']')
|
||||
key = key.slice(0, -1);
|
||||
result.push(key);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private getFields(): JQuery {
|
||||
let inputs: JQuery = this.form.find("input,select,textarea").filter(':not([data-form-ignore="true"])');
|
||||
if (this.options.skipDisabled) inputs = inputs.filter(":not([disabled])");
|
||||
if (this.options.skipReadOnly) inputs = inputs.filter(":not([readonly])");
|
||||
return inputs;
|
||||
}
|
||||
|
||||
private getFieldValue(key: string, ref: any): any {
|
||||
if (!key || !ref) return;
|
||||
|
||||
let parts = this.parseString(key, this.options.delimiter);
|
||||
for (let i = 0, l = parts.length; i < l; i++) {
|
||||
let part = ref[parts[i]];
|
||||
|
||||
if (typeof part === 'undefined' || part === null) return;
|
||||
|
||||
//if last
|
||||
if (i === l - 1) {
|
||||
return part;
|
||||
} else {
|
||||
let index = parts[i + 1];
|
||||
if (index === '') {
|
||||
return part;
|
||||
} else if ($.isNumeric(index)) {
|
||||
//if second last
|
||||
if (i === l - 2)
|
||||
return part[index];
|
||||
else
|
||||
ref = part[index];
|
||||
i++;
|
||||
} else {
|
||||
ref = part;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setValueToInput(input: any, value: any) {
|
||||
let nodeType = input.type && input.type.toLowerCase();
|
||||
switch (nodeType) {
|
||||
case 'radio':
|
||||
if (value == input.value) input.checked = true;
|
||||
break;
|
||||
case 'checkbox':
|
||||
input.checked = this.isArray(value)
|
||||
? this.contains(value, input.value)
|
||||
: value === true || value == input.value;
|
||||
break;
|
||||
case 'select-multiple':
|
||||
if (this.isArray(value))
|
||||
for (let i = input.options.length; i--;)
|
||||
input.options[i].selected = this.contains(value, input.options[i].value);
|
||||
else
|
||||
input.value = value;
|
||||
break;
|
||||
case 'button':
|
||||
case 'submit':
|
||||
case 'reset':
|
||||
case 'file':
|
||||
break;
|
||||
default:
|
||||
input.value = value;
|
||||
}
|
||||
if (this.options.triggerChange) {
|
||||
$(input).change();
|
||||
}
|
||||
}
|
||||
|
||||
/*** Helper functions ***/
|
||||
|
||||
private contains(array: any[], value: any): boolean {
|
||||
for (let item of array) {
|
||||
if (item == value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isObject(obj: any) {
|
||||
return typeof obj === 'object';
|
||||
}
|
||||
|
||||
private isArray(arr: any) {
|
||||
return Array.isArray(arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/*!
|
||||
* Swirl Table Library v1.0.0
|
||||
* Copyright 2017 cuigh. All rights reserved.
|
||||
*
|
||||
* @author cuigh(noname@live.com)
|
||||
*/
|
||||
///<reference path="dispatcher.ts"/>
|
||||
namespace Swirl.Core {
|
||||
export class Table {
|
||||
protected $table: JQuery;
|
||||
private dispatcher: Dispatcher;
|
||||
|
||||
constructor(table: string | Element | JQuery) {
|
||||
this.$table = $(table);
|
||||
this.dispatcher = Dispatcher.bind(this.$table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind action
|
||||
*
|
||||
* @param action
|
||||
* @param handler
|
||||
* @returns {Swirl.Core.ListTable}
|
||||
*/
|
||||
on(action: string, handler: (e: JQueryEventObject) => any): this {
|
||||
this.dispatcher.on(action, handler);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class ListTable extends Table {
|
||||
constructor(table: string | Element | JQuery) {
|
||||
super(table);
|
||||
|
||||
this.on("check-all", e => {
|
||||
let checked = (<HTMLInputElement>e.target).checked;
|
||||
this.$table.find("tbody>tr").each((i, elem) => {
|
||||
$(elem).find("td:first>:checkbox").prop("checked", checked);
|
||||
});
|
||||
});
|
||||
this.on("check", () => {
|
||||
let rows = this.$table.find("tbody>tr").length;
|
||||
let checkedRows = this.selectedRows().length;
|
||||
this.$table.find("thead>tr>th:first>:checkbox").prop("checked", checkedRows > 0 && rows == checkedRows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return selected rows.
|
||||
*/
|
||||
selectedRows(): JQuery {
|
||||
return this.$table.find("tbody>tr").filter((i, elem) => {
|
||||
let cb = $(elem).find("td:first>:checkbox");
|
||||
return cb.prop("checked");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return keys of selected items.
|
||||
*/
|
||||
selectedKeys(): string[] {
|
||||
let keys: string[] = [];
|
||||
this.$table.find("tbody>tr").each((i, elem) => {
|
||||
let cb = $(elem).find("td:first>:checkbox");
|
||||
if (cb.prop("checked")) {
|
||||
keys.push(cb.val());
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class EditTable extends Table {
|
||||
protected name: string;
|
||||
protected index: number;
|
||||
protected alias: string;
|
||||
|
||||
constructor(elem: string | JQuery | Element) {
|
||||
super(elem);
|
||||
|
||||
this.name = this.$table.data("name");
|
||||
this.alias = this.name.replace(".", "-");
|
||||
this.index = this.$table.find("tbody>tr").length;
|
||||
|
||||
super.on("add-" + this.alias, this.addRow.bind(this)).on("delete-" + this.alias, OptionTable.deleteRow);
|
||||
}
|
||||
|
||||
protected abstract render(): string;
|
||||
|
||||
private addRow() {
|
||||
this.$table.find("tbody").append(this.render());
|
||||
this.index++;
|
||||
}
|
||||
|
||||
private static deleteRow(e: JQueryEventObject) {
|
||||
$(e.target).closest("tr").remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionTable extends EditTable {
|
||||
constructor(elem: string | JQuery | Element) {
|
||||
super(elem);
|
||||
}
|
||||
|
||||
protected render(): string {
|
||||
return `<tr>
|
||||
<td><input name="${this.name}s[${this.index}].name" class="input is-small" type="text"></td>
|
||||
<td><input name="${this.name}s[${this.index}].value" class="input is-small" type="text"></td>
|
||||
<td>
|
||||
<a class="button is-small is-danger is-outlined" data-action="delete-${this.alias}">
|
||||
<span class="icon is-small"><i class="far fa-trash-alt"></i></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
/*!
|
||||
* Swirl Validator Library v1.0.0
|
||||
* Copyright 2017 cuigh. All rights reserved.
|
||||
*
|
||||
* @author cuigh(noname@live.com)
|
||||
*/
|
||||
namespace Swirl.Core {
|
||||
type InputElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;
|
||||
|
||||
/**
|
||||
* The result of validation.
|
||||
*
|
||||
* @interface ValidationResult
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
input: JQuery;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// interface ValidationRule {
|
||||
// ($input: JQuery): boolean;
|
||||
// }
|
||||
|
||||
export interface ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string};
|
||||
}
|
||||
|
||||
export interface ValidationMarker {
|
||||
setError($input: JQuery, errors: string[]): void;
|
||||
clearError($input: JQuery): void;
|
||||
reset($input: JQuery): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 form element native validator
|
||||
*
|
||||
* @class NativeRule
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class NativeRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
let el = <InputElement>$input[0];
|
||||
return {ok: el.checkValidity ? el.checkValidity() : true};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Required validator
|
||||
*
|
||||
* @class RequiredRule
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class RequiredRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
return {ok: $.trim($input.val()).length > 0};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checked validator(for radio/checkbox), e.g. checked, checked(2), checked(1~2)
|
||||
*
|
||||
* @class CheckedRule
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class CheckedRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
let count = parseInt(arg);
|
||||
let siblings = $form.find(`:input:checked[name='${$input.attr("name")}']`);
|
||||
return {ok: siblings.length >= count};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Email validator
|
||||
*
|
||||
* @class EmailValidator
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class EmailRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
const regex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
|
||||
let value = $.trim($input.val());
|
||||
return {ok: !value || regex.test(value)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP/FTP URL validator
|
||||
*
|
||||
* @class UrlValidator
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class UrlRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
const regex = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
|
||||
let value = $.trim($input.val());
|
||||
return {ok: !value || regex.test(value)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IPV4 address validator
|
||||
*
|
||||
* @class IPValidator
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class IPRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
const regex = /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/i;
|
||||
let value = $.trim($input.val());
|
||||
return {ok: !value || regex.test(value)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match validator(e.g. password confirmation)
|
||||
*
|
||||
* @class MatchValidator
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class MatchValidator implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
return {ok: $input.val() == $('#' + arg).val()};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String length validator.
|
||||
*
|
||||
* @class LengthValidator
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class LengthRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
let r: {ok: boolean, error?: string} = {ok: true};
|
||||
if (arg) {
|
||||
let len = this.getLength($.trim($input.val()));
|
||||
let args = arg.split('~');
|
||||
if (args.length == 1) {
|
||||
if ($.isNumeric(args[0])) {
|
||||
r.ok = len >= parseInt(args[0]);
|
||||
}
|
||||
} else {
|
||||
if ($.isNumeric(args[0]) && $.isNumeric(args[1])) {
|
||||
r.ok = len >= parseInt(args[0]) && len <= parseInt(args[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
protected getLength(value: string): number {
|
||||
return value.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String width validator, the width of CJK characters is considered as 2.
|
||||
*
|
||||
* @class WidthValidator
|
||||
* @extends {LengthRule}
|
||||
*/
|
||||
class WidthRule extends LengthRule {
|
||||
protected getLength(value: string): number {
|
||||
let doubleByteChars = value.match(/[^\x00-\xff]/ig);
|
||||
return value.length + (doubleByteChars == null ? 0 : doubleByteChars.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 整数验证器
|
||||
*
|
||||
* @class IntegerValidator
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class IntegerRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
const regex = /^\d*$/;
|
||||
return {ok: regex.test($.trim($input.val()))};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex validator
|
||||
*
|
||||
* @class RegexValidator
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class RegexRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
let regex = new RegExp(arg);
|
||||
let value = $.trim($input.val());
|
||||
return {ok: !value || regex.test(value)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote validator
|
||||
*
|
||||
* @class RemoteRule
|
||||
* @implements {ValidationRule}
|
||||
*/
|
||||
class RemoteRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
if (!arg) {
|
||||
throw new Error("服务器验证地址未设置");
|
||||
}
|
||||
|
||||
let value = $.trim($input.val());
|
||||
let r: {ok: boolean, error?: string} = {ok: false};
|
||||
$ajax.post(arg, {value: value}).encoder("form").async(false).json<{error: string}>(result => {
|
||||
r.ok = !result.error;
|
||||
r.error = result.error;
|
||||
});
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ValidatorOptions {
|
||||
}
|
||||
|
||||
export class Validator {
|
||||
private static selector = ':input[data-v-rule]:not(:submit,:button,:reset,:image,:disabled)';
|
||||
// error marker
|
||||
private static marker: ValidationMarker;
|
||||
// error message
|
||||
private static messages: { [index: string]: string } = {
|
||||
"required": "This field is required",
|
||||
"checked": "Number of checked items is invalid",
|
||||
"email": "Please input a valid email address",
|
||||
"match": "Input confirmation doesn't match",
|
||||
"length": "The length of the field does not meet the requirements",
|
||||
"width": "The width of the field does not meet the requirements",
|
||||
"url": "Please input a valid url",
|
||||
"ip": "Please input a valid IPV4 address",
|
||||
"integer": "Please input an integer",
|
||||
"regex": "Input is invalid",
|
||||
"remote": "Input is invalid",
|
||||
};
|
||||
private static rules: { [index: string]: ValidationRule } = {
|
||||
"native": new NativeRule(),
|
||||
"required": new RequiredRule(),
|
||||
"checked": new CheckedRule(),
|
||||
"email": new EmailRule(),
|
||||
"match": new MatchValidator(),
|
||||
"length": new LengthRule(),
|
||||
"width": new WidthRule(),
|
||||
"url": new UrlRule(),
|
||||
"ip": new IPRule(),
|
||||
"integer": new IntegerRule(),
|
||||
"regex": new RegexRule(),
|
||||
"remote": new RemoteRule(),
|
||||
};
|
||||
private form: JQuery;
|
||||
private options: Object;
|
||||
|
||||
/**
|
||||
* Creates an instance of Validator.
|
||||
*
|
||||
* @param {(string | HTMLElement | JQuery)} elem the parent element which contains all form inputs
|
||||
* @param {*} [options] the validation options
|
||||
*
|
||||
* @memberOf Validator
|
||||
*/
|
||||
private constructor(elem: string | HTMLElement | JQuery, options?: ValidatorOptions) {
|
||||
this.form = $(elem);
|
||||
this.options = options;
|
||||
|
||||
// disable default validation of HTML5, and bind submit event
|
||||
if (this.form.is("form")) {
|
||||
this.form.attr("novalidate", "true");
|
||||
// this.form.submit(e => {
|
||||
// let results = this.validate();
|
||||
// if (results != null && results.length > 0) {
|
||||
// e.preventDefault();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
// realtime validate events
|
||||
this.form.on("click", ':radio[data-v-rule],:checkbox[data-v-rule]', this.checkValue.bind(this));
|
||||
this.form.on("change", 'select[data-v-rule],input[type="file"][data-v-rule]', this.checkValue.bind(this));
|
||||
this.form.on("blur", ':input[data-v-rule]:not(select,:radio,:checkbox,:file)', this.checkValue.bind(this));
|
||||
}
|
||||
|
||||
private checkValue(e: JQueryEventObject) {
|
||||
let $input = $(e.target);
|
||||
let result = this.validateInput($input);
|
||||
Validator.mark($input, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建验证器并绑定到表单
|
||||
*
|
||||
* @static
|
||||
* @param {(string | HTMLElement | JQuery)} elem 验证表单或其它容器元素
|
||||
* @param {ValidatorOptions} [options]
|
||||
* @returns {Validator} 选项
|
||||
*
|
||||
* @memberOf Validator
|
||||
*/
|
||||
static bind(elem: string | HTMLElement | JQuery, options?: ValidatorOptions): Validator {
|
||||
let v = $(elem).data("validator");
|
||||
if (!v) {
|
||||
v = new Validator(elem, options);
|
||||
$(elem).data("validator", v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证表单
|
||||
*
|
||||
* @returns {ValidationResult[]}
|
||||
*/
|
||||
validate(): ValidationResult[] {
|
||||
let results: ValidationResult[] = [];
|
||||
this.form.find(Validator.selector).each((i, el) => {
|
||||
let $input = $(el);
|
||||
let result = this.validateInput($input);
|
||||
if (result != null) {
|
||||
results.push(result);
|
||||
}
|
||||
Validator.mark($input, result);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除验证标识
|
||||
*/
|
||||
reset(): void {
|
||||
this.form.find(Validator.selector).each((i, el) => {
|
||||
let $input = $(el);
|
||||
Validator.marker.reset($input);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册验证器
|
||||
*
|
||||
* @static
|
||||
* @param {string} name 验证器名称
|
||||
* @param {ValidationRule} rule 验证方法
|
||||
* @param {string} msg 验证消息
|
||||
*
|
||||
* @memberOf Validator
|
||||
*/
|
||||
static register(name: string, rule: ValidationRule, msg: string) {
|
||||
this.rules[name] = rule;
|
||||
this.messages[name] = msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* set error message
|
||||
*/
|
||||
static setMessage(name: string, msg: string) {
|
||||
this.messages[name] = msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* set error marker
|
||||
*/
|
||||
static setMarker(marker: ValidationMarker) {
|
||||
this.marker = marker;
|
||||
}
|
||||
|
||||
private validateInput($input: JQuery): ValidationResult {
|
||||
let errors: string[] = [];
|
||||
let rules: string[]= ($input.data('v-rule') || 'native').split(';');
|
||||
rules.forEach(name => {
|
||||
let rule = Validator.rules[name];
|
||||
if (rule) {
|
||||
let arg = $input.data("v-arg-" + name);
|
||||
let r = rule.validate(this.form, $input, arg);
|
||||
if (!r.ok) {
|
||||
errors.push(r.error || Validator.getMessge($input, name));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (errors.length == 0) ? null : {
|
||||
input: $input,
|
||||
errors: errors,
|
||||
};
|
||||
}
|
||||
|
||||
private static mark($input: JQuery, result: ValidationResult) {
|
||||
if (Validator.marker != null) {
|
||||
if (result) {
|
||||
Validator.marker.setError($input, result.errors);
|
||||
}
|
||||
else {
|
||||
Validator.marker.clearError($input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static getMessge($input: JQuery, rule: string) {
|
||||
// $input[0].validationMessage
|
||||
// if (!success) $input[0].setCustomValidity("错误信息");
|
||||
if (rule == 'native') return (<InputElement>$input[0]).validationMessage;
|
||||
else {
|
||||
let msg = $input.data('v-msg-' + rule);
|
||||
if (!msg) msg = this.messages[rule];
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Image {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Table = Swirl.Core.ListTable;
|
||||
|
||||
export class ListPage {
|
||||
private table: Table;
|
||||
|
||||
constructor() {
|
||||
this.table = new Table("#table-items");
|
||||
|
||||
// bind events
|
||||
this.table.on("delete-image", this.deleteImage.bind(this));
|
||||
$("#btn-delete").click(this.deleteImages.bind(this));
|
||||
}
|
||||
|
||||
private deleteImage(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:eq(1)").text().trim();
|
||||
let id = $tr.find(":checkbox:first").val();
|
||||
Modal.confirm(`Are you sure to remove image: <strong>${name}</strong>?`, "Delete image", (dlg, e) => {
|
||||
$ajax.post("delete", { ids: id }).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private deleteImages() {
|
||||
let ids = this.table.selectedKeys();
|
||||
if (ids.length == 0) {
|
||||
Modal.alert("Please select one or more items.");
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm(`Are you sure to remove ${ids.length} images?`, "Delete images", (dlg, e) => {
|
||||
$ajax.post("delete", { ids: ids.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
this.table.selectedRows().remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
///<reference path="core/core.ts" />
|
||||
///<reference path="core/chart.ts" />
|
||||
namespace Swirl {
|
||||
import ChartDashboard = Swirl.Core.ChartDashboard;
|
||||
|
||||
export class IndexPage {
|
||||
private dashboard: ChartDashboard;
|
||||
|
||||
constructor() {
|
||||
this.dashboard = new ChartDashboard("#div-charts", window.charts, {name: "home"});
|
||||
$("#cb-time").change(e => {
|
||||
this.dashboard.setPeriod($(e.target).val());
|
||||
});
|
||||
dragula([$('#div-charts').get(0)], {
|
||||
moves: function (el, container, handle): boolean {
|
||||
return $(handle).closest('a.drag').length > 0;
|
||||
// return handle.classList.contains('drag');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Network {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class DetailPage {
|
||||
constructor() {
|
||||
let dispatcher = Dispatcher.bind("#table-containers");
|
||||
dispatcher.on("disconnect", this.disconnect.bind(this));
|
||||
}
|
||||
|
||||
private disconnect(e: JQueryEventObject) {
|
||||
let $btn = $(e.target);
|
||||
let $tr = $btn.closest("tr");
|
||||
let id = $btn.val();
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to disconnect container: <strong>${name}</strong>?`, "Disconnect container", (dlg, e) => {
|
||||
$ajax.post("disconnect", {container: id}).trigger($btn).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Network {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class ListPage {
|
||||
constructor() {
|
||||
let dispatcher = Dispatcher.bind("#table-items");
|
||||
dispatcher.on("delete-network", this.deleteNetwork.bind(this));
|
||||
}
|
||||
|
||||
private deleteNetwork(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to remove network: <strong>${name}</strong>?`, "Delete network", (dlg, e) => {
|
||||
$ajax.post("delete", {name: name}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Network {
|
||||
import OptionTable = Swirl.Core.OptionTable;
|
||||
|
||||
export class NewPage {
|
||||
constructor() {
|
||||
new OptionTable("#table-options");
|
||||
new OptionTable("#table-labels");
|
||||
|
||||
$("#drivers :radio[name=driver]").change(e => {
|
||||
$("#txt-custom-driver").prop("disabled", $(e.target).val() != "other");
|
||||
});
|
||||
$("#ipv6_enabled").change(e => {
|
||||
let enabled = $(e.target).prop("checked");
|
||||
$("#ipv6_subnet,#ipv6_gateway").prop("disabled", !enabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Node {
|
||||
import OptionTable = Swirl.Core.OptionTable;
|
||||
|
||||
export class EditPage {
|
||||
constructor() {
|
||||
new OptionTable("#table-labels");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Node {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class ListPage {
|
||||
constructor() {
|
||||
let dispatcher = Dispatcher.bind("#table-items");
|
||||
dispatcher.on("delete-node", this.deleteNode.bind(this));
|
||||
}
|
||||
|
||||
private deleteNode(e: JQueryEventObject) {
|
||||
let $btn = $(e.target);
|
||||
let $tr = $btn.closest("tr");
|
||||
let id =$btn.val();
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to remove node: <strong>${name}</strong>?`, "Delete node", (dlg, e) => {
|
||||
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Perm {
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
import Modal = Swirl.Core.Modal;
|
||||
|
||||
export class EditPage {
|
||||
constructor() {
|
||||
// bind events
|
||||
$("#txt-query").keydown(this.searchUser);
|
||||
$("#btn-add-user").click(this.addUser);
|
||||
Dispatcher.bind("#div-users").on("delete-user", this.deleteUser.bind(this));
|
||||
}
|
||||
|
||||
private deleteUser(e: JQueryEventObject) {
|
||||
$(e.target).closest("div.control").remove();
|
||||
}
|
||||
|
||||
private searchUser(e: JQueryEventObject) {
|
||||
if (e.keyCode == 13) {
|
||||
let query = $.trim($(e.target).val());
|
||||
if (query.length == 0) {
|
||||
return;
|
||||
}
|
||||
$ajax.post("/system/user/search", {query: query}).encoder("form").json((users: any) => {
|
||||
let $panel = $("#nav-users");
|
||||
$panel.find("label.panel-block").remove();
|
||||
for (let user of users) {
|
||||
$panel.append(`<label class="panel-block">
|
||||
<input type="checkbox" value="${user.id}" data-name="${user.name}"> ${user.name}
|
||||
</label>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private addUser() {
|
||||
let users: { [index: string]: boolean } = {};
|
||||
$("#div-users").find("input").each((i, e) => {
|
||||
users[$(e).val()] = true;
|
||||
});
|
||||
|
||||
let $panel = $("#nav-users");
|
||||
$panel.find("input:checked").each((i, e) => {
|
||||
let $el = $(e);
|
||||
if (users[$el.val()]) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#div-users").append(`<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-info">${$el.data("name")}</span>
|
||||
<a class="tag is-delete" data-action="delete-user"></a>
|
||||
<input name="users[]" value="${$el.val()}" type="hidden">
|
||||
</div>`);
|
||||
});
|
||||
|
||||
Modal.close();
|
||||
$panel.find("label.panel-block").remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Registry {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class ListPage {
|
||||
constructor() {
|
||||
let dispatcher = Dispatcher.bind("#table-items");
|
||||
dispatcher.on("delete-registry", this.deleteRegistry.bind(this));
|
||||
dispatcher.on("edit-registry", this.editRegistry.bind(this));
|
||||
}
|
||||
|
||||
private deleteRegistry(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let id = $tr.data("id");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to remove registry: <strong>${name}</strong>?`, "Delete registry", (dlg, e) => {
|
||||
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private editRegistry(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let dlg = new Modal("#dlg-edit");
|
||||
dlg.find("input[name=id]").val($tr.data("id"));
|
||||
dlg.find("input[name=name]").val($tr.find("td:first").text().trim());
|
||||
dlg.find("input[name=url]").val($tr.find("td:eq(1)").text().trim());
|
||||
dlg.find("input[name=username]").val($tr.find("td:eq(2)").text().trim());
|
||||
dlg.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Role {
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class EditPage {
|
||||
constructor() {
|
||||
$("#table-perms").find("tr").each((i, elem) => {
|
||||
let $tr = $(elem);
|
||||
let $cbs = $tr.find("td :checkbox");
|
||||
$tr.find("th>:checkbox").prop("checked", $cbs.length == $cbs.filter(":checked").length);
|
||||
});
|
||||
|
||||
// bind events
|
||||
Dispatcher.bind("#table-perms", "change")
|
||||
.on("check-row", this.checkRow.bind(this))
|
||||
.on("check", this.check.bind(this))
|
||||
}
|
||||
|
||||
private checkRow(e: JQueryEventObject) {
|
||||
let $cb = $(e.target);
|
||||
let checked = $cb.prop("checked");
|
||||
$cb.closest("th").next("td").find(":checkbox").prop("checked", checked);
|
||||
}
|
||||
|
||||
private check(e: JQueryEventObject) {
|
||||
let $cb = $(e.target);
|
||||
let $cbs = $cb.closest("td").find(":checkbox");
|
||||
let checked = $cbs.length == $cbs.filter(":checked").length;
|
||||
$cb.closest("td").prev("th").find(":checkbox").prop("checked", checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Role {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class ListPage {
|
||||
constructor() {
|
||||
// bind events
|
||||
Dispatcher.bind("#table-items")
|
||||
.on("delete-role", this.deleteUser.bind(this))
|
||||
}
|
||||
|
||||
private deleteUser(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let id = $tr.data("id");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to remove role: <strong>${name}</strong>?`, "Delete role", (dlg, e) => {
|
||||
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Role {
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class NewPage {
|
||||
constructor() {
|
||||
// bind events
|
||||
Dispatcher.bind("#table-perms", "change")
|
||||
.on("check-row", this.checkRow.bind(this))
|
||||
.on("check", this.check.bind(this))
|
||||
}
|
||||
|
||||
private checkRow(e: JQueryEventObject) {
|
||||
let $cb = $(e.target);
|
||||
let checked = $cb.prop("checked");
|
||||
$cb.closest("th").next("td").find(":checkbox").prop("checked", checked);
|
||||
}
|
||||
|
||||
private check(e: JQueryEventObject) {
|
||||
let $cb = $(e.target);
|
||||
let $cbs = $cb.closest("td").find(":checkbox");
|
||||
let checked = $cbs.length == $cbs.filter(":checked").length;
|
||||
$cb.closest("td").prev("th").find(":checkbox").prop("checked", checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Secret {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Table = Swirl.Core.ListTable;
|
||||
|
||||
export class ListPage {
|
||||
private table: Table;
|
||||
|
||||
constructor() {
|
||||
this.table = new Table("#table-items");
|
||||
|
||||
// bind events
|
||||
this.table.on("delete-secret", this.deleteSecret.bind(this));
|
||||
$("#btn-delete").click(this.deleteSecrets.bind(this));
|
||||
}
|
||||
|
||||
private deleteSecret(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:eq(1)").text().trim();
|
||||
let id = $tr.find(":checkbox:first").val();
|
||||
Modal.confirm(`Are you sure to remove secret: <strong>${name}</strong>?`, "Delete secret", (dlg, e) => {
|
||||
$ajax.post("delete", { ids: id }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private deleteSecrets(e: JQueryEventObject) {
|
||||
let ids = this.table.selectedKeys();
|
||||
if (ids.length == 0) {
|
||||
Modal.alert("Please select one or more items.")
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm(`Are you sure to remove ${ids.length} secrets?`, "Delete secrets", (dlg, e) => {
|
||||
$ajax.post("delete", { ids: ids.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
this.table.selectedRows().remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Secret {
|
||||
import OptionTable = Swirl.Core.OptionTable;
|
||||
|
||||
export class NewPage {
|
||||
constructor() {
|
||||
new OptionTable("#table-labels");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Service {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
|
||||
export class DetailPage {
|
||||
constructor() {
|
||||
$("#btn-delete").click(this.deleteService.bind(this));
|
||||
$("#btn-scale").click(this.scaleService.bind(this));
|
||||
$("#btn-restart").click(this.restartService.bind(this));
|
||||
$("#btn-rollback").click(this.rollbackService.bind(this));
|
||||
}
|
||||
|
||||
private deleteService(e: JQueryEventObject) {
|
||||
let name = $("#h2-name").text().trim();
|
||||
Modal.confirm(`Are you sure to remove service: <strong>${name}</strong>?`, "Delete service", (dlg, e) => {
|
||||
$ajax.post(`delete`).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
location.href = "/service/";
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private scaleService(e: JQueryEventObject) {
|
||||
let data = {
|
||||
count: $("#span-replicas").text().trim(),
|
||||
};
|
||||
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", dlg => {
|
||||
data.count = dlg.find("input[name=count]").val();
|
||||
$ajax.post(`scale`, data).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private rollbackService(e: JQueryEventObject) {
|
||||
let name = $("#h2-name").text().trim();
|
||||
Modal.confirm(`Are you sure to rollback service: <strong>${name}</strong>?`, "Rollback service", dlg => {
|
||||
$ajax.post(`rollback`).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private restartService(e: JQueryEventObject) {
|
||||
let name = $("#h2-name").text().trim();
|
||||
Modal.confirm(`Are you sure to restart service: <strong>${name}</strong>?`, "Restart service", dlg => {
|
||||
$ajax.post(`restart`).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Service {
|
||||
import Validator = Swirl.Core.Validator;
|
||||
import OptionTable = Swirl.Core.OptionTable;
|
||||
import EditTable = Swirl.Core.EditTable;
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import Table = Swirl.Core.Table;
|
||||
|
||||
class ServiceModeRule implements Swirl.Core.ValidationRule {
|
||||
private $mode: JQuery;
|
||||
|
||||
constructor($model: JQuery) {
|
||||
this.$mode = $model;
|
||||
}
|
||||
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): { ok: boolean, error?: string } {
|
||||
if (this.$mode.val() == "global") {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
const regex = /^(0|[1-9]\d*)$/;
|
||||
return { ok: regex.test($.trim($input.val())) };
|
||||
}
|
||||
}
|
||||
|
||||
class MountTable extends EditTable {
|
||||
protected render(): string {
|
||||
return `<tr>
|
||||
<td>
|
||||
<div class="select is-small">
|
||||
<select name="mounts[${this.index}].type">
|
||||
<option value="bind">Bind</option>
|
||||
<option value="volume">Volume</option>
|
||||
<option value="tmpfs">TempFS</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input name="mounts[${this.index}].src" class="input is-small" placeholder="path in host">
|
||||
</td>
|
||||
<td><input name="mounts[${this.index}].dst" class="input is-small" placeholder="path in container"></td>
|
||||
<td>
|
||||
<div class="select is-small">
|
||||
<select name="mounts[${this.index}].read_only" data-type="bool">
|
||||
<option value="false">No</option>
|
||||
<option value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="select is-small">
|
||||
<select name="mounts[${this.index}].propagation">
|
||||
<option value="">--Select--</option>
|
||||
<option value="rprivate">rprivate</option>
|
||||
<option value="private">private</option>
|
||||
<option value="rshared">rshared</option>
|
||||
<option value="shared">shared</option>
|
||||
<option value="rslave">rslave</option>
|
||||
<option value="slave">slave</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="button is-small is-outlined is-danger" data-action="delete-mount">
|
||||
<span class="icon is-small">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
class PortTable extends EditTable {
|
||||
protected render(): string {
|
||||
return `<tr>
|
||||
<td><input name="endpoint.ports[${this.index}].published_port" class="input is-small" placeholder="port in host" data-type="integer"></td>
|
||||
<td>
|
||||
<input name="endpoint.ports[${this.index}].target_port" class="input is-small" placeholder="port in container" data-type="integer">
|
||||
</td>
|
||||
<td>
|
||||
<div class="select is-small">
|
||||
<select name="endpoint.ports[${this.index}].protocol">
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
<option value="sctp">SCTP</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="select is-small">
|
||||
<select name="endpoint.ports[${this.index}].publish_mode">
|
||||
<option value="ingress">ingress</option>
|
||||
<option value="host">host</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="button is-small is-outlined is-danger" data-action="delete-endpoint-port">
|
||||
<span class="icon is-small">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
class ConstraintTable extends EditTable {
|
||||
protected render(): string {
|
||||
return `<tr>
|
||||
<td>
|
||||
<input name="placement.constraints[${this.index}].name" class="input is-small" placeholder="e.g. node.role/node.hostname/node.id/node.labels.*/engine.labels.*/...">
|
||||
</td>
|
||||
<td>
|
||||
<div class="select is-small">
|
||||
<select name="placement.constraints[${this.index}].op">
|
||||
<option value="==">==</option>
|
||||
<option value="!=">!=</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input name="placement.constraints[${this.index}].value" class="input is-small" placeholder="e.g. manager">
|
||||
</td>
|
||||
<td>
|
||||
<a class="button is-small is-outlined is-danger" data-action="delete-constraint">
|
||||
<span class="icon is-small">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
class PreferenceTable extends EditTable {
|
||||
protected render(): string {
|
||||
return `<tr>
|
||||
<td>
|
||||
<input name="placement.preferences[${this.index}].spread" class="input is-small" placeholder="e.g. engine.labels.az">
|
||||
</td>
|
||||
<td>
|
||||
<a class="button is-small is-outlined is-danger" data-action="delete-preference">
|
||||
<span class="icon is-small">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigTable extends Table {
|
||||
public readonly name: string;
|
||||
private index: number;
|
||||
private $body: JQuery;
|
||||
|
||||
constructor(elem: string | JQuery | Element) {
|
||||
super(elem);
|
||||
|
||||
this.name = this.$table.data("name");
|
||||
this.$body = this.$table.find("tbody");
|
||||
this.index = this.$body.find("tr").length;
|
||||
|
||||
super.on("add-" + this.name, this.showAddDialog.bind(this)).on("delete-" + this.name, ConfigTable.deleteRow);
|
||||
}
|
||||
|
||||
public addRow(id: string, name: string) {
|
||||
let field = `${this.name}s[${this.index}]`;
|
||||
this.$body.append(`<tr>
|
||||
<td>${name}<input name="${field}.id" value="${id}" type="hidden"><input name="${field}.name" value="${name}" type="hidden"></td>
|
||||
<td><input name="${field}.file_name" value="${name}" class="input is-small"></td>
|
||||
<td><input name="${field}.uid" value="0" class="input is-small"></td>
|
||||
<td><input name="${field}.gid" value="0" class="input is-small"></td>
|
||||
<td><input name="${field}.mode" value="444" class="input is-small" data-type="integer"></td>
|
||||
<td>
|
||||
<a class="button is-small is-outlined is-danger" data-action="delete-${this.name}">
|
||||
<span class="icon is-small">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>`);
|
||||
this.index++;
|
||||
}
|
||||
|
||||
private showAddDialog(e: JQueryEventObject) {
|
||||
let dlg = new Modal("#dlg-add-"+this.name);
|
||||
dlg.find(":checked").prop("checked", false);
|
||||
dlg.error();
|
||||
dlg.show();
|
||||
}
|
||||
|
||||
private static deleteRow(e: JQueryEventObject) {
|
||||
$(e.target).closest("tr").remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditPage {
|
||||
private $mode: JQuery;
|
||||
private $replicas: JQuery;
|
||||
private secret: ConfigTable;
|
||||
private config: ConfigTable;
|
||||
|
||||
constructor() {
|
||||
this.$mode = $("#cb-mode");
|
||||
this.$replicas = $("#txt-replicas");
|
||||
new OptionTable("#table-envs");
|
||||
new OptionTable("#table-slabels");
|
||||
new OptionTable("#table-clabels");
|
||||
new OptionTable("#table-log_driver-options");
|
||||
new PortTable("#table-endpoint-ports");
|
||||
new MountTable("#table-mounts");
|
||||
new ConstraintTable("#table-constraints");
|
||||
new PreferenceTable("#table-preferences");
|
||||
this.secret = new ConfigTable("#table-secrets");
|
||||
this.config = new ConfigTable("#table-configs");
|
||||
|
||||
// register custom validators
|
||||
Validator.register("service-mode", new ServiceModeRule(this.$mode), "Please input a valid integer.");
|
||||
|
||||
// bind events
|
||||
this.$mode.change(e => this.$replicas.toggle(this.$mode.val() != "global"))
|
||||
$("#btn-add-secret").click(() => EditPage.addConfig(this.secret));
|
||||
$("#btn-add-config").click(() => EditPage.addConfig(this.config));
|
||||
}
|
||||
|
||||
private static addConfig(t: ConfigTable) {
|
||||
let dlg = Modal.current();
|
||||
let $cbs = dlg.find(":checked");
|
||||
if ($cbs.length == 0) {
|
||||
dlg.error(`Please select the ${t.name} files.`)
|
||||
} else {
|
||||
dlg.close();
|
||||
$cbs.each((i, cb) => {
|
||||
let $cb = $(cb);
|
||||
t.addRow($cb.val(), $cb.data("name"));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NewPage extends EditPage {
|
||||
private $registry: JQuery;
|
||||
private $registryUrl: JQuery;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.$registryUrl = $("#a-registry-url");
|
||||
this.$registry = $("#cb-registry");
|
||||
this.$registry.change(e => {
|
||||
let url = this.$registry.find("option:selected").data("url") || "";
|
||||
this.$registryUrl.text(url + "/").toggle(url != "");
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Service {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Table = Swirl.Core.ListTable;
|
||||
|
||||
export class ListPage {
|
||||
private table: Table;
|
||||
|
||||
constructor() {
|
||||
this.table = new Table("#table-items");
|
||||
|
||||
// bind events
|
||||
this.table.on("delete-service", this.deleteService.bind(this))
|
||||
.on("scale-service", this.scaleService.bind(this))
|
||||
.on("rollback-service", this.rollbackService.bind(this))
|
||||
.on("restart-service", this.restartService.bind(this));
|
||||
}
|
||||
|
||||
private deleteService(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:eq(0)").text().trim();
|
||||
Modal.confirm(`Are you sure to remove service: <strong>${name}</strong>?`, "Delete service", (dlg, e) => {
|
||||
$ajax.post(`${name}/delete`).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private scaleService(e: JQueryEventObject) {
|
||||
let $target = $(e.target),
|
||||
$tr = $target.closest("tr"),
|
||||
name = $tr.find("td:eq(0)").text().trim();
|
||||
let data = {
|
||||
count: $target.data("replicas"),
|
||||
};
|
||||
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", dlg => {
|
||||
data.count = dlg.find("input[name=count]").val();
|
||||
$ajax.post(`${name}/scale`, data).encoder("form").json<AjaxResult>(() => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private rollbackService(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr"),
|
||||
name = $tr.find("td:eq(0)").text().trim();
|
||||
Modal.confirm(`Are you sure to rollback service: <strong>${name}</strong>?`, "Rollback service", dlg => {
|
||||
$ajax.post(`${name}/rollback`).encoder("form").json<AjaxResult>(() => {
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private restartService(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr"),
|
||||
name = $tr.find("td:eq(0)").text().trim();
|
||||
Modal.confirm(`Are you sure to restart service: <strong>${name}</strong>?`, "Restart service", dlg => {
|
||||
$ajax.post(`${name}/restart`).encoder("form").json<AjaxResult>(() => {
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
///<reference path="../core/chart.ts" />
|
||||
namespace Swirl.Service {
|
||||
import ChartDashboard = Swirl.Core.ChartDashboard;
|
||||
|
||||
export class StatsPage {
|
||||
private dashboard: ChartDashboard;
|
||||
|
||||
constructor() {
|
||||
let $cb_time = $("#cb-time");
|
||||
if ($cb_time.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dashboard = new ChartDashboard("#div-charts", window.charts, {
|
||||
name: "service",
|
||||
key: $("#h2-service-name").text()
|
||||
});
|
||||
dragula([$('#div-charts').get(0)]);
|
||||
|
||||
// bind events
|
||||
$cb_time.change(e => {
|
||||
this.dashboard.setPeriod($(e.target).val());
|
||||
});
|
||||
$("#cb-refresh").change(e => {
|
||||
if ($(e.target).prop("checked")) {
|
||||
this.dashboard.refresh();
|
||||
} else {
|
||||
this.dashboard.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
///<reference path="../../core/core.ts" />
|
||||
namespace Swirl.Service.Template {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class ListPage {
|
||||
constructor() {
|
||||
// bind events
|
||||
let dispatcher = Dispatcher.bind("#table-items");
|
||||
dispatcher.on("delete-template", this.deleteTemplate.bind(this));
|
||||
}
|
||||
|
||||
private deleteTemplate(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let id = $tr.data("id");
|
||||
let name = $tr.find("td:first").text();
|
||||
Modal.confirm(`Are you sure to remove template: <strong>${name}</strong>?`, "Delete template", (dlg, e) => {
|
||||
$ajax.post("delete", { id: id }).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace Swirl.Setting {
|
||||
export class IndexPage {
|
||||
constructor() {
|
||||
$("#ldap-enabled").change(e => {
|
||||
let enabled = $(e.target).prop("checked");
|
||||
$("#fs-ldap").find("input:not(:checkbox)").prop("readonly", !enabled);
|
||||
});
|
||||
$("#ldap-auth-simple,#ldap-auth-bind").click(e => {
|
||||
if ($(e.target).val() == "0") {
|
||||
$("#div-auth-simple").show();
|
||||
$("#div-auth-bind").hide();
|
||||
} else {
|
||||
$("#div-auth-simple").hide();
|
||||
$("#div-auth-bind").show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Stack {
|
||||
import Validator = Swirl.Core.Validator;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Notification = Swirl.Core.Notification;
|
||||
import ValidationRule = Swirl.Core.ValidationRule;
|
||||
|
||||
class ContentRequiredRule implements ValidationRule {
|
||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||
let el = <HTMLInputElement>$input[0];
|
||||
if ($("#type-" + arg).prop("checked")) {
|
||||
return {ok: el.checkValidity ? el.checkValidity() : true, error: el.validationMessage};
|
||||
}
|
||||
return {ok: true}
|
||||
}
|
||||
}
|
||||
|
||||
export class EditPage {
|
||||
private editor: any;
|
||||
|
||||
constructor() {
|
||||
Validator.register("content", new ContentRequiredRule(), "");
|
||||
|
||||
this.editor = CodeMirror.fromTextArea($("#txt-content")[0], {lineNumbers: true});
|
||||
|
||||
$("#file-content").change(e => {
|
||||
let file = <HTMLInputElement>e.target;
|
||||
if (file.files.length > 0) {
|
||||
$('#filename').text(file.files[0].name);
|
||||
}
|
||||
});
|
||||
$("#type-input,#type-upload").click(e => {
|
||||
let type = $(e.target).val();
|
||||
$("#div-input").toggle(type == "input");
|
||||
$("#div-upload").toggle(type == "upload");
|
||||
});
|
||||
$("#btn-submit").click(this.submit.bind(this))
|
||||
}
|
||||
|
||||
private submit(e: JQueryEventObject) {
|
||||
this.editor.save();
|
||||
|
||||
let results = Validator.bind("#div-form").validate();
|
||||
if (results.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = new FormData();
|
||||
data.append('name', $("#name").val());
|
||||
if ($("#type-input").prop("checked")) {
|
||||
data.append('content', $('#txt-content').val());
|
||||
} else {
|
||||
let file = <HTMLInputElement>$('#file-content')[0];
|
||||
data.append('content', file.files[0]);
|
||||
}
|
||||
|
||||
let url = $(e.target).data("url") || "";
|
||||
$ajax.post(url, data).encoder("none").trigger(e.target).json((r: AjaxResult) => {
|
||||
if (r.success) {
|
||||
location.href = "/stack/"
|
||||
} else {
|
||||
Notification.show("danger", `FAILED: ${r.message}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare var CodeMirror: any;
|
||||
@@ -1,46 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Stack {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class ListPage {
|
||||
constructor() {
|
||||
let dispatcher = Dispatcher.bind("#table-items");
|
||||
dispatcher.on("deploy-stack", this.deployStack.bind(this));
|
||||
dispatcher.on("shutdown-stack", this.shutdownStack.bind(this));
|
||||
dispatcher.on("delete-stack", this.deleteStack.bind(this));
|
||||
}
|
||||
|
||||
private deployStack(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to deploy stack: <strong>${name}</strong>?`, "Deploy stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/deploy`).trigger(e.target).json<AjaxResult>(r => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private shutdownStack(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to shutdown stack: <strong>${name}</strong>?`, "Shutdown stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/shutdown`).trigger(e.target).json<AjaxResult>(r => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private deleteStack(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to remove archive: <strong>${name}</strong>?`, "Delete stack", (dlg, e) => {
|
||||
$ajax.post(`${name}/delete`).trigger(e.target).json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Task {
|
||||
export class LogsPage {
|
||||
private $stdout: JQuery;
|
||||
private $stderr: JQuery;
|
||||
private $line: JQuery;
|
||||
private $timestamps: JQuery;
|
||||
private $refresh: JQuery;
|
||||
private timer: number;
|
||||
private refreshInterval = 3000;
|
||||
|
||||
constructor() {
|
||||
this.$line = $("#txt-line");
|
||||
this.$timestamps = $("#cb-timestamps");
|
||||
this.$refresh = $("#cb-refresh");
|
||||
this.$stdout = $("#txt-stdout");
|
||||
this.$stderr = $("#txt-stderr");
|
||||
|
||||
this.$refresh.change(e => {
|
||||
let elem = <HTMLInputElement>(e.target);
|
||||
if (elem.checked) {
|
||||
this.refreshData();
|
||||
} else if (this.timer > 0) {
|
||||
window.clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
}
|
||||
});
|
||||
|
||||
this.refreshData();
|
||||
// let ws = new WebSocket("ws://" + location.host + location.pathname + "_ws");
|
||||
// ws.onopen = e => console.log("open");
|
||||
// ws.onclose = e => console.log("close");
|
||||
// ws.onmessage = e => console.log("message: " + e.data);
|
||||
}
|
||||
|
||||
private refreshData() {
|
||||
let args: any = {
|
||||
line: this.$line.val(),
|
||||
timestamps: this.$timestamps.prop("checked"),
|
||||
};
|
||||
$ajax.get('fetch_logs', args).json((r: any) => {
|
||||
this.$stdout.val(r.stdout);
|
||||
this.$stderr.val(r.stderr);
|
||||
this.$stdout.get(0).scrollTop = this.$stdout.get(0).scrollHeight;
|
||||
this.$stderr.get(0).scrollTop = this.$stderr.get(0).scrollHeight;
|
||||
});
|
||||
|
||||
if (this.$refresh.prop("checked")) {
|
||||
this.timer = setTimeout(this.refreshData.bind(this), this.refreshInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.User {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
|
||||
export class ListPage {
|
||||
constructor() {
|
||||
// bind events
|
||||
Dispatcher.bind("#table-items")
|
||||
.on("delete-user", this.deleteUser.bind(this))
|
||||
.on("block-user", this.blockUser.bind(this))
|
||||
.on("unblock-user", this.unblockUser.bind(this));
|
||||
}
|
||||
|
||||
private deleteUser(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let id = $tr.data("id");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to remove user: <strong>${name}</strong>?`, "Delete user", (dlg, e) => {
|
||||
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private blockUser(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let id = $tr.data("id");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to block user: <strong>${name}</strong>?`, "Block user", (dlg, e) => {
|
||||
$ajax.post("block", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private unblockUser(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let id = $tr.data("id");
|
||||
let name = $tr.find("td:first").text().trim();
|
||||
Modal.confirm(`Are you sure to unblock user: <strong>${name}</strong>?`, "Unblock user", (dlg, e) => {
|
||||
$ajax.post("unblock", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Volume {
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import AjaxResult = Swirl.Core.AjaxResult;
|
||||
import Table = Swirl.Core.ListTable;
|
||||
|
||||
export class ListPage {
|
||||
private table: Table;
|
||||
|
||||
constructor() {
|
||||
this.table = new Table("#table-items");
|
||||
|
||||
// bind events
|
||||
this.table.on("delete-volume", this.deleteVolume.bind(this));
|
||||
$("#btn-delete").click(this.deleteVolumes.bind(this));
|
||||
$("#btn-prune").click(this.pruneVolumes.bind(this));
|
||||
}
|
||||
|
||||
private deleteVolume(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:eq(1)").text().trim();
|
||||
Modal.confirm(`Are you sure to remove volume: <strong>${name}</strong>?`, "Delete volume", (dlg, e) => {
|
||||
$ajax.post("delete", { names: name }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private deleteVolumes(e: JQueryEventObject) {
|
||||
let names = this.table.selectedKeys();
|
||||
if (names.length == 0) {
|
||||
Modal.alert("Please select one or more items.")
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm(`Are you sure to remove ${names.length} volumes?`, "Delete volumes", (dlg, e) => {
|
||||
$ajax.post("delete", { names: names.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
|
||||
this.table.selectedRows().remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private pruneVolumes(e: JQueryEventObject) {
|
||||
Modal.confirm(`Are you sure to remove all unused volumes?`, "Prune volumes", (dlg, e) => {
|
||||
$ajax.post("prune").trigger(e.target).json<AjaxResult>(r => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Volume {
|
||||
import OptionTable = Swirl.Core.OptionTable;
|
||||
|
||||
export class NewPage {
|
||||
constructor() {
|
||||
new OptionTable("#table-options");
|
||||
new OptionTable("#table-labels");
|
||||
|
||||
$("#drivers").find(":radio[name=driver]").change(e => {
|
||||
$("#txt-custom-driver").prop("disabled", $(e.target).val() != "other");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"noImplicitAny": true,
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
"outFile": "js/swirl.js",
|
||||
"lib": ["dom", "es2015.promise", "es5"]
|
||||
}
|
||||
}
|
||||
47
assets/swirl/typings/dragula.d.ts
vendored
47
assets/swirl/typings/dragula.d.ts
vendored
@@ -1,47 +0,0 @@
|
||||
// Type definitions for dragula v2.1.2
|
||||
// Project: http://bevacqua.github.io/dragula/
|
||||
// Definitions by: Paul Welter <https://github.com/pwelter34>
|
||||
// Yang He <https://github.com/abruzzihraig>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
declare var dragula: dragula.Dragula;
|
||||
|
||||
export = dragula;
|
||||
export as namespace dragula;
|
||||
|
||||
declare namespace dragula {
|
||||
interface DragulaOptions {
|
||||
containers?: Element[];
|
||||
isContainer?: (el?: Element) => boolean;
|
||||
moves?: (el?: Element, container?: Element, handle?: Element, sibling?: Element) => boolean;
|
||||
accepts?: (el?: Element, target?: Element, source?: Element, sibling?: Element) => boolean;
|
||||
invalid?: (el?: Element, target?: Element) => boolean;
|
||||
direction?: string;
|
||||
copy?: ((el: Element, source: Element) => boolean) | boolean;
|
||||
revertOnSpill?: boolean;
|
||||
removeOnSpill?: boolean;
|
||||
delay?: boolean | number;
|
||||
mirrorContainer?: Element;
|
||||
ignoreInputTextSelection?: boolean;
|
||||
}
|
||||
|
||||
interface Drake {
|
||||
containers: Element[];
|
||||
dragging: boolean;
|
||||
start(item:Element): void;
|
||||
end(): void;
|
||||
cancel(revert:boolean): void;
|
||||
cancel(): void;
|
||||
remove(): void;
|
||||
on(events: string, callback: Function): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
interface Dragula {
|
||||
(containers: Element[], options: DragulaOptions): Drake;
|
||||
(containers: Element, options: DragulaOptions): Drake;
|
||||
(containers: Element[]): Drake;
|
||||
(options: DragulaOptions): Drake;
|
||||
(): Drake;
|
||||
}
|
||||
}
|
||||
199
assets/swirl/typings/echarts.d.ts
vendored
199
assets/swirl/typings/echarts.d.ts
vendored
@@ -1,199 +0,0 @@
|
||||
// Type definitions for echarts
|
||||
// Project: http://echarts.baidu.com/
|
||||
// Definitions by: Xie Jingyang <https://github.com/xieisabug>, AntiMoron <https://github.com/AntiMoron>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// TypeScript Version: 2.3
|
||||
|
||||
declare namespace echarts {
|
||||
function init(dom: HTMLDivElement | HTMLCanvasElement, theme?: Object | string, opts?: {
|
||||
devicePixelRatio?: number
|
||||
renderer?: string
|
||||
}): ECharts;
|
||||
|
||||
const graphic: Graphic;
|
||||
|
||||
interface Graphic {
|
||||
clipPointsByRect(points: number[][], rect: ERectangle): number[][];
|
||||
clipRectByRect(targetRect: ERectangle, rect: ERectangle): ERectangle;
|
||||
LinearGradient: { new(x: number, y: number, x2: number, y2: number, colorStops: Array<Object>, globalCoord?: boolean): LinearGradient }
|
||||
}
|
||||
|
||||
function connect(group: string | Array<string>): void;
|
||||
|
||||
function disConnect(group: string): void;
|
||||
|
||||
function dispose(target: ECharts | HTMLDivElement | HTMLCanvasElement): void;
|
||||
|
||||
function getInstanceByDom(target: HTMLDivElement | HTMLCanvasElement): ECharts;
|
||||
|
||||
function registerMap(mapName: string, geoJson: Object, specialAreas?: Object): void;
|
||||
|
||||
function registerTheme(themeName: string, theme: Object): void;
|
||||
|
||||
interface MapObj {
|
||||
/** geoJson data for map */
|
||||
geoJson: object,
|
||||
/** special areas fro map */
|
||||
specialAreas: object
|
||||
}
|
||||
|
||||
function getMap(mapName: string): MapObj;
|
||||
|
||||
interface LinearGradient {
|
||||
colorStops: Array<Object>;
|
||||
global: boolean;
|
||||
type: string;
|
||||
x: number
|
||||
x2: number
|
||||
y: number
|
||||
y2: number
|
||||
}
|
||||
|
||||
interface ECharts {
|
||||
group: string
|
||||
|
||||
setOption(option: EChartOption, notMerge?: boolean, notRefreshImmediately?: boolean): void
|
||||
|
||||
getWidth(): number
|
||||
|
||||
getHeight(): number
|
||||
|
||||
getDom(): HTMLCanvasElement | HTMLDivElement
|
||||
|
||||
getOption(): Object
|
||||
|
||||
resize(): void
|
||||
|
||||
dispatchAction(payload: Object): void
|
||||
|
||||
on(eventName: string, handler: Function, context?: Object): void
|
||||
|
||||
off(eventName: string, handler?: Function): void
|
||||
|
||||
showLoading(type?: string, opts?: Object): void
|
||||
|
||||
hideLoading(): void
|
||||
|
||||
getDataURL(opts: {
|
||||
/** 导出的格式,可选 png, jpeg */
|
||||
type?: string,
|
||||
/** 导出的图片分辨率比例,默认为 1。*/
|
||||
pixelRatio?: number,
|
||||
/** 导出的图片背景色,默认使用 option 里的 backgroundColor */
|
||||
backgroundColor?: string
|
||||
}): string
|
||||
|
||||
getConnectedDataURL(opts: {
|
||||
/** 导出的格式,可选 png, jpeg */
|
||||
type: string,
|
||||
/** 导出的图片分辨率比例,默认为 1。 */
|
||||
pixelRatio: number,
|
||||
/** 导出的图片背景色,默认使用 option 里的 backgroundColor */
|
||||
backgroundColor: string
|
||||
}): string
|
||||
|
||||
clear(): void
|
||||
|
||||
isDisposed(): boolean
|
||||
|
||||
dispose(): void
|
||||
|
||||
/** 转换逻辑点到像素 */
|
||||
convertToPixel(finder: ConvertFinder | string, value: string | Array<any>): string | Array<any>
|
||||
|
||||
convertFromPixel(finder: ConvertFinder | string, value: Array<any> | string): Array<any> | string
|
||||
|
||||
containPixel(finder: ConvertFinder | string,
|
||||
/** 要被判断的点,为像素坐标值,以 echarts 实例的 dom 节点的左上角为坐标 [0, 0] 点。*/
|
||||
value: any[]): boolean
|
||||
}
|
||||
|
||||
interface ConvertFinder {
|
||||
seriesIndex?: number,
|
||||
seriesId?: string,
|
||||
seriesName?: string,
|
||||
geoIndex?: number,
|
||||
geoId?: string,
|
||||
geoName?: string,
|
||||
xAxisIndex?: number,
|
||||
xAxisId?: string,
|
||||
xAxisName?: string,
|
||||
yAxisIndex?: number,
|
||||
yAxisId?: string,
|
||||
yAxisName?: string,
|
||||
gridIndex?: number,
|
||||
gridId?: string
|
||||
gridName?: string
|
||||
}
|
||||
|
||||
interface ERectangle {
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
|
||||
interface EChartOption {
|
||||
title?: EChartTitleOption
|
||||
legend?: Object,
|
||||
grid?: Object,
|
||||
xAxis?: Object,
|
||||
yAxis?: Object,
|
||||
polar?: Object,
|
||||
radiusAxis?: Object,
|
||||
angleAxis?: Object,
|
||||
radar?: Object,
|
||||
dataZoom?: Array<Object>,
|
||||
visualMap?: Array<Object>,
|
||||
tooltip?: Object,
|
||||
toolbox?: Object,
|
||||
geo?: Object,
|
||||
parallel?: Object,
|
||||
parallelAxis?: Object,
|
||||
timeline?: Object,
|
||||
series?: Array<Object>,
|
||||
color?: Array<Object>,
|
||||
backgroundColor?: string,
|
||||
textStyle?: Object,
|
||||
animation?: boolean,
|
||||
animationDuration?: number,
|
||||
animationEasing?: string,
|
||||
animationDurationUpdate?: number,
|
||||
animationEasingUpdate?: string
|
||||
}
|
||||
|
||||
interface EChartTitleOption {
|
||||
show?: boolean;
|
||||
text?: string;
|
||||
link?: string,
|
||||
target?: string,
|
||||
textStyle?: Object,
|
||||
subtext?: string,
|
||||
sublink?: string,
|
||||
subtarget?: string,
|
||||
subtextStyle?: Object,
|
||||
padding?: number,
|
||||
itemGap?: number,
|
||||
zlevel?: number,
|
||||
z?: number,
|
||||
left?: string,
|
||||
top?: string,
|
||||
right?: string,
|
||||
bottom?: string,
|
||||
backgroundColor?: string,
|
||||
borderColor?: string,
|
||||
borderWidth?: number,
|
||||
shadowBlur?: number,
|
||||
shadowColor?: number,
|
||||
shadowOffsetX?: number,
|
||||
shadowOffsetY?: number,
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'echarts' {
|
||||
export = echarts;
|
||||
}
|
||||
|
||||
declare module 'echarts/lib/echarts' {
|
||||
export = echarts;
|
||||
}
|
||||
3218
assets/swirl/typings/jquery.d.ts
vendored
3218
assets/swirl/typings/jquery.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fit = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function proposeGeometry(term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
|
||||
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
var elementStyle = window.getComputedStyle(term.element);
|
||||
var elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
var elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
var elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
var availableHeight = parentElementHeight - elementPaddingVer;
|
||||
var availableWidth = parentElementWidth - elementPaddingHor - term.viewport.scrollBarWidth;
|
||||
var geometry = {
|
||||
cols: Math.floor(availableWidth / term.renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / term.renderer.dimensions.actualCellHeight)
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
exports.proposeGeometry = proposeGeometry;
|
||||
function fit(term) {
|
||||
var geometry = proposeGeometry(term);
|
||||
if (geometry) {
|
||||
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
|
||||
term.renderer.clear();
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.fit = fit;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.proposeGeometry = function () {
|
||||
return proposeGeometry(this);
|
||||
};
|
||||
terminalConstructor.prototype.fit = function () {
|
||||
fit(this);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=fit.js.map
|
||||
@@ -1,159 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* 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.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
font-family: courier-new, courier, monospace;
|
||||
font-feature-settings: "liga" 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
/*
|
||||
* HACK: to fix IE's blinking cursor
|
||||
* Move textarea out of the screen to the far left, so that the cursor is not visible.
|
||||
*/
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -10;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility,
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
122
biz/biz.go
122
biz/biz.go
@@ -1,15 +1,123 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/swirl/dao"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/app/container"
|
||||
"github.com/cuigh/auxo/data"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func do(fn func(d dao.Interface)) {
|
||||
d, err := dao.Get()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "failed to load storage engine"))
|
||||
func mapToOptions(m map[string]string) (opts data.Options) {
|
||||
if len(m) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fn(d)
|
||||
opts = data.Options{}
|
||||
for k, v := range m {
|
||||
opts = append(opts, data.Option{Name: k, Value: v})
|
||||
}
|
||||
sort.Slice(opts, func(i, j int) bool {
|
||||
return opts[i].Name < opts[j].Name
|
||||
})
|
||||
return opts
|
||||
}
|
||||
|
||||
func envToOptions(env []string) (opts data.Options) {
|
||||
if len(env) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
opts = make(data.Options, len(env))
|
||||
for i, e := range env {
|
||||
opts[i] = data.ParseOption(e, "=")
|
||||
}
|
||||
sort.Slice(opts, func(i, j int) bool {
|
||||
return opts[i].Name < opts[j].Name
|
||||
})
|
||||
return opts
|
||||
}
|
||||
|
||||
func toEnv(opts data.Options) (env []string) {
|
||||
if len(opts) > 0 {
|
||||
env = make([]string, len(opts))
|
||||
for i, opt := range opts {
|
||||
env[i] = opt.Name + "=" + opt.Value
|
||||
}
|
||||
sort.Strings(env)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func toMap(opts data.Options) (m map[string]string) {
|
||||
if len(opts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m = make(map[string]string)
|
||||
for _, opt := range opts {
|
||||
m[opt.Name] = opt.Value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseArgs(args string) []string {
|
||||
if args == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(args, " ")
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
return t.Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// generate 8-chars short id, only suitable for small dataset
|
||||
func createId() string {
|
||||
id := [12]byte(primitive.NewObjectID())
|
||||
return fmt.Sprintf("%x", md5.Sum(id[:]))[:8]
|
||||
}
|
||||
|
||||
func normalizeImage(image string) string {
|
||||
// remove hash added by docker
|
||||
if i := strings.Index(image, "@sha256:"); i > 0 {
|
||||
image = image[:i]
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
func indentJSON(raw []byte) (s string, err error) {
|
||||
buf := &bytes.Buffer{}
|
||||
err = json.Indent(buf, raw, "", " ")
|
||||
if err == nil {
|
||||
s = buf.String()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
container.Put(NewNetwork)
|
||||
container.Put(NewNode)
|
||||
container.Put(NewRegistry)
|
||||
container.Put(NewService)
|
||||
container.Put(NewTask)
|
||||
container.Put(NewConfig)
|
||||
container.Put(NewSecret)
|
||||
container.Put(NewStack)
|
||||
container.Put(NewImage)
|
||||
container.Put(NewContainer)
|
||||
container.Put(NewVolume)
|
||||
container.Put(NewUser)
|
||||
container.Put(NewRole)
|
||||
container.Put(NewEvent)
|
||||
container.Put(NewSetting)
|
||||
container.Put(NewMetric)
|
||||
container.Put(NewChart)
|
||||
container.Put(NewSystem)
|
||||
}
|
||||
|
||||
371
biz/chart.go
371
biz/chart.go
@@ -1,6 +1,7 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -10,156 +11,124 @@ import (
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/dao"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
||||
// Chart return a chart biz instance.
|
||||
var Chart = newChartBiz()
|
||||
type ChartBiz interface {
|
||||
Search(title, dashboard string, pageIndex, pageSize int) (charts []*Chart, total int, err error)
|
||||
Delete(id, title string, user web.User) (err error)
|
||||
Find(id string) (chart *model.Chart, err error)
|
||||
Batch(ids ...string) (charts []*model.Chart, err error)
|
||||
Create(chart *model.Chart, user web.User) (err error)
|
||||
Update(chart *model.Chart, user web.User) (err error)
|
||||
FetchData(key string, ids []string, period time.Duration) (data.Map, error)
|
||||
FindDashboard(name, key string) (dashboard *Dashboard, err error)
|
||||
UpdateDashboard(dashboard *model.Dashboard, user web.User) (err error)
|
||||
}
|
||||
|
||||
func NewChart(d dao.Interface, mb MetricBiz, eb EventBiz) ChartBiz {
|
||||
return &chartBiz{
|
||||
d: d,
|
||||
mb: mb,
|
||||
eb: eb,
|
||||
builtin: []*model.Chart{
|
||||
model.NewChart("service", "$cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="${service}"}[5m]) * 100`, "percent:100", 60),
|
||||
model.NewChart("service", "$memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="${service}"}`, "size:bytes", 60),
|
||||
model.NewChart("service", "$network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes", 60),
|
||||
model.NewChart("service", "$network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes", 60),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type chartBiz struct {
|
||||
builtin []*model.Chart
|
||||
d dao.Interface
|
||||
mb MetricBiz
|
||||
eb EventBiz
|
||||
}
|
||||
|
||||
func newChartBiz() *chartBiz {
|
||||
b := &chartBiz{}
|
||||
b.builtin = append(b.builtin, model.NewChart("service", "$cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="${service}"}[5m]) * 100`, "percent:100"))
|
||||
b.builtin = append(b.builtin, model.NewChart("service", "$memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="${service}"}`, "size:bytes"))
|
||||
b.builtin = append(b.builtin, model.NewChart("service", "$network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes"))
|
||||
b.builtin = append(b.builtin, model.NewChart("service", "$network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes"))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *chartBiz) List() (charts []*model.Chart, err error) {
|
||||
do(func(d dao.Interface) {
|
||||
charts, err = d.ChartList()
|
||||
})
|
||||
func (b *chartBiz) Search(title, dashboard string, pageIndex, pageSize int) (charts []*Chart, total int, err error) {
|
||||
var list []*model.Chart
|
||||
list, total, err = b.d.ChartList(context.TODO(), title, dashboard, pageIndex, pageSize)
|
||||
if err == nil {
|
||||
charts = make([]*Chart, len(list))
|
||||
for i, c := range list {
|
||||
charts[i] = newChart(c)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) Create(chart *model.Chart, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
// chart.CreatedAt = time.Now()
|
||||
// chart.UpdatedAt = chart.CreatedAt
|
||||
err = d.ChartCreate(chart)
|
||||
})
|
||||
chart.ID = createId()
|
||||
chart.CreatedAt = time.Now()
|
||||
chart.UpdatedAt = chart.CreatedAt
|
||||
err = b.d.ChartCreate(context.TODO(), chart)
|
||||
if err == nil {
|
||||
b.eb.CreateChart(EventActionCreate, chart.ID, chart.Title, user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) Delete(id string, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
err = d.ChartDelete(id)
|
||||
})
|
||||
func (b *chartBiz) Delete(id, title string, user web.User) (err error) {
|
||||
err = b.d.ChartDelete(context.TODO(), id)
|
||||
if err == nil {
|
||||
b.eb.CreateChart(EventActionDelete, id, title, user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) Get(name string) (chart *model.Chart, err error) {
|
||||
do(func(d dao.Interface) {
|
||||
chart, err = d.ChartGet(name)
|
||||
if len(chart.Metrics) == 0 {
|
||||
chart.Metrics = append(chart.Metrics, model.ChartMetric{Legend: chart.Legend, Query: chart.Query})
|
||||
}
|
||||
})
|
||||
func (b *chartBiz) Find(id string) (chart *model.Chart, err error) {
|
||||
chart, err = b.d.ChartGet(context.TODO(), id)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) Batch(names ...string) (charts []*model.Chart, err error) {
|
||||
do(func(d dao.Interface) {
|
||||
charts, err = d.ChartBatch(names...)
|
||||
if err == nil {
|
||||
for _, c := range charts {
|
||||
if len(c.Metrics) == 0 {
|
||||
c.Metrics = append(c.Metrics, model.ChartMetric{Legend: c.Legend, Query: c.Query})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
func (b *chartBiz) Batch(ids ...string) (charts []*model.Chart, err error) {
|
||||
charts, err = b.d.ChartBatch(context.TODO(), ids...)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) Update(chart *model.Chart, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
// chart.UpdatedAt = time.Now()
|
||||
err = d.ChartUpdate(chart)
|
||||
})
|
||||
chart.UpdatedAt = time.Now()
|
||||
err = b.d.ChartUpdate(context.TODO(), chart)
|
||||
if err == nil {
|
||||
b.eb.CreateChart(EventActionUpdate, chart.ID, chart.Title, user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) GetServiceCharts(name string) (charts []*model.Chart, err error) {
|
||||
// service, _, err := docker.ServiceInspect(name)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
func (b *chartBiz) FindDashboard(name, key string) (dashboard *Dashboard, err error) {
|
||||
var d *model.Dashboard
|
||||
d, err = b.d.DashboardGet(context.TODO(), name, key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if label := service.Spec.Labels["swirl.metrics"]; label != "" {
|
||||
// names := strings.Split(label, ",")
|
||||
// }
|
||||
charts = b.builtin
|
||||
if d == nil {
|
||||
dashboard = defaultDashboard(name, key)
|
||||
} else {
|
||||
dashboard = newDashboard(d)
|
||||
}
|
||||
err = b.fillCharts(dashboard)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) GetDashboard(name, key string) (dashboard *model.ChartDashboard, err error) {
|
||||
do(func(d dao.Interface) {
|
||||
dashboard, err = d.DashboardGet(name, key)
|
||||
})
|
||||
return
|
||||
func (b *chartBiz) UpdateDashboard(dashboard *model.Dashboard, user web.User) (err error) {
|
||||
return b.d.DashboardUpdate(context.TODO(), dashboard)
|
||||
}
|
||||
|
||||
func (b *chartBiz) UpdateDashboard(dashboard *model.ChartDashboard, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
err = d.DashboardUpdate(dashboard)
|
||||
})
|
||||
return
|
||||
}
|
||||
func (b *chartBiz) FetchData(key string, ids []string, period time.Duration) (data.Map, error) {
|
||||
if !b.mb.Enabled() {
|
||||
return data.Map{}, nil
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func (b *chartBiz) GetDashboardCharts(dashboard *model.ChartDashboard) (charts []*model.Chart, err error) {
|
||||
do(func(d dao.Interface) {
|
||||
if dashboard == nil || len(dashboard.Charts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
names := make([]string, len(dashboard.Charts))
|
||||
for i, c := range dashboard.Charts {
|
||||
names[i] = c.Name
|
||||
}
|
||||
|
||||
var cs []*model.Chart
|
||||
cs, err = b.getCharts(names)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(cs) > 0 {
|
||||
m := make(map[string]*model.Chart)
|
||||
for _, c := range cs {
|
||||
m[c.Name] = c
|
||||
}
|
||||
for _, c := range dashboard.Charts {
|
||||
if chart := m[c.Name]; chart != nil {
|
||||
if c.Width > 0 {
|
||||
chart.Width = c.Width
|
||||
}
|
||||
if c.Height > 0 {
|
||||
chart.Height = c.Height
|
||||
}
|
||||
//if len(c.Colors) > 0 {
|
||||
// chart.Colors = c.Colors
|
||||
//}
|
||||
charts = append(charts, chart)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) FetchDatas(key string, names []string, period time.Duration) (data.Map, error) {
|
||||
charts, err := b.getCharts(names)
|
||||
charts, err := b.getCharts(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
name string
|
||||
id string
|
||||
data interface{}
|
||||
err error
|
||||
}
|
||||
@@ -169,7 +138,7 @@ func (b *chartBiz) FetchDatas(key string, names []string, period time.Duration)
|
||||
start := end.Add(-period)
|
||||
for _, chart := range charts {
|
||||
go func(c *model.Chart) {
|
||||
d := Data{name: c.Name}
|
||||
d := Data{id: c.ID}
|
||||
switch c.Type {
|
||||
case "line", "bar":
|
||||
d.data, d.err = b.fetchMatrixData(c, key, start, end)
|
||||
@@ -190,42 +159,48 @@ func (b *chartBiz) FetchDatas(key string, names []string, period time.Duration)
|
||||
if d.err != nil {
|
||||
log.Get("metric").Error(d.err)
|
||||
} else {
|
||||
ds.Set(d.name, d.data)
|
||||
ds.Set(d.id, d.data)
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func (b *chartBiz) fetchMatrixData(chart *model.Chart, key string, start, end time.Time) (*model.ChartMatrixData, error) {
|
||||
var cmd *model.ChartMatrixData
|
||||
func (b *chartBiz) fetchMatrixData(chart *model.Chart, key string, start, end time.Time) (md *MatrixData, err error) {
|
||||
var (
|
||||
q string
|
||||
d *MatrixData
|
||||
)
|
||||
for i, m := range chart.Metrics {
|
||||
q, err := b.formatQuery(m.Query, chart.Dashboard, key)
|
||||
q, err = b.formatQuery(m.Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d, err := Metric.GetMatrix(q, m.Legend, start, end); err != nil {
|
||||
if d, err = b.mb.GetMatrix(q, m.Legend, start, end); err != nil {
|
||||
log.Get("metric").Error(err)
|
||||
} else if i == 0 {
|
||||
cmd = d
|
||||
md = d
|
||||
} else {
|
||||
cmd.Legend = append(cmd.Legend, d.Legend...)
|
||||
cmd.Series = append(cmd.Series, d.Series...)
|
||||
md.Legend = append(md.Legend, d.Legend...)
|
||||
md.Series = append(md.Series, d.Series...)
|
||||
}
|
||||
}
|
||||
return cmd, nil
|
||||
return md, nil
|
||||
}
|
||||
|
||||
func (b *chartBiz) fetchVectorData(chart *model.Chart, key string, end time.Time) (*model.ChartVectorData, error) {
|
||||
var cvd *model.ChartVectorData
|
||||
func (b *chartBiz) fetchVectorData(chart *model.Chart, key string, end time.Time) (cvd *VectorData, err error) {
|
||||
var (
|
||||
q string
|
||||
d *VectorData
|
||||
)
|
||||
for i, m := range chart.Metrics {
|
||||
query, err := b.formatQuery(m.Query, chart.Dashboard, key)
|
||||
q, err = b.formatQuery(m.Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d, err := Metric.GetVector(query, m.Legend, end); err != nil {
|
||||
if d, err = b.mb.GetVector(q, m.Legend, end); err != nil {
|
||||
log.Get("metric").Error(err)
|
||||
} else if i == 0 {
|
||||
cvd = d
|
||||
@@ -237,18 +212,18 @@ func (b *chartBiz) fetchVectorData(chart *model.Chart, key string, end time.Time
|
||||
return cvd, nil
|
||||
}
|
||||
|
||||
func (b *chartBiz) fetchScalarData(chart *model.Chart, key string, end time.Time) (*model.ChartValue, error) {
|
||||
func (b *chartBiz) fetchScalarData(chart *model.Chart, key string, end time.Time) (*VectorValue, error) {
|
||||
query, err := b.formatQuery(chart.Metrics[0].Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := Metric.GetScalar(query, end)
|
||||
v, err := b.mb.GetScalar(query, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.ChartValue{
|
||||
return &VectorValue{
|
||||
//Name: "",
|
||||
Value: v,
|
||||
}, nil
|
||||
@@ -274,28 +249,148 @@ func (b *chartBiz) formatQuery(query, dashboard, key string) (string, error) {
|
||||
return "", errs[0]
|
||||
}
|
||||
|
||||
func (b *chartBiz) getCharts(names []string) (charts []*model.Chart, err error) {
|
||||
func (b *chartBiz) getCharts(ids []string) (charts map[string]*model.Chart, err error) {
|
||||
var (
|
||||
customNames []string
|
||||
customIds []string
|
||||
customCharts []*model.Chart
|
||||
)
|
||||
|
||||
for _, n := range names {
|
||||
if n[0] == '$' {
|
||||
charts = make(map[string]*model.Chart)
|
||||
for _, id := range ids {
|
||||
if id[0] == '$' {
|
||||
for _, c := range b.builtin {
|
||||
if c.Name == n {
|
||||
charts = append(charts, c)
|
||||
if c.ID == id {
|
||||
charts[id] = c
|
||||
}
|
||||
}
|
||||
} else {
|
||||
customNames = append(customNames, n)
|
||||
customIds = append(customIds, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(customNames) > 0 {
|
||||
if customCharts, err = b.Batch(customNames...); err == nil {
|
||||
charts = append(charts, customCharts...)
|
||||
if len(customIds) > 0 {
|
||||
if customCharts, err = b.Batch(customIds...); err == nil {
|
||||
for _, chart := range customCharts {
|
||||
charts[chart.ID] = chart
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) fillCharts(d *Dashboard) (err error) {
|
||||
if len(d.Charts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
m map[string]*model.Chart
|
||||
ids = make([]string, len(d.Charts))
|
||||
)
|
||||
|
||||
for i, c := range d.Charts {
|
||||
ids[i] = c.ID
|
||||
}
|
||||
|
||||
m, err = b.getCharts(ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, info := range d.Charts {
|
||||
if c := m[info.ID]; c != nil {
|
||||
info.Type = c.Type
|
||||
info.Title = c.Title
|
||||
info.Unit = c.Unit
|
||||
if info.Width == 0 {
|
||||
info.Width = c.Width
|
||||
}
|
||||
if info.Height == 0 {
|
||||
info.Height = c.Height
|
||||
}
|
||||
info.Margin.Left = c.Margin.Left
|
||||
info.Margin.Right = c.Margin.Right
|
||||
info.Margin.Top = c.Margin.Top
|
||||
info.Margin.Bottom = c.Margin.Bottom
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Dashboard struct {
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Period int32 `json:"period,omitempty"` // data range in minutes
|
||||
Interval int32 `json:"interval,omitempty"` // refresh interval in seconds, 0 means disabled.
|
||||
Charts []*Chart `json:"charts,omitempty"`
|
||||
}
|
||||
|
||||
type Chart struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title" valid:"required"`
|
||||
Description string `json:"desc,omitempty"`
|
||||
Dashboard string `json:"dashboard,omitempty"`
|
||||
Type string `json:"type" valid:"required"`
|
||||
Unit string `json:"unit"`
|
||||
Width int32 `json:"width" valid:"required"`
|
||||
Height int32 `json:"height" valid:"required"`
|
||||
Margin struct {
|
||||
Left int32 `json:"left,omitempty"`
|
||||
Right int32 `json:"right,omitempty"`
|
||||
Top int32 `json:"top,omitempty"`
|
||||
Bottom int32 `json:"bottom,omitempty"`
|
||||
} `json:"margin"`
|
||||
Metrics []model.ChartMetric `json:"metrics" valid:"required"`
|
||||
Options data.Map `json:"options,omitempty"`
|
||||
CreatedAt string `json:"createdAt,omitempty" copier:"-"`
|
||||
UpdatedAt string `json:"updatedAt,omitempty" copier:"-"`
|
||||
}
|
||||
|
||||
func newChart(c *model.Chart) *Chart {
|
||||
chart := &Chart{
|
||||
CreatedAt: formatTime(c.CreatedAt),
|
||||
UpdatedAt: formatTime(c.UpdatedAt),
|
||||
}
|
||||
if err := copier.CopyWithOption(chart, c, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return chart
|
||||
}
|
||||
|
||||
func defaultDashboard(name, key string) *Dashboard {
|
||||
d := &Dashboard{
|
||||
Name: name,
|
||||
Key: key,
|
||||
Period: 30,
|
||||
Interval: 15,
|
||||
}
|
||||
if name == "service" {
|
||||
d.Charts = []*Chart{
|
||||
{ID: "$cpu"},
|
||||
{ID: "$memory"},
|
||||
{ID: "$network_in"},
|
||||
{ID: "$network_out"},
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func newDashboard(d *model.Dashboard) *Dashboard {
|
||||
dashboard := &Dashboard{
|
||||
Name: d.Name,
|
||||
Key: d.Key,
|
||||
Period: d.Period,
|
||||
Interval: d.Interval,
|
||||
}
|
||||
if len(d.Charts) > 0 {
|
||||
dashboard.Charts = make([]*Chart, len(d.Charts))
|
||||
for i, c := range d.Charts {
|
||||
dashboard.Charts[i] = &Chart{
|
||||
ID: c.ID,
|
||||
Width: c.Width,
|
||||
Height: c.Height,
|
||||
}
|
||||
}
|
||||
}
|
||||
return dashboard
|
||||
}
|
||||
|
||||
136
biz/config.go
Normal file
136
biz/config.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/docker"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Version uint64 `json:"version"`
|
||||
Data string `json:"data"`
|
||||
Labels data.Options `json:"labels,omitempty"`
|
||||
Templating Driver `json:"templating"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type Driver struct {
|
||||
Name string `json:"name"`
|
||||
Options data.Options `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
func newConfig(c *swarm.Config) *Config {
|
||||
config := &Config{
|
||||
ID: c.ID,
|
||||
Name: c.Spec.Name,
|
||||
Version: c.Version.Index,
|
||||
Data: string(c.Spec.Data),
|
||||
Labels: mapToOptions(c.Spec.Labels),
|
||||
CreatedAt: formatTime(c.CreatedAt),
|
||||
UpdatedAt: formatTime(c.UpdatedAt),
|
||||
}
|
||||
if c.Spec.Templating != nil {
|
||||
config.Templating.Name = c.Spec.Templating.Name
|
||||
config.Templating.Options = mapToOptions(c.Spec.Templating.Options)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
type ConfigBiz interface {
|
||||
Search(name string, pageIndex, pageSize int) (configs []*Config, total int, err error)
|
||||
Find(id string) (config *Config, raw string, err error)
|
||||
Delete(id, name string, user web.User) (err error)
|
||||
Create(c *Config, user web.User) (err error)
|
||||
Update(c *Config, user web.User) (err error)
|
||||
}
|
||||
|
||||
func NewConfig(d *docker.Docker, eb EventBiz) ConfigBiz {
|
||||
return &configBiz{d: d, eb: eb}
|
||||
}
|
||||
|
||||
type configBiz struct {
|
||||
d *docker.Docker
|
||||
eb EventBiz
|
||||
}
|
||||
|
||||
func (b *configBiz) Find(id string) (config *Config, raw string, err error) {
|
||||
var (
|
||||
c swarm.Config
|
||||
r []byte
|
||||
)
|
||||
c, r, err = b.d.ConfigInspect(context.TODO(), id)
|
||||
if err == nil {
|
||||
raw, err = indentJSON(r)
|
||||
}
|
||||
if err == nil {
|
||||
config = newConfig(&c)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *configBiz) Search(name string, pageIndex, pageSize int) ([]*Config, int, error) {
|
||||
list, total, err := b.d.ConfigList(context.TODO(), name, pageIndex, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
configs := make([]*Config, len(list))
|
||||
for i, n := range list {
|
||||
configs[i] = newConfig(&n)
|
||||
}
|
||||
return configs, total, nil
|
||||
}
|
||||
|
||||
func (b *configBiz) Delete(id, name string, user web.User) (err error) {
|
||||
err = b.d.ConfigRemove(context.TODO(), id)
|
||||
if err == nil {
|
||||
b.eb.CreateConfig(EventActionDelete, id, name, user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *configBiz) Create(c *Config, user web.User) (err error) {
|
||||
spec := swarm.ConfigSpec{
|
||||
Data: []byte(c.Data),
|
||||
}
|
||||
spec.Name = c.Name
|
||||
spec.Labels = toMap(c.Labels)
|
||||
if c.Templating.Name != "none" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: c.Templating.Name,
|
||||
Options: toMap(c.Templating.Options),
|
||||
}
|
||||
}
|
||||
|
||||
var id string
|
||||
id, err = b.d.ConfigCreate(context.TODO(), &spec)
|
||||
if err == nil {
|
||||
b.eb.CreateConfig(EventActionCreate, id, c.Name, user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *configBiz) Update(c *Config, user web.User) (err error) {
|
||||
spec := &swarm.ConfigSpec{
|
||||
Data: []byte(c.Data),
|
||||
}
|
||||
spec.Name = c.Name
|
||||
spec.Labels = toMap(c.Labels)
|
||||
if c.Templating.Name != "" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: c.Templating.Name,
|
||||
Options: toMap(c.Templating.Options),
|
||||
}
|
||||
}
|
||||
err = b.d.ConfigUpdate(context.TODO(), c.ID, c.Version, spec)
|
||||
if err == nil {
|
||||
b.eb.CreateConfig(EventActionUpdate, c.ID, c.Name, user)
|
||||
}
|
||||
return
|
||||
}
|
||||
224
biz/container.go
Normal file
224
biz/container.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
/* Summary */
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Ports []*ContainerPort `json:"ports,omitempty"`
|
||||
SizeRw int64 `json:"sizeRw"`
|
||||
SizeRootFs int64 `json:"sizeRootFs"`
|
||||
Labels data.Options `json:"labels"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
NetworkMode string `json:"networkMode"`
|
||||
Mounts []*ContainerMount `json:"mounts"`
|
||||
//Networks map[string]*network.EndpointSettings
|
||||
|
||||
/* Detail */
|
||||
PID int `json:"pid,omitempty"`
|
||||
StartedAt string `json:"startedAt,omitempty"`
|
||||
}
|
||||
|
||||
type ContainerPort struct {
|
||||
IP string `json:"ip,omitempty"`
|
||||
PrivatePort uint16 `json:"privatePort,omitempty"`
|
||||
PublicPort uint16 `json:"publicPort,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type ContainerMount struct {
|
||||
Type mount.Type `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Destination string `json:"destination,omitempty"`
|
||||
Driver string `json:"driver,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
RW bool `json:"rw,omitempty"`
|
||||
Propagation mount.Propagation `json:"propagation,omitempty"`
|
||||
}
|
||||
|
||||
func newContainerMount(m types.MountPoint) *ContainerMount {
|
||||
return &ContainerMount{
|
||||
Type: m.Type,
|
||||
Name: m.Name,
|
||||
Source: m.Source,
|
||||
Destination: m.Destination,
|
||||
Driver: m.Driver,
|
||||
Mode: m.Mode,
|
||||
RW: m.RW,
|
||||
Propagation: m.Propagation,
|
||||
}
|
||||
}
|
||||
|
||||
// EndpointSettings stores the network endpoint details
|
||||
//type EndpointSettings struct {
|
||||
// // Configurations
|
||||
// IPAMConfig *EndpointIPAMConfig
|
||||
// Links []string
|
||||
// Aliases []string
|
||||
// // Operational data
|
||||
// NetworkID string
|
||||
// EndpointID string
|
||||
// Gateway string
|
||||
// IPAddress string
|
||||
// IPPrefixLen int
|
||||
// IPv6Gateway string
|
||||
// GlobalIPv6Address string
|
||||
// GlobalIPv6PrefixLen int
|
||||
// MacAddress string
|
||||
// DriverOpts map[string]string
|
||||
//}
|
||||
|
||||
func newContainerSummary(c *types.Container) *Container {
|
||||
container := &Container{
|
||||
ID: c.ID,
|
||||
Name: c.Names[0],
|
||||
Image: normalizeImage(c.Image),
|
||||
Command: c.Command,
|
||||
CreatedAt: formatTime(time.Unix(c.Created, 0)),
|
||||
SizeRw: c.SizeRw,
|
||||
SizeRootFs: c.SizeRootFs,
|
||||
Labels: mapToOptions(c.Labels),
|
||||
State: c.State,
|
||||
Status: c.Status,
|
||||
NetworkMode: c.HostConfig.NetworkMode,
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
container.Ports = append(container.Ports, &ContainerPort{
|
||||
IP: p.IP,
|
||||
PrivatePort: p.PrivatePort,
|
||||
PublicPort: p.PublicPort,
|
||||
Type: p.Type,
|
||||
})
|
||||
}
|
||||
for _, m := range c.Mounts {
|
||||
container.Mounts = append(container.Mounts, newContainerMount(m))
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
func newContainerDetail(c *types.ContainerJSON) *Container {
|
||||
created, _ := time.Parse(time.RFC3339Nano, c.Created)
|
||||
startedAt, _ := time.Parse(time.RFC3339Nano, c.State.StartedAt)
|
||||
container := &Container{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Image: c.Image,
|
||||
//Command: c.Command,
|
||||
CreatedAt: formatTime(created),
|
||||
Labels: mapToOptions(c.Config.Labels),
|
||||
State: c.State.Status,
|
||||
//Status: c.Status,
|
||||
NetworkMode: string(c.HostConfig.NetworkMode),
|
||||
// detail
|
||||
PID: c.State.Pid,
|
||||
StartedAt: formatTime(startedAt),
|
||||
}
|
||||
if c.SizeRw != nil {
|
||||
container.SizeRw = *c.SizeRw
|
||||
}
|
||||
if c.SizeRootFs != nil {
|
||||
container.SizeRootFs = *c.SizeRootFs
|
||||
}
|
||||
//for _, p := range c.Ports {
|
||||
// container.Ports = append(container.Ports, &ContainerPort{
|
||||
// IP: p.IP,
|
||||
// PrivatePort: p.PrivatePort,
|
||||
// PublicPort: p.PublicPort,
|
||||
// Type: p.Type,
|
||||
// })
|
||||
//}
|
||||
for _, m := range c.Mounts {
|
||||
container.Mounts = append(container.Mounts, newContainerMount(m))
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
type ContainerBiz interface {
|
||||
Search(name, status string, pageIndex, pageSize int) ([]*Container, int, error)
|
||||
Find(id string) (container *Container, raw string, err error)
|
||||
Delete(id string, user web.User) (err error)
|
||||
FetchLogs(id string, lines int, timestamps bool) (stdout, stderr string, err error)
|
||||
ExecCreate(id string, cmd string) (resp types.IDResponse, err error)
|
||||
ExecAttach(id string) (resp types.HijackedResponse, err error)
|
||||
ExecStart(id string) error
|
||||
}
|
||||
|
||||
func NewContainer(d *docker.Docker) ContainerBiz {
|
||||
return &containerBiz{d: d}
|
||||
}
|
||||
|
||||
type containerBiz struct {
|
||||
d *docker.Docker
|
||||
}
|
||||
|
||||
func (b *containerBiz) Find(id string) (c *Container, raw string, err error) {
|
||||
var (
|
||||
cj types.ContainerJSON
|
||||
r []byte
|
||||
)
|
||||
|
||||
if cj, r, err = b.d.ContainerInspect(context.TODO(), id); err == nil {
|
||||
raw, err = indentJSON(r)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
c = newContainerDetail(&cj)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *containerBiz) Search(name, status string, pageIndex, pageSize int) (containers []*Container, total int, err error) {
|
||||
list, total, err := b.d.ContainerList(context.TODO(), name, status, pageIndex, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
containers = make([]*Container, len(list))
|
||||
for i, nr := range list {
|
||||
containers[i] = newContainerSummary(&nr)
|
||||
}
|
||||
return containers, total, nil
|
||||
}
|
||||
|
||||
func (b *containerBiz) Delete(id string, user web.User) (err error) {
|
||||
err = b.d.ContainerRemove(context.TODO(), id)
|
||||
//if err == nil {
|
||||
// Event.CreateContainer(model.EventActionDelete, id, user)
|
||||
//}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *containerBiz) ExecCreate(id, cmd string) (resp types.IDResponse, err error) {
|
||||
return b.d.ContainerExecCreate(context.TODO(), id, cmd)
|
||||
}
|
||||
|
||||
func (b *containerBiz) ExecAttach(id string) (resp types.HijackedResponse, err error) {
|
||||
return b.d.ContainerExecAttach(context.TODO(), id)
|
||||
}
|
||||
|
||||
func (b *containerBiz) ExecStart(id string) error {
|
||||
return b.d.ContainerExecStart(context.TODO(), id)
|
||||
}
|
||||
|
||||
func (b *containerBiz) FetchLogs(id string, lines int, timestamps bool) (string, string, error) {
|
||||
stdout, stderr, err := b.d.ContainerLogs(context.TODO(), id, lines, timestamps)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return stdout.String(), stderr.String(), nil
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package docker
|
||||
|
||||
//import (
|
||||
// "io/ioutil"
|
||||
//
|
||||
// "github.com/docker/cli/cli/compose/loader"
|
||||
// composetypes "github.com/docker/cli/cli/compose/types"
|
||||
//)
|
||||
//
|
||||
//func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
|
||||
// bytes, err := ioutil.ReadFile(filename)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// config, err := loader.ParseYAML(bytes)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return &composetypes.ConfigFile{
|
||||
// Filename: filename,
|
||||
// Config: config,
|
||||
// }, nil
|
||||
//}
|
||||
@@ -1,95 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// ConfigList return all configs.
|
||||
func ConfigList(name string, pageIndex, pageSize int) (configs []swarm.Config, totalCount int, err error) {
|
||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ConfigListOptions{}
|
||||
if name != "" {
|
||||
opts.Filters = filters.NewArgs()
|
||||
opts.Filters.Add("name", name)
|
||||
}
|
||||
configs, err = cli.ConfigList(ctx, opts)
|
||||
if err == nil {
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
return configs[i].Spec.Name < configs[j].Spec.Name
|
||||
})
|
||||
totalCount = len(configs)
|
||||
start, end := misc.Page(totalCount, pageIndex, pageSize)
|
||||
configs = configs[start:end]
|
||||
}
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConfigCreate create a config.
|
||||
func ConfigCreate(info *model.ConfigCreateInfo) error {
|
||||
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
spec := swarm.ConfigSpec{}
|
||||
spec.Name = info.Name
|
||||
spec.Data = []byte(info.Data)
|
||||
spec.Labels = info.Labels.ToMap()
|
||||
if info.Template.Name != "" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: info.Template.Name,
|
||||
Options: info.Template.Options,
|
||||
}
|
||||
}
|
||||
_, err = cli.ConfigCreate(ctx, spec)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// ConfigUpdate update a config.
|
||||
func ConfigUpdate(info *model.ConfigUpdateInfo) error {
|
||||
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
var cfg swarm.Config
|
||||
cfg, _, err = cli.ConfigInspectWithRaw(ctx, info.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spec := cfg.Spec
|
||||
// only the Labels field can be updated on API 1.30
|
||||
//spec.Name = info.Name
|
||||
//spec.Data = []byte(info.Data)
|
||||
spec.Labels = info.Labels.ToMap()
|
||||
return cli.ConfigUpdate(ctx, info.ID, version(info.Version), spec)
|
||||
})
|
||||
}
|
||||
|
||||
// ConfigInspect returns config information with raw data.
|
||||
func ConfigInspect(id string) (cfg swarm.Config, raw []byte, err error) {
|
||||
var (
|
||||
ctx context.Context
|
||||
cli *client.Client
|
||||
)
|
||||
if ctx, cli, err = mgr.Client(); err == nil {
|
||||
cfg, raw, err = cli.ConfigInspectWithRaw(ctx, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ConfigRemove remove a config.
|
||||
func ConfigRemove(ids []string) error {
|
||||
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
for _, id := range ids {
|
||||
if err = cli.ConfigRemove(ctx, id); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
)
|
||||
|
||||
// ContainerList return containers on the host.
|
||||
func ContainerList(args *model.ContainerListArgs) (infos []*model.ContainerListInfo, totalCount int, err error) {
|
||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
var (
|
||||
containers []types.Container
|
||||
opts = types.ContainerListOptions{Filters: filters.NewArgs()}
|
||||
)
|
||||
|
||||
if args.Filter == "" {
|
||||
opts.All = true
|
||||
} else {
|
||||
opts.Filters.Add("status", args.Filter)
|
||||
}
|
||||
if args.Name != "" {
|
||||
opts.Filters.Add("name", args.Name)
|
||||
}
|
||||
|
||||
containers, err = cli.ContainerList(ctx, opts)
|
||||
if err == nil {
|
||||
//sort.Slice(containers, func(i, j int) bool {
|
||||
// return containers[i] < containers[j].Description.Hostname
|
||||
//})
|
||||
totalCount = len(containers)
|
||||
start, end := misc.Page(totalCount, args.PageIndex, args.PageSize)
|
||||
containers = containers[start:end]
|
||||
if length := len(containers); length > 0 {
|
||||
infos = make([]*model.ContainerListInfo, length)
|
||||
for i, c := range containers {
|
||||
infos[i] = model.NewContainerListInfo(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerInspect return detail information of a container.
|
||||
func ContainerInspect(id string) (container types.ContainerJSON, err error) {
|
||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
container, err = cli.ContainerInspect(ctx, id)
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerInspectRaw return container raw information.
|
||||
func ContainerInspectRaw(id string) (container types.ContainerJSON, raw []byte, err error) {
|
||||
var (
|
||||
ctx context.Context
|
||||
cli *client.Client
|
||||
)
|
||||
if ctx, cli, err = mgr.Client(); err == nil {
|
||||
container, raw, err = cli.ContainerInspectWithRaw(ctx, id, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerRemove remove a container.
|
||||
func ContainerRemove(id string) error {
|
||||
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ContainerRemoveOptions{}
|
||||
err = cli.ContainerRemove(ctx, id, opts)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// ContainerLogs returns the logs generated by a container.
|
||||
func ContainerLogs(id string, line int, timestamps bool) (stdout, stderr *bytes.Buffer, err error) {
|
||||
var (
|
||||
ctx context.Context
|
||||
cli *client.Client
|
||||
rc io.ReadCloser
|
||||
)
|
||||
|
||||
ctx, cli, err = mgr.Client()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
opts := types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Tail: strconv.Itoa(line),
|
||||
Timestamps: timestamps,
|
||||
//Since: (time.Hour * 24).String()
|
||||
}
|
||||
if rc, err = cli.ContainerLogs(ctx, id, opts); err == nil {
|
||||
defer rc.Close()
|
||||
|
||||
stdout = &bytes.Buffer{}
|
||||
stderr = &bytes.Buffer{}
|
||||
_, err = stdcopy.StdCopy(stdout, stderr, rc)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerExecCreate creates an exec instance.
|
||||
func ContainerExecCreate(id string, cmd string) (resp types.IDResponse, err error) {
|
||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ExecConfig{
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
//User: "root",
|
||||
Cmd: strings.Split(cmd, " "),
|
||||
}
|
||||
//cli.DialSession()
|
||||
resp, err = cli.ContainerExecCreate(ctx, id, opts)
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerExecAttach attaches a connection to an exec process in the server.
|
||||
func ContainerExecAttach(id string) (resp types.HijackedResponse, err error) {
|
||||
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ExecStartCheck{
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
}
|
||||
resp, err = cli.ContainerExecAttach(ctx, id, opts)
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ContainerExecStart starts an exec instance.
|
||||
func ContainerExecStart(id string) error {
|
||||
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ExecStartCheck{
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
}
|
||||
return cli.ContainerExecStart(ctx, id, opts)
|
||||
})
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAPIVersion = "1.32"
|
||||
)
|
||||
|
||||
var mgr = &manager{}
|
||||
|
||||
type manager struct {
|
||||
client *client.Client
|
||||
locker sync.Mutex
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (m *manager) Do(fn func(ctx context.Context, cli *client.Client) error) (err error) {
|
||||
ctx, cli, err := m.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ctx, cli)
|
||||
}
|
||||
|
||||
func (m *manager) Client() (ctx context.Context, cli *client.Client, err error) {
|
||||
if m.client == nil {
|
||||
m.locker.Lock()
|
||||
defer m.locker.Unlock()
|
||||
|
||||
if m.client == nil {
|
||||
apiVersion := misc.Options.DockerAPIVersion
|
||||
if apiVersion == "" {
|
||||
apiVersion = defaultAPIVersion
|
||||
}
|
||||
if misc.Options.DockerEndpoint == "" {
|
||||
os.Setenv("DOCKER_API_VERSION", apiVersion)
|
||||
m.client, err = client.NewClientWithOpts(client.FromEnv)
|
||||
} else {
|
||||
m.client, err = client.NewClientWithOpts(client.WithHost(misc.Options.DockerEndpoint), client.WithVersion(apiVersion))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return context.TODO(), m.client, nil
|
||||
}
|
||||
|
||||
func (m *manager) Logger() log.Logger {
|
||||
if m.logger == nil {
|
||||
m.locker.Lock()
|
||||
defer m.locker.Unlock()
|
||||
|
||||
if m.logger == nil {
|
||||
m.logger = log.Get("docker")
|
||||
}
|
||||
}
|
||||
return m.logger
|
||||
}
|
||||
|
||||
func version(v uint64) swarm.Version {
|
||||
return swarm.Version{Index: v}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// ImageList return images on the host.
|
||||
func ImageList(name string, pageIndex, pageSize int) (images []*model.ImageListInfo, totalCount int, err error) {
|
||||
ctx, cli, err := mgr.Client()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
opts := types.ImageListOptions{}
|
||||
if name != "" {
|
||||
opts.Filters = filters.NewArgs()
|
||||
opts.Filters.Add("reference", name)
|
||||
}
|
||||
summaries, err := cli.ImageList(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
//sort.Slice(images, func(i, j int) bool {
|
||||
// return images[i].ID < images[j].ID
|
||||
//})
|
||||
|
||||
totalCount = len(summaries)
|
||||
start, end := misc.Page(totalCount, pageIndex, pageSize)
|
||||
summaries = summaries[start:end]
|
||||
if length := len(summaries); length > 0 {
|
||||
images = make([]*model.ImageListInfo, length)
|
||||
for i, summary := range summaries {
|
||||
images[i] = model.NewImageListInfo(summary)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ImageInspect returns image information.
|
||||
func ImageInspect(id string) (image types.ImageInspect, raw []byte, err error) {
|
||||
var (
|
||||
ctx context.Context
|
||||
cli *client.Client
|
||||
)
|
||||
if ctx, cli, err = mgr.Client(); err == nil {
|
||||
return cli.ImageInspectWithRaw(ctx, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ImageHistory returns the changes in an image in history format.
|
||||
func ImageHistory(id string) (histories []image.HistoryResponseItem, err error) {
|
||||
var (
|
||||
ctx context.Context
|
||||
cli *client.Client
|
||||
)
|
||||
if ctx, cli, err = mgr.Client(); err == nil {
|
||||
return cli.ImageHistory(ctx, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ImageRemove remove a image.
|
||||
func ImageRemove(id string) error {
|
||||
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
|
||||
opts := types.ImageRemoveOptions{}
|
||||
_, err = cli.ImageRemove(ctx, id, opts)
|
||||
return
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user