Refactor UI with vue3

This commit is contained in:
cuigh
2021-12-06 20:24:22 +08:00
parent afbc618abb
commit 2dd9cd15e1
411 changed files with 22726 additions and 69351 deletions

3
.gitattributes vendored
View File

@@ -1,3 +0,0 @@
*.js linguist-language=Go
*.css linguist-language=Go
*.jet linguist-language=Go

38
.gitignore vendored
View File

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

View File

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

View File

@@ -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 list](docs/images/service-list.png)
![Service list](docs/images/services.png)
### Service stats
@@ -34,11 +36,11 @@
### Stack list
![Stack list](docs/images/stack-list.png)
![Stack list](docs/images/stacks.png)
### Settings
![Setting](docs/images/setting.png)
![Setting](docs/images/settings.png)
## 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 != "");
})
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"target": "es2015",
"noImplicitAny": true,
"removeComments": true,
"sourceMap": true,
"outFile": "js/swirl.js",
"lib": ["dom", "es2015.promise", "es5"]
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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