mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-21 07:49:34 +01:00
Merge pull request #619 from ansible-semaphore/web2
New web UI with using Vue.js
This commit is contained in:
commit
35ff8782a3
@ -54,7 +54,7 @@ aliases:
|
|||||||
- &test-compile-changes
|
- &test-compile-changes
|
||||||
run:
|
run:
|
||||||
name: test that compile did not create/modify untracked files
|
name: test that compile did not create/modify untracked files
|
||||||
command: git diff --exit-code --stat -- . ':(exclude)web/package-lock.json' ':(exclude)go.mod' ':(exclude)go.sum'
|
command: git diff --exit-code --stat -- . ':(exclude)web2/package-lock.json' ':(exclude)web/package-lock.json' ':(exclude)go.mod' ':(exclude)go.sum'
|
||||||
|
|
||||||
- &save-npm-cache
|
- &save-npm-cache
|
||||||
save_cache:
|
save_cache:
|
||||||
|
@ -95,6 +95,7 @@ func main() {
|
|||||||
addCapabilities([]string{"repository", "inventory", "environment"})
|
addCapabilities([]string{"repository", "inventory", "environment"})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
h.Before("project > /api/project/{project_id}/templates/{template_id} > Get template > 200 > application/json", capabilityWrapper("template"))
|
||||||
h.Before("project > /api/project/{project_id}/templates/{template_id} > Updates template > 204 > application/json", capabilityWrapper("template"))
|
h.Before("project > /api/project/{project_id}/templates/{template_id} > Updates template > 204 > application/json", capabilityWrapper("template"))
|
||||||
h.Before("project > /api/project/{project_id}/templates/{template_id} > Removes template > 204 > application/json", capabilityWrapper("template"))
|
h.Before("project > /api/project/{project_id}/templates/{template_id} > Removes template > 204 > application/json", capabilityWrapper("template"))
|
||||||
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@ web/public/js/bundle.js
|
|||||||
web/public/css/*.*
|
web/public/css/*.*
|
||||||
web/public/html/**/*.*
|
web/public/html/**/*.*
|
||||||
web/public/fonts/*.*
|
web/public/fonts/*.*
|
||||||
|
web2/dist
|
||||||
config.json
|
config.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules/
|
node_modules/
|
||||||
|
31
Taskfile.yml
31
Taskfile.yml
@ -26,6 +26,7 @@ tasks:
|
|||||||
- task: deps:tools
|
- task: deps:tools
|
||||||
- task: deps:be
|
- task: deps:be
|
||||||
- task: deps:fe
|
- task: deps:fe
|
||||||
|
- task: deps:fe2
|
||||||
|
|
||||||
deps:be:
|
deps:be:
|
||||||
desc: Vendor application dependencies
|
desc: Vendor application dependencies
|
||||||
@ -39,6 +40,13 @@ tasks:
|
|||||||
- npm install
|
- npm install
|
||||||
- npm audit fix
|
- npm audit fix
|
||||||
|
|
||||||
|
deps:fe2:
|
||||||
|
desc: Installs npm requirements for front end from package.json
|
||||||
|
dir: web2
|
||||||
|
cmds:
|
||||||
|
- npm install
|
||||||
|
- npm audit fix
|
||||||
|
|
||||||
deps:integration:
|
deps:integration:
|
||||||
desc: Installs requirements for integration testing with dredd
|
desc: Installs requirements for integration testing with dredd
|
||||||
dir: web
|
dir: web
|
||||||
@ -68,6 +76,7 @@ tasks:
|
|||||||
desc: Generates compiled frontend and backend resources (must be in this order)
|
desc: Generates compiled frontend and backend resources (must be in this order)
|
||||||
cmds:
|
cmds:
|
||||||
- task: compile:fe
|
- task: compile:fe
|
||||||
|
- task: compile:fe2
|
||||||
- task: compile:be
|
- task: compile:be
|
||||||
|
|
||||||
compile:fe:
|
compile:fe:
|
||||||
@ -87,9 +96,31 @@ tasks:
|
|||||||
- '{{ if eq OS "windows" }} .\\node_modules\\.bin\\pug.cmd --pretty {{ else }} ./node_modules/.bin/pug {{ end }} resources/pug --out public/html'
|
- '{{ if eq OS "windows" }} .\\node_modules\\.bin\\pug.cmd --pretty {{ else }} ./node_modules/.bin/pug {{ end }} resources/pug --out public/html'
|
||||||
- '{{ if eq OS "windows" }} xcopy node_modules\\font-awesome\\fonts public\\fonts /y {{ else }} cp node_modules/font-awesome/fonts/* public/fonts {{ end }}'
|
- '{{ if eq OS "windows" }} xcopy node_modules\\font-awesome\\fonts public\\fonts /y {{ else }} cp node_modules/font-awesome/fonts/* public/fonts {{ end }}'
|
||||||
- node bundler.js
|
- node bundler.js
|
||||||
|
|
||||||
|
compile:fe2:
|
||||||
|
desc: Build vue.js project
|
||||||
|
dir: web2
|
||||||
|
sources:
|
||||||
|
- src/*.*
|
||||||
|
- src/**/*.*
|
||||||
|
- public/index.html
|
||||||
|
- public/favicon.ico
|
||||||
|
- package.json
|
||||||
|
- package-lock.json
|
||||||
|
- babel.config.js
|
||||||
|
- vue.config.js
|
||||||
|
generates:
|
||||||
|
- dist/css/*.css
|
||||||
|
- dist/js/*.js
|
||||||
|
- dist/index.html
|
||||||
|
- dist/favicon.ico
|
||||||
|
cmds:
|
||||||
|
- npm run build
|
||||||
|
|
||||||
compile:be:
|
compile:be:
|
||||||
desc: Runs Packr for static assets
|
desc: Runs Packr for static assets
|
||||||
sources:
|
sources:
|
||||||
|
- web2/dist/*
|
||||||
- web/public/*
|
- web/public/*
|
||||||
- db/migrations/*
|
- db/migrations/*
|
||||||
generates:
|
generates:
|
||||||
|
@ -1175,6 +1175,15 @@ paths:
|
|||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/parameters/project_id"
|
- $ref: "#/parameters/project_id"
|
||||||
- $ref: "#/parameters/template_id"
|
- $ref: "#/parameters/template_id"
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
summary: Get template
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: template object
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Template"
|
||||||
put:
|
put:
|
||||||
tags:
|
tags:
|
||||||
- project
|
- project
|
||||||
|
@ -45,6 +45,11 @@ func EnvironmentMiddleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// GetEnvironment retrieves sorted environments from the database
|
// GetEnvironment retrieves sorted environments from the database
|
||||||
func GetEnvironment(w http.ResponseWriter, r *http.Request) {
|
func GetEnvironment(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if environment := context.Get(r, "environment"); environment != nil {
|
||||||
|
util.WriteJSON(w, http.StatusOK, environment.(db.Environment))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
project := context.Get(r, "project").(db.Project)
|
project := context.Get(r, "project").(db.Project)
|
||||||
var env []db.Environment
|
var env []db.Environment
|
||||||
|
|
||||||
|
@ -52,7 +52,13 @@ func InventoryMiddleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// GetInventory returns an inventory from the database
|
// GetInventory returns an inventory from the database
|
||||||
func GetInventory(w http.ResponseWriter, r *http.Request) {
|
func GetInventory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if inventory := context.Get(r, "inventory"); inventory != nil {
|
||||||
|
util.WriteJSON(w, http.StatusOK, inventory.(db.Inventory))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
project := context.Get(r, "project").(db.Project)
|
project := context.Get(r, "project").(db.Project)
|
||||||
|
|
||||||
var inv []db.Inventory
|
var inv []db.Inventory
|
||||||
|
|
||||||
sort := r.URL.Query().Get("sort")
|
sort := r.URL.Query().Get("sort")
|
||||||
|
@ -37,6 +37,11 @@ func KeyMiddleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// GetKeys retrieves sorted keys from the database
|
// GetKeys retrieves sorted keys from the database
|
||||||
func GetKeys(w http.ResponseWriter, r *http.Request) {
|
func GetKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if key := context.Get(r, "accessKey"); key != nil {
|
||||||
|
util.WriteJSON(w, http.StatusOK, key.(db.AccessKey))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
project := context.Get(r, "project").(db.Project)
|
project := context.Get(r, "project").(db.Project)
|
||||||
var keys []db.AccessKey
|
var keys []db.AccessKey
|
||||||
|
|
||||||
|
@ -49,6 +49,11 @@ func RepositoryMiddleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// GetRepositories returns all repositories in a project sorted by type
|
// GetRepositories returns all repositories in a project sorted by type
|
||||||
func GetRepositories(w http.ResponseWriter, r *http.Request) {
|
func GetRepositories(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if repo := context.Get(r, "repository"); repo != nil {
|
||||||
|
util.WriteJSON(w, http.StatusOK, repo.(db.Repository))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
project := context.Get(r, "project").(db.Project)
|
project := context.Get(r, "project").(db.Project)
|
||||||
var repos []db.Repository
|
var repos []db.Repository
|
||||||
|
|
||||||
|
@ -36,6 +36,12 @@ func TemplatesMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTemplate returns single template by ID
|
||||||
|
func GetTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
template := context.Get(r, "template").(db.Template)
|
||||||
|
util.WriteJSON(w, http.StatusOK, template)
|
||||||
|
}
|
||||||
|
|
||||||
// GetTemplates returns all templates for a project in a sort order
|
// GetTemplates returns all templates for a project in a sort order
|
||||||
func GetTemplates(w http.ResponseWriter, r *http.Request) {
|
func GetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||||
project := context.Get(r, "project").(db.Project)
|
project := context.Get(r, "project").(db.Project)
|
||||||
|
@ -38,6 +38,11 @@ func UserMiddleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// GetUsers returns all users in a project
|
// GetUsers returns all users in a project
|
||||||
func GetUsers(w http.ResponseWriter, r *http.Request) {
|
func GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if user := context.Get(r, "projectUser"); user != nil {
|
||||||
|
util.WriteJSON(w, http.StatusOK, user.(db.User))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
project := context.Get(r, "project").(db.Project)
|
project := context.Get(r, "project").(db.Project)
|
||||||
var users []db.User
|
var users []db.User
|
||||||
|
|
||||||
|
@ -16,7 +16,15 @@ import (
|
|||||||
"github.com/russross/blackfriday"
|
"github.com/russross/blackfriday"
|
||||||
)
|
)
|
||||||
|
|
||||||
var publicAssets = packr.NewBox("../web/public")
|
var publicAssets packr.Box
|
||||||
|
|
||||||
|
func getPublicAssetsPath() string {
|
||||||
|
if util.Config != nil && util.Config.OldFrontend {
|
||||||
|
return "../web/public"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "../web2/dist"
|
||||||
|
}
|
||||||
|
|
||||||
//JSONMiddleware ensures that all the routes respond with Json, this is added by default to all routes
|
//JSONMiddleware ensures that all the routes respond with Json, this is added by default to all routes
|
||||||
func JSONMiddleware(next http.Handler) http.Handler {
|
func JSONMiddleware(next http.Handler) http.Handler {
|
||||||
@ -50,6 +58,8 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Route declares all routes
|
// Route declares all routes
|
||||||
func Route() *mux.Router {
|
func Route() *mux.Router {
|
||||||
|
publicAssets = packr.NewBox(getPublicAssetsPath())
|
||||||
|
|
||||||
r := mux.NewRouter().StrictSlash(true)
|
r := mux.NewRouter().StrictSlash(true)
|
||||||
r.NotFoundHandler = http.HandlerFunc(servePublic)
|
r.NotFoundHandler = http.HandlerFunc(servePublic)
|
||||||
|
|
||||||
@ -140,6 +150,7 @@ func Route() *mux.Router {
|
|||||||
projectUserManagement := projectAdminAPI.PathPrefix("/users").Subrouter()
|
projectUserManagement := projectAdminAPI.PathPrefix("/users").Subrouter()
|
||||||
projectUserManagement.Use(projects.UserMiddleware)
|
projectUserManagement.Use(projects.UserMiddleware)
|
||||||
|
|
||||||
|
projectUserManagement.HandleFunc("/{user_id}", projects.GetUsers).Methods("GET", "HEAD")
|
||||||
projectUserManagement.HandleFunc("/{user_id}/admin", projects.MakeUserAdmin).Methods("POST")
|
projectUserManagement.HandleFunc("/{user_id}/admin", projects.MakeUserAdmin).Methods("POST")
|
||||||
projectUserManagement.HandleFunc("/{user_id}/admin", projects.MakeUserAdmin).Methods("DELETE")
|
projectUserManagement.HandleFunc("/{user_id}/admin", projects.MakeUserAdmin).Methods("DELETE")
|
||||||
projectUserManagement.HandleFunc("/{user_id}", projects.RemoveUser).Methods("DELETE")
|
projectUserManagement.HandleFunc("/{user_id}", projects.RemoveUser).Methods("DELETE")
|
||||||
@ -147,24 +158,28 @@ func Route() *mux.Router {
|
|||||||
projectKeyManagement := projectAdminAPI.PathPrefix("/keys").Subrouter()
|
projectKeyManagement := projectAdminAPI.PathPrefix("/keys").Subrouter()
|
||||||
projectKeyManagement.Use(projects.KeyMiddleware)
|
projectKeyManagement.Use(projects.KeyMiddleware)
|
||||||
|
|
||||||
|
projectKeyManagement.HandleFunc("/{key_id}", projects.GetKeys).Methods("GET", "HEAD")
|
||||||
projectKeyManagement.HandleFunc("/{key_id}", projects.UpdateKey).Methods("PUT")
|
projectKeyManagement.HandleFunc("/{key_id}", projects.UpdateKey).Methods("PUT")
|
||||||
projectKeyManagement.HandleFunc("/{key_id}", projects.RemoveKey).Methods("DELETE")
|
projectKeyManagement.HandleFunc("/{key_id}", projects.RemoveKey).Methods("DELETE")
|
||||||
|
|
||||||
projectRepoManagement := projectUserAPI.PathPrefix("/repositories").Subrouter()
|
projectRepoManagement := projectUserAPI.PathPrefix("/repositories").Subrouter()
|
||||||
projectRepoManagement.Use(projects.RepositoryMiddleware)
|
projectRepoManagement.Use(projects.RepositoryMiddleware)
|
||||||
|
|
||||||
|
projectRepoManagement.HandleFunc("/{repository_id}", projects.GetRepositories).Methods("GET", "HEAD")
|
||||||
projectRepoManagement.HandleFunc("/{repository_id}", projects.UpdateRepository).Methods("PUT")
|
projectRepoManagement.HandleFunc("/{repository_id}", projects.UpdateRepository).Methods("PUT")
|
||||||
projectRepoManagement.HandleFunc("/{repository_id}", projects.RemoveRepository).Methods("DELETE")
|
projectRepoManagement.HandleFunc("/{repository_id}", projects.RemoveRepository).Methods("DELETE")
|
||||||
|
|
||||||
projectInventoryManagement := projectUserAPI.PathPrefix("/inventory").Subrouter()
|
projectInventoryManagement := projectUserAPI.PathPrefix("/inventory").Subrouter()
|
||||||
projectInventoryManagement.Use(projects.InventoryMiddleware)
|
projectInventoryManagement.Use(projects.InventoryMiddleware)
|
||||||
|
|
||||||
|
projectInventoryManagement.HandleFunc("/{inventory_id}", projects.GetInventory).Methods("GET", "HEAD")
|
||||||
projectInventoryManagement.HandleFunc("/{inventory_id}", projects.UpdateInventory).Methods("PUT")
|
projectInventoryManagement.HandleFunc("/{inventory_id}", projects.UpdateInventory).Methods("PUT")
|
||||||
projectInventoryManagement.HandleFunc("/{inventory_id}", projects.RemoveInventory).Methods("DELETE")
|
projectInventoryManagement.HandleFunc("/{inventory_id}", projects.RemoveInventory).Methods("DELETE")
|
||||||
|
|
||||||
projectEnvManagement := projectUserAPI.PathPrefix("/environment").Subrouter()
|
projectEnvManagement := projectUserAPI.PathPrefix("/environment").Subrouter()
|
||||||
projectEnvManagement.Use(projects.EnvironmentMiddleware)
|
projectEnvManagement.Use(projects.EnvironmentMiddleware)
|
||||||
|
|
||||||
|
projectEnvManagement.HandleFunc("/{environment_id}", projects.GetEnvironment).Methods("GET", "HEAD")
|
||||||
projectEnvManagement.HandleFunc("/{environment_id}", projects.UpdateEnvironment).Methods("PUT")
|
projectEnvManagement.HandleFunc("/{environment_id}", projects.UpdateEnvironment).Methods("PUT")
|
||||||
projectEnvManagement.HandleFunc("/{environment_id}", projects.RemoveEnvironment).Methods("DELETE")
|
projectEnvManagement.HandleFunc("/{environment_id}", projects.RemoveEnvironment).Methods("DELETE")
|
||||||
|
|
||||||
@ -173,6 +188,9 @@ func Route() *mux.Router {
|
|||||||
|
|
||||||
projectTmplManagement.HandleFunc("/{template_id}", projects.UpdateTemplate).Methods("PUT")
|
projectTmplManagement.HandleFunc("/{template_id}", projects.UpdateTemplate).Methods("PUT")
|
||||||
projectTmplManagement.HandleFunc("/{template_id}", projects.RemoveTemplate).Methods("DELETE")
|
projectTmplManagement.HandleFunc("/{template_id}", projects.RemoveTemplate).Methods("DELETE")
|
||||||
|
projectTmplManagement.HandleFunc("/{template_id}", projects.GetTemplate).Methods("GET")
|
||||||
|
projectTmplManagement.HandleFunc("/{template_id}/tasks", tasks.GetAllTasks).Methods("GET")
|
||||||
|
projectTmplManagement.HandleFunc("/{template_id}/tasks/last", tasks.GetLastTasks).Methods("GET")
|
||||||
|
|
||||||
projectTaskManagement := projectUserAPI.PathPrefix("/tasks").Subrouter()
|
projectTaskManagement := projectUserAPI.PathPrefix("/tasks").Subrouter()
|
||||||
projectTaskManagement.Use(tasks.GetTaskMiddleware)
|
projectTaskManagement.Use(tasks.GetTaskMiddleware)
|
||||||
@ -223,21 +241,33 @@ func debugPrintRoutes(r *mux.Router) {
|
|||||||
func servePublic(w http.ResponseWriter, r *http.Request) {
|
func servePublic(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
|
|
||||||
|
htmlPrefix := ""
|
||||||
|
if util.Config.OldFrontend {
|
||||||
|
htmlPrefix = "/html"
|
||||||
|
}
|
||||||
|
|
||||||
|
publicAssetsPrefix := ""
|
||||||
|
if util.Config.OldFrontend {
|
||||||
|
publicAssetsPrefix = "public"
|
||||||
|
}
|
||||||
|
|
||||||
webPath := "/"
|
webPath := "/"
|
||||||
if util.WebHostURL != nil {
|
if util.WebHostURL != nil {
|
||||||
webPath = util.WebHostURL.RequestURI()
|
webPath = util.WebHostURL.RequestURI()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(path, webPath+"public") {
|
if publicAssetsPrefix != "" && !strings.HasPrefix(path, webPath+publicAssetsPrefix) {
|
||||||
if len(strings.Split(path, ".")) > 1 {
|
if len(strings.Split(path, ".")) > 1 {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
path = "/html/index.html"
|
path = htmlPrefix+"/index.html"
|
||||||
|
} else if len(strings.Split(path, ".")) == 1 {
|
||||||
|
path = htmlPrefix+"/index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
path = strings.Replace(path, webPath+"public/", "", 1)
|
path = strings.Replace(path, webPath+publicAssetsPrefix+"/", "", 1)
|
||||||
split := strings.Split(path, ".")
|
split := strings.Split(path, ".")
|
||||||
suffix := split[len(split)-1]
|
suffix := split[len(split)-1]
|
||||||
|
|
||||||
@ -248,7 +278,7 @@ func servePublic(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// replace base path
|
// replace base path
|
||||||
if util.WebHostURL != nil && path == "/html/index.html" {
|
if util.WebHostURL != nil && path == htmlPrefix+"/index.html" {
|
||||||
res = []byte(strings.Replace(string(res),
|
res = []byte(strings.Replace(string(res),
|
||||||
"<base href=\"/\">",
|
"<base href=\"/\">",
|
||||||
"<base href=\""+util.WebHostURL.String()+"\">",
|
"<base href=\""+util.WebHostURL.String()+"\">",
|
||||||
|
@ -57,11 +57,17 @@ func GetTasksList(w http.ResponseWriter, r *http.Request, limit uint64) {
|
|||||||
project := context.Get(r, "project").(db.Project)
|
project := context.Get(r, "project").(db.Project)
|
||||||
|
|
||||||
q := squirrel.Select("task.*, tpl.playbook as tpl_playbook, user.name as user_name, tpl.alias as tpl_alias").
|
q := squirrel.Select("task.*, tpl.playbook as tpl_playbook, user.name as user_name, tpl.alias as tpl_alias").
|
||||||
From(taskTypeID).
|
From("task").
|
||||||
Join("project__template as tpl on task.template_id=tpl.id").
|
Join("project__template as tpl on task.template_id=tpl.id").
|
||||||
LeftJoin("user on task.user_id=user.id").
|
LeftJoin("user on task.user_id=user.id");
|
||||||
Where("tpl.project_id=?", project.ID).
|
|
||||||
OrderBy("task.created desc")
|
if tpl := context.Get(r, "template"); tpl != nil {
|
||||||
|
q = q.Where("tpl.project_id=? AND task.template_id=?", project.ID, tpl.(db.Template).ID)
|
||||||
|
} else {
|
||||||
|
q = q.Where("tpl.project_id=?", project.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
q = q.OrderBy("task.created desc, id desc")
|
||||||
|
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
q = q.Limit(limit)
|
q = q.Limit(limit)
|
||||||
|
@ -101,6 +101,8 @@ type ConfigType struct {
|
|||||||
TelegramAlert bool `json:"telegram_alert"`
|
TelegramAlert bool `json:"telegram_alert"`
|
||||||
LdapEnable bool `json:"ldap_enable"`
|
LdapEnable bool `json:"ldap_enable"`
|
||||||
LdapNeedTLS bool `json:"ldap_needtls"`
|
LdapNeedTLS bool `json:"ldap_needtls"`
|
||||||
|
|
||||||
|
OldFrontend bool `json:"old_frontend"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//Config exposes the application configuration storage for use in the application
|
//Config exposes the application configuration storage for use in the application
|
||||||
|
3
web2/.browserslistrc
Normal file
3
web2/.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
7
web2/.editorconfig
Normal file
7
web2/.editorconfig
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 100
|
28
web2/.eslintrc.js
Normal file
28
web2/.eslintrc.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/essential',
|
||||||
|
'@vue/airbnb',
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/__tests__/*.{j,t}s?(x)',
|
||||||
|
'**/tests/unit/**/*.spec.{j,t}s?(x)',
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
mocha: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
23
web2/.gitignore
vendored
Normal file
23
web2/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
29
web2/README.md
Normal file
29
web2/README.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# web2
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run your unit tests
|
||||||
|
```
|
||||||
|
npm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
web2/babel.config.js
Normal file
5
web2/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset',
|
||||||
|
],
|
||||||
|
};
|
14489
web2/package-lock.json
generated
Normal file
14489
web2/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
web2/package.json
Normal file
38
web2/package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "web2",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"test:unit": "vue-cli-service test:unit",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-router": "^3.2.0",
|
||||||
|
"vuetify": "^2.2.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-unit-mocha": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"@vue/eslint-config-airbnb": "^5.0.2",
|
||||||
|
"@vue/test-utils": "^1.0.3",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"chai": "^4.1.2",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-import": "^2.20.2",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"node-sass": "^4.12.0",
|
||||||
|
"sass": "^1.19.0",
|
||||||
|
"sass-loader": "^8.0.2",
|
||||||
|
"vue-cli-plugin-vuetify": "~2.0.7",
|
||||||
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
"vuetify-loader": "^1.3.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
web2/public/favicon.ico
Normal file
BIN
web2/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
19
web2/public/index.html
Normal file
19
web2/public/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
701
web2/src/App.vue
Normal file
701
web2/src/App.vue
Normal file
@ -0,0 +1,701 @@
|
|||||||
|
<template>
|
||||||
|
<v-app v-if="state === 'success'" class="app">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="userDialog"
|
||||||
|
save-button-text="Save"
|
||||||
|
title="Edit User"
|
||||||
|
v-if="user"
|
||||||
|
event-name="i-user"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<UserForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="user.id"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<ItemDialog
|
||||||
|
v-model="taskLogDialog"
|
||||||
|
save-button-text="Delete"
|
||||||
|
:max-width="800"
|
||||||
|
>
|
||||||
|
<template v-slot:title={}>
|
||||||
|
<router-link
|
||||||
|
class="breadcrumbs__item breadcrumbs__item--link"
|
||||||
|
:to="`/project/${projectId}/templates/${template ? template.id : null}`"
|
||||||
|
@click="taskLogDialog = false"
|
||||||
|
>{{ template ? template.alias : null }}</router-link>
|
||||||
|
<span class="breadcrumbs__separator">></span>
|
||||||
|
<span class="breadcrumbs__item">Task #{{ task ? task.id : null }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-slot:form="{}">
|
||||||
|
<TaskLogView :project-id="projectId" :item-id="task ? task.id : null" />
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<ItemDialog
|
||||||
|
v-model="newProjectDialog"
|
||||||
|
save-button-text="Create"
|
||||||
|
title="New Project"
|
||||||
|
event-name="i-project"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<ProjectForm
|
||||||
|
item-id="new"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar"
|
||||||
|
:color="snackbarColor"
|
||||||
|
:timeout="3000"
|
||||||
|
top
|
||||||
|
>
|
||||||
|
{{ snackbarText }}
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
@click="snackbar = false"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
|
<v-navigation-drawer
|
||||||
|
app
|
||||||
|
dark
|
||||||
|
fixed
|
||||||
|
width="260"
|
||||||
|
v-model="drawer"
|
||||||
|
mobile-breakpoint="960"
|
||||||
|
v-if="$route.path.startsWith('/project/')"
|
||||||
|
>
|
||||||
|
<v-menu bottom max-width="235" v-if="project">
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-list class="pa-0">
|
||||||
|
<v-list-item
|
||||||
|
key="project"
|
||||||
|
class="app__project-selector"
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-avatar :color="getProjectColor(project)" size="24">
|
||||||
|
<span class="white--text">{{ getProjectInitials(project) }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title class="app__project-selector-title">
|
||||||
|
{{ project.name }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-chevron-down</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
v-for="(item, i) in projects"
|
||||||
|
:key="i"
|
||||||
|
:to="`/project/${item.id}`"
|
||||||
|
@click="selectProject(item.id)"
|
||||||
|
>
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-avatar :color="getProjectColor(item)" size="24">
|
||||||
|
<span class="white--text">{{ getProjectInitials(item) }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
</v-list-item-icon>
|
||||||
|
<v-list-item-content>{{ item.name }}</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item @click="newProjectDialog = true">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-plus</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
New project...
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<v-list class="pt-0" v-if="!project">
|
||||||
|
<v-list-item key="new_project" :to="`/project/new`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-plus</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>New Project</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-list class="pt-0" v-if="project">
|
||||||
|
<v-list-item key="dashboard" :to="`/project/${projectId}/history`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-view-dashboard</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Dashboard</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item key="templates" :to="`/project/${projectId}/templates`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-check-all</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Task Templates</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item key="inventory" :to="`/project/${projectId}/inventory`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-monitor-multiple</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Inventory</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item key="environment" :to="`/project/${projectId}/environment`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-code-braces</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Environment</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item key="keys" :to="`/project/${projectId}/keys`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-key-change</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Key Store</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item key="repositories" :to="`/project/${projectId}/repositories`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-git</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Playbook Repositories</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item key="team" :to="`/project/${projectId}/team`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-account-multiple</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Team</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-menu top max-width="235" nudge-top="12">
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-list class="pa-0">
|
||||||
|
<v-list-item
|
||||||
|
key="project"
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-account</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>{{ user.name }}</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-item key="users" to="/users">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-account-multiple</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
Users
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item key="edit" @click="userDialog = true">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-pencil</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
Edit Account
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item key="sign_out" @click="signOut()">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-exit-to-app</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
Sign Out
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<router-view :projectId="projectId" :userId="user ? user.id : null"></router-view>
|
||||||
|
</v-main>
|
||||||
|
|
||||||
|
</v-app>
|
||||||
|
|
||||||
|
<v-app v-else-if="state === 'loading'">
|
||||||
|
<v-main>
|
||||||
|
<v-container
|
||||||
|
fluid
|
||||||
|
fill-height
|
||||||
|
align-center
|
||||||
|
justify-center
|
||||||
|
class="pa-0"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
:size="70"
|
||||||
|
color="primary"
|
||||||
|
indeterminate
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
|
||||||
|
<v-app v-else></v-app>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__item {
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__item--link {
|
||||||
|
text-decoration-line: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__separator {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app__project-selector {
|
||||||
|
height: 64px;
|
||||||
|
.v-list-item__icon {
|
||||||
|
margin-top: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app__project-selector-title {
|
||||||
|
font-size: 1.25rem !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-application--is-ltr .v-list-item__action:first-child,
|
||||||
|
.v-application--is-ltr .v-list-item__icon:first-child {
|
||||||
|
margin-right: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-toolbar__content {
|
||||||
|
height: 64px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table-header {
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme--light.v-data-table > .v-data-table__wrapper > table > thead > tr:last-child > th {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table > .v-data-table__wrapper > table > tbody > tr {
|
||||||
|
background: transparent !important;
|
||||||
|
& > td:first-child {
|
||||||
|
//font-weight: bold !important;
|
||||||
|
a {
|
||||||
|
text-decoration-line: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table > .v-data-table__wrapper > table > tbody > tr > th,
|
||||||
|
.v-data-table > .v-data-table__wrapper > table > thead > tr > th,
|
||||||
|
.v-data-table > .v-data-table__wrapper > table > tfoot > tr > th,
|
||||||
|
.v-data-table > .v-data-table__wrapper > table > tbody > tr > td {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-toolbar__title {
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-app-bar__nav-icon {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-toolbar__title:not(:first-child) {
|
||||||
|
margin-left: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.v-app-bar__nav-icon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-toolbar__title:not(:first-child) {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import { getErrorMessage } from '@/lib/error';
|
||||||
|
import ItemDialog from '@/components/ItemDialog.vue';
|
||||||
|
import TaskLogView from '@/components/TaskLogView.vue';
|
||||||
|
import ProjectForm from '@/components/ProjectForm.vue';
|
||||||
|
import UserForm from '@/components/UserForm.vue';
|
||||||
|
import EventBus from './event-bus';
|
||||||
|
|
||||||
|
const PROJECT_COLORS = [
|
||||||
|
'red',
|
||||||
|
'blue',
|
||||||
|
'orange',
|
||||||
|
'green',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
UserForm,
|
||||||
|
ItemDialog,
|
||||||
|
TaskLogView,
|
||||||
|
ProjectForm,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
drawer: null,
|
||||||
|
user: null,
|
||||||
|
state: 'loading',
|
||||||
|
snackbar: false,
|
||||||
|
snackbarText: '',
|
||||||
|
snackbarColor: '',
|
||||||
|
projects: null,
|
||||||
|
newProjectDialog: null,
|
||||||
|
userDialog: null,
|
||||||
|
|
||||||
|
taskLogDialog: null,
|
||||||
|
task: null,
|
||||||
|
template: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
async projects(val) {
|
||||||
|
if (val.length === 0
|
||||||
|
&& this.$route.path.startsWith('/project/')
|
||||||
|
&& this.$route.path !== '/project/new') {
|
||||||
|
await this.$router.push({ path: '/project/new' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
projectId() {
|
||||||
|
return parseInt(this.$route.params.projectId, 10) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
project() {
|
||||||
|
return this.projects.find((x) => x.id === this.projectId);
|
||||||
|
},
|
||||||
|
|
||||||
|
isAuthenticated() {
|
||||||
|
return document.cookie.includes('semaphore=');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
if (this.$route.path !== '/auth/login') {
|
||||||
|
await this.$router.push({ path: '/auth/login' });
|
||||||
|
}
|
||||||
|
this.state = 'success';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.init();
|
||||||
|
this.state = 'success';
|
||||||
|
} catch (err) { // notify about problem and sign out
|
||||||
|
EventBus.$emit('i-snackbar', {
|
||||||
|
color: 'error',
|
||||||
|
text: getErrorMessage(err),
|
||||||
|
});
|
||||||
|
// EventBus.$emit('i-session-end');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
EventBus.$on('i-snackbar', (e) => {
|
||||||
|
this.snackbar = true;
|
||||||
|
this.snackbarColor = e.color;
|
||||||
|
this.snackbarText = e.text;
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$on('i-session-end', async () => {
|
||||||
|
await this.signOut();
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$on('i-session-create', async () => {
|
||||||
|
await this.init();
|
||||||
|
await this.trySelectMostSuitableProject();
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$on('i-account-change', async () => {
|
||||||
|
await this.loadUserInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$on('i-show-drawer', async () => {
|
||||||
|
this.drawer = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$on('i-show-task', async (e) => {
|
||||||
|
this.task = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/tasks/${e.taskId}`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.template = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/templates/${this.task.template_id}`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.taskLogDialog = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$on('i-open-last-project', async () => {
|
||||||
|
await this.trySelectMostSuitableProject();
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$on('i-user', async (e) => {
|
||||||
|
let text;
|
||||||
|
|
||||||
|
switch (e.action) {
|
||||||
|
case 'new':
|
||||||
|
text = `User ${e.item.name} created`;
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
text = `User ${e.item.name} saved`;
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
text = `User ${e.item.name} deleted`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown project action');
|
||||||
|
}
|
||||||
|
|
||||||
|
EventBus.$emit('i-snackbar', {
|
||||||
|
color: 'success',
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.user && e.item.id === this.user.id) {
|
||||||
|
await this.loadUserInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$on('i-project', async (e) => {
|
||||||
|
let text;
|
||||||
|
|
||||||
|
const project = this.projects.find((p) => p.id === e.item.id) || e.item;
|
||||||
|
const projectName = project.name || `#${project.id}`;
|
||||||
|
|
||||||
|
switch (e.action) {
|
||||||
|
case 'new':
|
||||||
|
text = `Project ${projectName} created`;
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
text = `Project ${projectName} saved`;
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
text = `Project ${projectName} deleted`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown project action');
|
||||||
|
}
|
||||||
|
|
||||||
|
EventBus.$emit('i-snackbar', {
|
||||||
|
color: 'success',
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.loadProjects();
|
||||||
|
|
||||||
|
switch (e.action) {
|
||||||
|
case 'new':
|
||||||
|
await this.selectProject(e.item.id);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
if (this.projectId === e.item.id && this.projects.length > 0) {
|
||||||
|
await this.selectProject(this.projects[0].id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
await this.loadUserInfo();
|
||||||
|
await this.loadProjects();
|
||||||
|
|
||||||
|
if (this.$route.path === '/'
|
||||||
|
|| this.$route.path === '/project'
|
||||||
|
|| (this.$route.path.startsWith('/project/'))) {
|
||||||
|
// try to find project and switch to it
|
||||||
|
await this.trySelectMostSuitableProject();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$route.query.t) {
|
||||||
|
EventBus.$emit('i-show-task', {
|
||||||
|
itemId: parseInt(this.$route.query.t || '', 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async trySelectMostSuitableProject() {
|
||||||
|
if (this.projects.length === 0) {
|
||||||
|
if (this.$route.path !== '/project/new') {
|
||||||
|
await this.$router.push({ path: '/project/new' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let projectId;
|
||||||
|
|
||||||
|
if (this.projectId) {
|
||||||
|
projectId = this.projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((projectId == null || !this.projects.some((p) => p.id === projectId))
|
||||||
|
&& localStorage.getItem('projectId')) {
|
||||||
|
projectId = parseInt(localStorage.getItem('projectId'), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId == null || !this.projects.some((p) => p.id === projectId)) {
|
||||||
|
projectId = this.projects[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId != null) {
|
||||||
|
await this.selectProject(projectId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async selectProject(projectId) {
|
||||||
|
localStorage.setItem('projectId', projectId);
|
||||||
|
if (this.projectId === projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.$router.push({ path: `/project/${projectId}` });
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadProjects() {
|
||||||
|
this.projects = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/projects',
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadUserInfo() {
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.user = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/user',
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getProjectColor(projectData) {
|
||||||
|
const projectIndex = this.projects.length
|
||||||
|
- this.projects.findIndex((p) => p.id === projectData.id);
|
||||||
|
return PROJECT_COLORS[projectIndex % PROJECT_COLORS.length];
|
||||||
|
},
|
||||||
|
|
||||||
|
getProjectInitials(projectData) {
|
||||||
|
const parts = projectData.name.split(/\s/);
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
||||||
|
}
|
||||||
|
return parts[0].substr(0, 2).toUpperCase();
|
||||||
|
},
|
||||||
|
|
||||||
|
async signOut() {
|
||||||
|
this.snackbar = false;
|
||||||
|
this.snackbarColor = '';
|
||||||
|
this.snackbarText = '';
|
||||||
|
|
||||||
|
(await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/auth/logout',
|
||||||
|
responseType: 'json',
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (this.$route.path !== '/auth/login') {
|
||||||
|
await this.$router.push({ path: '/auth/login' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
BIN
web2/src/assets/logo.png
Normal file
BIN
web2/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
1
web2/src/assets/logo.svg
Normal file
1
web2/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
After Width: | Height: | Size: 539 B |
51
web2/src/components/EnvironmentForm.vue
Normal file
51
web2/src/components/EnvironmentForm.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="item != null"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:value="formError"
|
||||||
|
color="error"
|
||||||
|
class="pb-2"
|
||||||
|
>{{ formError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.name"
|
||||||
|
label="Environment Name"
|
||||||
|
:rules="[v => !!v || 'Name is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
class="mb-4"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="item.json"
|
||||||
|
label="Environment (This has to be a JSON object)"
|
||||||
|
:disabled="formSaving"
|
||||||
|
solo
|
||||||
|
></v-textarea>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Must be valid JSON. You may use the key ENV to pass a json object which sets environmental
|
||||||
|
variables for the ansible command execution environment
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/environment`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/environment/${this.itemId}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
99
web2/src/components/InventoryForm.vue
Normal file
99
web2/src/components/InventoryForm.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="item != null && keys != null"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:value="formError"
|
||||||
|
color="error"
|
||||||
|
class="pb-2"
|
||||||
|
>{{ formError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.name"
|
||||||
|
label="Name"
|
||||||
|
:rules="[v => !!v || 'Name is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.ssh_key_id"
|
||||||
|
label="SSH Key"
|
||||||
|
:items="keys"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
:rules="[v => !!v || 'SSH Key is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.type"
|
||||||
|
label="Type"
|
||||||
|
:rules="[v => !!v || 'Type is required']"
|
||||||
|
:items="inventoryTypes"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.inventory"
|
||||||
|
label="Path to inventory file"
|
||||||
|
:rules="[v => !!v || 'Path to inventory file is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
v-if="item.type === 'file'"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="item.inventory"
|
||||||
|
label="Inventory"
|
||||||
|
:rules="[v => !!v || 'Inventory is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
v-if="item.type === 'static'"
|
||||||
|
solo
|
||||||
|
></v-textarea>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keys: null,
|
||||||
|
inventoryTypes: [{
|
||||||
|
id: 'static',
|
||||||
|
name: 'Static',
|
||||||
|
}, {
|
||||||
|
id: 'file',
|
||||||
|
name: 'File',
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.keys = (await axios({
|
||||||
|
keys: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/keys`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/inventory`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/inventory/${this.itemId}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
106
web2/src/components/ItemDialog.vue
Normal file
106
web2/src/components/ItemDialog.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialog"
|
||||||
|
:max-width="maxWidth || 400"
|
||||||
|
persistent
|
||||||
|
:transition="false"
|
||||||
|
:content-class="'item-dialog item-dialog--' + position"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
<slot
|
||||||
|
name="title"
|
||||||
|
>{{ title }}</slot>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<slot
|
||||||
|
name="form"
|
||||||
|
:onSave="close"
|
||||||
|
:onError="clearFlags"
|
||||||
|
:needSave="needSave"
|
||||||
|
:needReset="needReset"
|
||||||
|
></slot>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
@click="close()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
@click="needSave = true"
|
||||||
|
>
|
||||||
|
{{ saveButtonText }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
.item-dialog--top {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
.item-dialog--center {
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
position: String,
|
||||||
|
title: String,
|
||||||
|
saveButtonText: String,
|
||||||
|
value: Boolean,
|
||||||
|
maxWidth: Number,
|
||||||
|
eventName: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialog: false,
|
||||||
|
needSave: false,
|
||||||
|
needReset: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
async dialog(val) {
|
||||||
|
this.$emit('input', val);
|
||||||
|
this.needReset = val;
|
||||||
|
},
|
||||||
|
|
||||||
|
async value(val) {
|
||||||
|
this.dialog = val;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close(e) {
|
||||||
|
this.dialog = false;
|
||||||
|
this.clearFlags();
|
||||||
|
if (e) {
|
||||||
|
this.$emit('save', e);
|
||||||
|
if (this.eventName) {
|
||||||
|
EventBus.$emit(this.eventName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFlags() {
|
||||||
|
this.needSave = false;
|
||||||
|
this.needReset = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
110
web2/src/components/ItemFormBase.js
Normal file
110
web2/src/components/ItemFormBase.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { getErrorMessage } from '@/lib/error';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
itemId: [Number, String],
|
||||||
|
projectId: [Number, String],
|
||||||
|
needSave: Boolean,
|
||||||
|
needReset: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
item: null,
|
||||||
|
formValid: false,
|
||||||
|
formError: null,
|
||||||
|
formSaving: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
await this.loadData();
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isNew() {
|
||||||
|
return this.itemId === 'new';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
async needSave(val) {
|
||||||
|
if (val) {
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async needReset(val) {
|
||||||
|
if (val) {
|
||||||
|
await this.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async reset() {
|
||||||
|
this.item = null;
|
||||||
|
if (this.$refs.form) {
|
||||||
|
this.$refs.form.resetValidation();
|
||||||
|
}
|
||||||
|
await this.loadData();
|
||||||
|
},
|
||||||
|
|
||||||
|
getItemsUrl() {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
if (this.isNew) {
|
||||||
|
this.item = {};
|
||||||
|
} else {
|
||||||
|
this.item = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: this.getSingleItemUrl(),
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves or creates item via API.
|
||||||
|
* @returns {Promise<null>} null if validation didn't pass or user data if user saved.
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
this.formError = null;
|
||||||
|
|
||||||
|
if (!this.$refs.form.validate()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formSaving = true;
|
||||||
|
let item;
|
||||||
|
|
||||||
|
try {
|
||||||
|
item = (await axios({
|
||||||
|
method: this.isNew ? 'post' : 'put',
|
||||||
|
url: this.isNew
|
||||||
|
? this.getItemsUrl()
|
||||||
|
: this.getSingleItemUrl(),
|
||||||
|
responseType: 'json',
|
||||||
|
data: this.item,
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.$emit('save', {
|
||||||
|
item: item || this.item,
|
||||||
|
action: this.isNew ? 'new' : 'edit',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.formError = getErrorMessage(err);
|
||||||
|
} finally {
|
||||||
|
this.formSaving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item || this.item;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
100
web2/src/components/ItemListPageBase.js
Normal file
100
web2/src/components/ItemListPageBase.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
import InventoryForm from '@/components/InventoryForm.vue';
|
||||||
|
import ItemDialog from '@/components/ItemDialog.vue';
|
||||||
|
import YesNoDialog from '@/components/YesNoDialog.vue';
|
||||||
|
import { getErrorMessage } from '@/lib/error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
YesNoDialog,
|
||||||
|
ItemDialog,
|
||||||
|
InventoryForm,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
projectId: Number,
|
||||||
|
userId: Number,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
items: null,
|
||||||
|
itemId: null,
|
||||||
|
editDialog: null,
|
||||||
|
deleteItemDialog: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
await this.loadItems();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getSingleItemUrl() {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
|
||||||
|
getHeaders() {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
|
||||||
|
getEventName() {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
|
||||||
|
showDrawer() {
|
||||||
|
EventBus.$emit('i-show-drawer');
|
||||||
|
},
|
||||||
|
|
||||||
|
async onItemSave() {
|
||||||
|
await this.loadItems();
|
||||||
|
},
|
||||||
|
|
||||||
|
askDeleteItem(itemId) {
|
||||||
|
this.itemId = itemId;
|
||||||
|
this.deleteItemDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteItem(itemId) {
|
||||||
|
try {
|
||||||
|
const item = this.items.find((x) => x.id === itemId);
|
||||||
|
|
||||||
|
await axios({
|
||||||
|
method: 'delete',
|
||||||
|
url: this.getSingleItemUrl(),
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$emit(this.getEventName(), {
|
||||||
|
action: 'delete',
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.loadItems();
|
||||||
|
} catch (err) {
|
||||||
|
EventBus.$emit('i-snackbar', {
|
||||||
|
color: 'error',
|
||||||
|
text: getErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
editItem(itemId) {
|
||||||
|
this.itemId = itemId;
|
||||||
|
this.editDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadItems() {
|
||||||
|
this.items = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: this.getItemsUrl(),
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
81
web2/src/components/KeyForm.vue
Normal file
81
web2/src/components/KeyForm.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="item != null"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:value="formError"
|
||||||
|
color="error"
|
||||||
|
class="pb-2"
|
||||||
|
>{{ formError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.name"
|
||||||
|
label="Key Name"
|
||||||
|
:rules="[v => !!v || 'Name is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.type"
|
||||||
|
label="Type"
|
||||||
|
:rules="[v => !!v || 'Type is required']"
|
||||||
|
:items="inventoryTypes"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="item.key"
|
||||||
|
label="Public Key"
|
||||||
|
:disabled="formSaving"
|
||||||
|
v-if="item.type === 'ssh'"
|
||||||
|
></v-textarea>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="item.secret"
|
||||||
|
label="Private Key"
|
||||||
|
:disabled="formSaving"
|
||||||
|
v-if="item.type === 'ssh'"
|
||||||
|
></v-textarea>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keys: null,
|
||||||
|
inventoryTypes: [{
|
||||||
|
id: 'ssh',
|
||||||
|
name: 'SSH Key',
|
||||||
|
}, {
|
||||||
|
id: 'aws',
|
||||||
|
name: 'AWS IAM credentials',
|
||||||
|
}, {
|
||||||
|
id: 'gcloud',
|
||||||
|
name: 'Google Cloud API Key',
|
||||||
|
}, {
|
||||||
|
id: 'do',
|
||||||
|
name: 'DigitalOcean API Key',
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/keys`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/keys/${this.itemId}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
48
web2/src/components/ProjectForm.vue
Normal file
48
web2/src/components/ProjectForm.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="item != null"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:value="formError"
|
||||||
|
color="error"
|
||||||
|
class="pb-2"
|
||||||
|
>{{ formError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.name"
|
||||||
|
label="Playbook Alias"
|
||||||
|
:rules="[v => !!v || 'Project name is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-checkbox
|
||||||
|
v-model="item.alert"
|
||||||
|
label="Allow alerts for this project"
|
||||||
|
></v-checkbox>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.alert_chat"
|
||||||
|
label="Chat ID"
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return '/api/projects';
|
||||||
|
},
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.itemId}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
77
web2/src/components/RepositoryForm.vue
Normal file
77
web2/src/components/RepositoryForm.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="item != null && keys != null"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:value="formError"
|
||||||
|
color="error"
|
||||||
|
class="pb-2"
|
||||||
|
>{{ formError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.name"
|
||||||
|
label="Name"
|
||||||
|
:rules="[v => !!v || 'Name is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.git_url"
|
||||||
|
label="Git URL"
|
||||||
|
:rules="[v => !!v || 'Repository is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.ssh_key_id"
|
||||||
|
label="SSH Key"
|
||||||
|
:items="keys"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
:rules="[v => !!v || 'SSH Key is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keys: null,
|
||||||
|
inventoryTypes: [{
|
||||||
|
id: 'static',
|
||||||
|
name: 'Static',
|
||||||
|
}, {
|
||||||
|
id: 'file',
|
||||||
|
name: 'File',
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.keys = (await axios({
|
||||||
|
keys: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/keys`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/repositories`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/repositories/${this.itemId}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
52
web2/src/components/TaskForm.vue
Normal file
52
web2/src/components/TaskForm.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="item != null"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:value="formError"
|
||||||
|
color="error"
|
||||||
|
class="pb-2"
|
||||||
|
>{{ formError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.playbook"
|
||||||
|
label="Playbook Override"
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="item.environment"
|
||||||
|
label="Environment Override (*MUST* be valid JSON)"
|
||||||
|
:disabled="formSaving"
|
||||||
|
rows="4"
|
||||||
|
></v-textarea>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="item.arguments"
|
||||||
|
label="Extra CLI Arguments"
|
||||||
|
:disabled="formSaving"
|
||||||
|
rows="4"
|
||||||
|
></v-textarea>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
props: {
|
||||||
|
templateId: Number,
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.item.template_id = this.templateId;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/tasks`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
120
web2/src/components/TaskLogView.vue
Normal file
120
web2/src/components/TaskLogView.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="item != null && output != null && user != null">
|
||||||
|
<v-container class="pa-0">
|
||||||
|
<v-row no-gutters>
|
||||||
|
<v-col>
|
||||||
|
<v-list two-line subheader class="pa-0">
|
||||||
|
<v-list-item class="pa-0">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Author</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user.name }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item class="pa-0">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Status</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.status }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-list two-line subheader class="pa-0">
|
||||||
|
<v-list-item class="pa-0">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Created</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.created }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item class="pa-0">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Started</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.start || '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item class="pa-0">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Ended</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.end || '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<div class="task-log-view">
|
||||||
|
<div class="task-log-view__record" v-for="record in output" :key="record.id">
|
||||||
|
<div class="task-log-view__time">{{ record.time }}</div>
|
||||||
|
<div class="task-log-view__output">{{ record.output }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
.task-log-view {
|
||||||
|
height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid gray;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log-view__record {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log-view__time {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log-view__output {
|
||||||
|
width: calc(100% - 250px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
itemId: Number,
|
||||||
|
projectId: Number,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
item: null,
|
||||||
|
output: null,
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData() {
|
||||||
|
this.item = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/tasks/${this.itemId}`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.output = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/tasks/${this.itemId}/output`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.user = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/users/${this.item.user_id}`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
63
web2/src/components/TeamMemberForm.vue
Normal file
63
web2/src/components/TeamMemberForm.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="users != null"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:value="formError"
|
||||||
|
color="error"
|
||||||
|
class="pb-2"
|
||||||
|
>{{ formError }}</v-alert>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.user_id"
|
||||||
|
label="User"
|
||||||
|
:items="users"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
:rules="[v => !!v || 'User is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
|
||||||
|
<v-checkbox
|
||||||
|
v-model="item.admin"
|
||||||
|
label="Administrator"
|
||||||
|
></v-checkbox>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
users: null,
|
||||||
|
userId: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
this.users = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/users',
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/users`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/users/${this.itemId}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
128
web2/src/components/TemplateForm.vue
Normal file
128
web2/src/components/TemplateForm.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="isLoaded"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.alias"
|
||||||
|
label="Playbook Alias"
|
||||||
|
:rules="[v => !!v || 'Playbook Alias is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.playbook"
|
||||||
|
label="Playbook Name"
|
||||||
|
:rules="[v => !!v || 'Playbook Name is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.ssh_key_id"
|
||||||
|
label="SSH Key"
|
||||||
|
:items="keys"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
:rules="[v => !!v || 'SSH Key is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.inventory_id"
|
||||||
|
label="Inventory"
|
||||||
|
:items="inventory"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
:rules="[v => !!v || 'Inventory is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.repository_id"
|
||||||
|
label="Playbook Repository"
|
||||||
|
:items="repositories"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
:rules="[v => !!v || 'Playbook Repository is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="item.environment_id"
|
||||||
|
label="Environment"
|
||||||
|
:items="environment"
|
||||||
|
item-value="id"
|
||||||
|
item-text="name"
|
||||||
|
:rules="[v => !!v || 'Environment is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-select>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
item: null,
|
||||||
|
keys: null,
|
||||||
|
inventory: null,
|
||||||
|
repositories: null,
|
||||||
|
environment: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
this.keys = (await axios({
|
||||||
|
keys: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/keys`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
this.repositories = (await axios({
|
||||||
|
keys: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/repositories`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
this.inventory = (await axios({
|
||||||
|
keys: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/inventory`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
this.environment = (await axios({
|
||||||
|
keys: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/environment`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isLoaded() {
|
||||||
|
if (this.isNew) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.keys && this.repositories && this.inventory && this.environment && this.item;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/templates`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/templates/${this.itemId}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
64
web2/src/components/UserForm.vue
Normal file
64
web2/src/components/UserForm.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
lazy-validation
|
||||||
|
v-model="formValid"
|
||||||
|
v-if="item != null"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:value="formError"
|
||||||
|
color="error"
|
||||||
|
class="pb-2"
|
||||||
|
>{{ formError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.name"
|
||||||
|
label="Name"
|
||||||
|
:rules="[v => !!v || 'Name is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.username"
|
||||||
|
label="Username"
|
||||||
|
:rules="[v => !!v || 'Username is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.email"
|
||||||
|
label="Email"
|
||||||
|
:rules="[v => !!v || 'Email is required']"
|
||||||
|
required
|
||||||
|
:disabled="formSaving"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-checkbox
|
||||||
|
v-model="item.admin"
|
||||||
|
label="Admin user"
|
||||||
|
></v-checkbox>
|
||||||
|
|
||||||
|
<v-checkbox
|
||||||
|
v-model="item.alert"
|
||||||
|
label="Send alerts"
|
||||||
|
></v-checkbox>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemFormBase from '@/components/ItemFormBase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemFormBase],
|
||||||
|
methods: {
|
||||||
|
getItemsUrl() {
|
||||||
|
return '/api/users';
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/users/${this.itemId}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
71
web2/src/components/YesNoDialog.vue
Normal file
71
web2/src/components/YesNoDialog.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialog"
|
||||||
|
max-width="290"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">{{ title }}</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>{{ text }}</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
@click="no()"
|
||||||
|
>
|
||||||
|
{{ noButtonTitle || 'Cancel' }}
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
@click="yes()"
|
||||||
|
>
|
||||||
|
{{ yesButtonTitle || 'Yes' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
title: String,
|
||||||
|
text: String,
|
||||||
|
yesButtonTitle: String,
|
||||||
|
noButtonTitle: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialog: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
async dialog(val) {
|
||||||
|
this.$emit('input', val);
|
||||||
|
},
|
||||||
|
|
||||||
|
async value(val) {
|
||||||
|
this.dialog = val;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async yes() {
|
||||||
|
this.$emit('yes');
|
||||||
|
this.dialog = false;
|
||||||
|
},
|
||||||
|
async no() {
|
||||||
|
this.$emit('no');
|
||||||
|
this.dialog = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
3
web2/src/event-bus.js
Normal file
3
web2/src/event-bus.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default new Vue();
|
3
web2/src/lib/delay.js
Normal file
3
web2/src/lib/delay.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function delay(milliseconds = 100) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||||
|
}
|
7
web2/src/lib/error.js
Normal file
7
web2/src/lib/error.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export function getErrorMessage(err) {
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
return err.response.data.error || err.message;
|
||||||
|
}
|
||||||
|
return err.message;
|
||||||
|
}
|
12
web2/src/main.js
Normal file
12
web2/src/main.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import router from './router';
|
||||||
|
import vuetify from './plugins/vuetify';
|
||||||
|
|
||||||
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
vuetify,
|
||||||
|
render: (h) => h(App),
|
||||||
|
}).$mount('#app');
|
7
web2/src/plugins/vuetify.js
Normal file
7
web2/src/plugins/vuetify.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Vuetify from 'vuetify/lib';
|
||||||
|
|
||||||
|
Vue.use(Vuetify);
|
||||||
|
|
||||||
|
export default new Vuetify({
|
||||||
|
});
|
89
web2/src/router/index.js
Normal file
89
web2/src/router/index.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import VueRouter from 'vue-router';
|
||||||
|
import History from '../views/project/History.vue';
|
||||||
|
import Activity from '../views/project/Activity.vue';
|
||||||
|
import Settings from '../views/project/Settings.vue';
|
||||||
|
import Templates from '../views/project/Templates.vue';
|
||||||
|
import TemplateView from '../views/project/TemplateView.vue';
|
||||||
|
import Environment from '../views/project/Environment.vue';
|
||||||
|
import Inventory from '../views/project/Inventory.vue';
|
||||||
|
import Keys from '../views/project/Keys.vue';
|
||||||
|
import Repositories from '../views/project/Repositories.vue';
|
||||||
|
import Team from '../views/project/Team.vue';
|
||||||
|
import Users from '../views/Users.vue';
|
||||||
|
import Auth from '../views/Auth.vue';
|
||||||
|
import New from '../views/project/New.vue';
|
||||||
|
import ChangePassword from '../views/ChangePassword.vue';
|
||||||
|
|
||||||
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/project/new',
|
||||||
|
component: New,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId',
|
||||||
|
redirect: '/project/:projectId/history',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/history',
|
||||||
|
component: History,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/activity',
|
||||||
|
component: Activity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/settings',
|
||||||
|
component: Settings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/templates',
|
||||||
|
component: Templates,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/templates/:templateId',
|
||||||
|
component: TemplateView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/environment',
|
||||||
|
component: Environment,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/inventory',
|
||||||
|
component: Inventory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/repositories',
|
||||||
|
component: Repositories,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/keys',
|
||||||
|
component: Keys,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/team',
|
||||||
|
component: Team,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth/login',
|
||||||
|
component: Auth,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
component: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/change-password',
|
||||||
|
component: ChangePassword,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
mode: 'history',
|
||||||
|
base: process.env.BASE_URL,
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
232
web2/src/views/Auth.vue
Normal file
232
web2/src/views/Auth.vue
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth">
|
||||||
|
<v-dialog
|
||||||
|
v-model="forgotPasswordDialog"
|
||||||
|
max-width="290"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">Forgot password?</v-card-title>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
:value="forgotPasswordSubmitted"
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
Check your inbox.
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
:value="forgotPasswordError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ forgotPasswordError }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-form
|
||||||
|
ref="forgotPasswordForm"
|
||||||
|
lazy-validation
|
||||||
|
v-model="forgotPasswordFormValid"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="email"
|
||||||
|
label="Email"
|
||||||
|
:rules="emailRules"
|
||||||
|
required
|
||||||
|
:disabled="forgotPasswordSubmitting"
|
||||||
|
></v-text-field>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="green darken-1"
|
||||||
|
text
|
||||||
|
:disabled="forgotPasswordSubmitting"
|
||||||
|
@click="forgotPasswordDialog = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="green darken-1"
|
||||||
|
text
|
||||||
|
:disabled="forgotPasswordSubmitting"
|
||||||
|
@click="submitForgotPassword()"
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-container
|
||||||
|
fluid
|
||||||
|
fill-height
|
||||||
|
align-center
|
||||||
|
justify-center
|
||||||
|
class="pa-0"
|
||||||
|
>
|
||||||
|
<v-form
|
||||||
|
ref="signInForm"
|
||||||
|
lazy-validation
|
||||||
|
v-model="signInFormValid"
|
||||||
|
style="width: 300px; height: 300px;"
|
||||||
|
>
|
||||||
|
<h3 class="text-center mb-8">SEMAPHORE</h3>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
:value="signInError"
|
||||||
|
color="error"
|
||||||
|
style="margin-bottom: 20px;"
|
||||||
|
>{{ signInError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="username"
|
||||||
|
label="Username"
|
||||||
|
:rules="usernameRules"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
:disabled="signInProcess"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
label="Password"
|
||||||
|
:rules="[v => !!v || 'Password is required']"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
:disabled="signInProcess"
|
||||||
|
@keyup.enter.native="signIn"
|
||||||
|
style="margin-bottom: 20px;"
|
||||||
|
></v-text-field>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="signIn"
|
||||||
|
:disabled="signInProcess"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
.auth {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import { getErrorMessage } from '@/lib/error';
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
signInFormValid: false,
|
||||||
|
signInError: null,
|
||||||
|
signInProcess: false,
|
||||||
|
password: '',
|
||||||
|
username: '',
|
||||||
|
|
||||||
|
forgotPasswordFormValid: false,
|
||||||
|
forgotPasswordError: false,
|
||||||
|
forgotPasswordSubmitted: false,
|
||||||
|
forgotPasswordSubmitting: false,
|
||||||
|
forgotPasswordDialog: false,
|
||||||
|
email: '',
|
||||||
|
|
||||||
|
newPassword: '',
|
||||||
|
newPassword2: '',
|
||||||
|
|
||||||
|
emailRules: [
|
||||||
|
(v) => !!v || 'Email is required',
|
||||||
|
],
|
||||||
|
passwordRules: [
|
||||||
|
(v) => !!v || 'Password is required',
|
||||||
|
(v) => v.length >= 6 || 'Password too short. Min 6 characters',
|
||||||
|
],
|
||||||
|
usernameRules: [
|
||||||
|
(v) => !!v || 'Username is required',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
if (this.isAuthenticated()) {
|
||||||
|
EventBus.$emit('i-session-create');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
isAuthenticated() {
|
||||||
|
return document.cookie.includes('semaphore=');
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitForgotPassword() {
|
||||||
|
this.forgotPasswordSubmitted = false;
|
||||||
|
this.forgotPasswordError = null;
|
||||||
|
|
||||||
|
if (!this.$refs.forgotPasswordForm.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.forgotPasswordSubmitting = true;
|
||||||
|
try {
|
||||||
|
await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: '/v1/session/forgot-password',
|
||||||
|
data: {
|
||||||
|
email: this.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.forgotPasswordSubmitted = true;
|
||||||
|
} catch (err) {
|
||||||
|
this.forgotPasswordError = err.response.data.error;
|
||||||
|
} finally {
|
||||||
|
this.forgotPasswordSubmitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async signIn() {
|
||||||
|
this.signInError = null;
|
||||||
|
|
||||||
|
if (!this.$refs.signInForm.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.signInProcess = true;
|
||||||
|
try {
|
||||||
|
await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/auth/login',
|
||||||
|
responseType: 'json',
|
||||||
|
data: {
|
||||||
|
auth: this.username,
|
||||||
|
password: this.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$emit('i-session-create');
|
||||||
|
} catch (err) {
|
||||||
|
this.signInError = getErrorMessage(err);
|
||||||
|
} finally {
|
||||||
|
this.signInProcess = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
forgotPassword() {
|
||||||
|
this.forgotPasswordError = null;
|
||||||
|
this.forgotPasswordSubmitted = false;
|
||||||
|
this.email = '';
|
||||||
|
this.$refs.forgotPasswordForm.resetValidation();
|
||||||
|
this.forgotPasswordDialog = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
112
web2/src/views/ChangePassword.vue
Normal file
112
web2/src/views/ChangePassword.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<v-container
|
||||||
|
fluid
|
||||||
|
fill-height
|
||||||
|
align-center
|
||||||
|
justify-center
|
||||||
|
class="pa-0"
|
||||||
|
>
|
||||||
|
<v-form
|
||||||
|
ref="changePasswordForm"
|
||||||
|
lazy-validation
|
||||||
|
v-model="changePasswordFormValid"
|
||||||
|
style="width: 300px;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
:value="changePasswordError"
|
||||||
|
color="error"
|
||||||
|
style="margin-bottom: 20px;"
|
||||||
|
>{{ changePasswordError }}</v-alert>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="newPassword"
|
||||||
|
label="Password"
|
||||||
|
:rules="passwordRules"
|
||||||
|
type="password"
|
||||||
|
counter
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="newPassword2"
|
||||||
|
label="Repeat password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
counter
|
||||||
|
style="margin-bottom: 20px;"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<div class="text-xs-right">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="changePassword"
|
||||||
|
style="margin-right: 0;"
|
||||||
|
align-end
|
||||||
|
>
|
||||||
|
Change password
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
import { getErrorMessage } from '@/lib/error';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
changePasswordFormValid: false,
|
||||||
|
changePasswordError: null,
|
||||||
|
changePasswordInProgress: false,
|
||||||
|
newPassword: '',
|
||||||
|
newPassword2: '',
|
||||||
|
|
||||||
|
passwordRules: [
|
||||||
|
(v) => !!v || 'Password is required',
|
||||||
|
(v) => v.length >= 6 || 'Password too short. Min 6 characters',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async changePassword() {
|
||||||
|
this.changePasswordError = null;
|
||||||
|
|
||||||
|
if (!this.$refs.changePasswordForm.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.newPassword !== this.newPassword2) {
|
||||||
|
this.changePasswordError = 'Passwords not equal';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changePasswordInProgress = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: '/v1/session/change-password',
|
||||||
|
data: {
|
||||||
|
token: this.$route.query.token,
|
||||||
|
password: this.newPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.$router.replace('/');
|
||||||
|
EventBus.$emit('i-snackbar', {
|
||||||
|
color: 'success',
|
||||||
|
text: 'Password changed',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.changePasswordError = getErrorMessage(err);
|
||||||
|
} finally {
|
||||||
|
this.changePasswordInProgress = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
140
web2/src/views/Users.vue
Normal file
140
web2/src/views/Users.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="items != null">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
save-button-text="Save"
|
||||||
|
title="Edit User"
|
||||||
|
@save="loadItems()"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<UserForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<YesNoDialog
|
||||||
|
title="Delete user"
|
||||||
|
text="Are you really want to delete this user?"
|
||||||
|
v-model="deleteItemDialog"
|
||||||
|
@yes="deleteItem(itemId)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-4"
|
||||||
|
@click="returnToProjects()"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-arrow-left</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-toolbar-title>Users</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="editItem('new')"
|
||||||
|
>New User</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
:items-per-page="Number.MAX_VALUE"
|
||||||
|
>
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<div style="white-space: nowrap">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
@click="askDeleteItem(item.id)"
|
||||||
|
:disabled="item.id === userId"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
@click="editItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
import YesNoDialog from '@/components/YesNoDialog.vue';
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
import ItemDialog from '@/components/ItemDialog.vue';
|
||||||
|
import UserForm from '@/components/UserForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
YesNoDialog,
|
||||||
|
UserForm,
|
||||||
|
ItemDialog,
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getHeaders() {
|
||||||
|
return [{
|
||||||
|
text: 'Name',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Username',
|
||||||
|
value: 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Email',
|
||||||
|
value: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Alert',
|
||||||
|
value: 'alert',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Admin',
|
||||||
|
value: 'admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'External',
|
||||||
|
value: 'external',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Actions',
|
||||||
|
value: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
async returnToProjects() {
|
||||||
|
EventBus.$emit('i-open-last-project');
|
||||||
|
},
|
||||||
|
|
||||||
|
getItemsUrl() {
|
||||||
|
return '/api/users';
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/users/${this.itemId}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getEventName() {
|
||||||
|
return 'i-user';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
52
web2/src/views/project/Activity.vue
Normal file
52
web2/src/views/project/Activity.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="items">
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Dashboard</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<div>
|
||||||
|
<v-tabs centered>
|
||||||
|
<v-tab key="history" :to="`/project/${projectId}/history`">History</v-tab>
|
||||||
|
<v-tab key="activity" :to="`/project/${projectId}/activity`">Activity</v-tab>
|
||||||
|
<v-tab key="settings" :to="`/project/${projectId}/settings`">Settings</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
</div>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getHeaders() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Time',
|
||||||
|
value: 'created',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Description',
|
||||||
|
value: 'description',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/events/last`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
113
web2/src/views/project/Environment.vue
Normal file
113
web2/src/views/project/Environment.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="items != null">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
save-button-text="Save"
|
||||||
|
title="Edit Environment"
|
||||||
|
:max-width="500"
|
||||||
|
@save="loadItems"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<EnvironmentForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<YesNoDialog
|
||||||
|
title="Delete environment"
|
||||||
|
text="Are you really want to delete this environment?"
|
||||||
|
v-model="deleteItemDialog"
|
||||||
|
@yes="deleteItem(itemId)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Environment</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="editItem('new')"
|
||||||
|
>New Environment</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
:items-per-page="Number.MAX_VALUE"
|
||||||
|
>
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<div style="white-space: nowrap">
|
||||||
|
<v-tooltip bottom>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
@click="askDeleteItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<span>Delete environment</span>
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
|
<v-tooltip bottom>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
@click="editItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<span>Edit environment</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
import EnvironmentForm from '@/components/EnvironmentForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { EnvironmentForm },
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
methods: {
|
||||||
|
getHeaders() {
|
||||||
|
return [{
|
||||||
|
text: 'Name',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Actions',
|
||||||
|
value: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/environment`;
|
||||||
|
},
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/environment/${this.itemId}`;
|
||||||
|
},
|
||||||
|
getEventName() {
|
||||||
|
return 'i-environment';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
86
web2/src/views/project/History.vue
Normal file
86
web2/src/views/project/History.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="items != null">
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Dashboard</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<div>
|
||||||
|
<v-tabs centered>
|
||||||
|
<v-tab key="history" :to="`/project/${projectId}/history`">History</v-tab>
|
||||||
|
<v-tab key="activity" :to="`/project/${projectId}/activity`">Activity</v-tab>
|
||||||
|
<v-tab key="settings" :to="`/project/${projectId}/settings`">Settings</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
</div>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<template v-slot:item.tpl_alias="{ item }">
|
||||||
|
<a @click="showTaskLog(item.id)">{{ item.tpl_alias }}</a>
|
||||||
|
<span style="color: gray; margin-left: 10px;">#{{ item.id }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.status="{ item }">
|
||||||
|
{{ item.status }}
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
async projectId() {
|
||||||
|
await this.loadItems();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
showTaskLog(taskId) {
|
||||||
|
EventBus.$emit('i-show-task', {
|
||||||
|
taskId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getHeaders() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Task',
|
||||||
|
value: 'tpl_alias',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Status',
|
||||||
|
value: 'status',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'User',
|
||||||
|
value: 'user_name',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Start',
|
||||||
|
value: 'start',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Duration',
|
||||||
|
value: 'start',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/tasks/last`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
108
web2/src/views/project/Inventory.vue
Normal file
108
web2/src/views/project/Inventory.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="items != null">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
:save-button-text="itemId === 'new' ? 'Create' : 'Save'"
|
||||||
|
:title="`${itemId === 'new' ? 'New' : 'Edit'} Inventory`"
|
||||||
|
:max-width="450"
|
||||||
|
@save="loadItems"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<InventoryForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<YesNoDialog
|
||||||
|
title="Delete inventory"
|
||||||
|
text="Are you really want to delete this inventory?"
|
||||||
|
v-model="deleteItemDialog"
|
||||||
|
@yes="deleteItem(itemId)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Inventory</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="editItem('new')"
|
||||||
|
>New Inventory</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
:items-per-page="Number.MAX_VALUE"
|
||||||
|
>
|
||||||
|
<template v-slot:item.inventory="{ item }">
|
||||||
|
<div v-if="item.type === 'file'">{{ item.inventory }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<div style="white-space: nowrap">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
@click="askDeleteItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
@click="editItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
methods: {
|
||||||
|
getHeaders() {
|
||||||
|
return [{
|
||||||
|
text: 'Name',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Type',
|
||||||
|
value: 'type',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Path',
|
||||||
|
value: 'inventory',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Actions',
|
||||||
|
value: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/inventory`;
|
||||||
|
},
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/inventory/${this.itemId}`;
|
||||||
|
},
|
||||||
|
getEventName() {
|
||||||
|
return 'i-inventory';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
118
web2/src/views/project/Keys.vue
Normal file
118
web2/src/views/project/Keys.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="items != null">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
:save-button-text="itemId === 'new' ? 'Create' : 'Save'"
|
||||||
|
:title="`${itemId === 'new' ? 'New' : 'Edit'} Key`"
|
||||||
|
:max-width="450"
|
||||||
|
position="top"
|
||||||
|
@save="loadItems()"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<KeyForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<YesNoDialog
|
||||||
|
title="Delete key"
|
||||||
|
text="Are you really want to delete this key?"
|
||||||
|
v-model="deleteItemDialog"
|
||||||
|
@yes="deleteItem(itemId)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Key Store</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="editItem('new')"
|
||||||
|
>New Key</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
:items-per-page="Number.MAX_VALUE"
|
||||||
|
>
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<div style="white-space: nowrap">
|
||||||
|
<v-tooltip bottom>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
@click="askDeleteItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<span>Delete key</span>
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
|
<v-tooltip bottom>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
@click="editItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<span>Edit key</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
import KeyForm from '@/components/KeyForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { KeyForm },
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
methods: {
|
||||||
|
getHeaders() {
|
||||||
|
return [{
|
||||||
|
text: 'Name',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Type',
|
||||||
|
value: 'type',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Actions',
|
||||||
|
value: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/keys`;
|
||||||
|
},
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/keys/${this.itemId}`;
|
||||||
|
},
|
||||||
|
getEventName() {
|
||||||
|
return 'i-keys';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
52
web2/src/views/project/New.vue
Normal file
52
web2/src/views/project/New.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div>
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>New Project</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<div class="project-settings-form">
|
||||||
|
<div style="height: 220px;">
|
||||||
|
<ProjectForm item-id="new" ref="editForm"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<v-btn color="primary" @click="createProject()">Create</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
import ProjectForm from '@/components/ProjectForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ProjectForm },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
showDrawer() {
|
||||||
|
EventBus.$emit('i-show-drawer');
|
||||||
|
},
|
||||||
|
|
||||||
|
async createProject() {
|
||||||
|
const item = await this.$refs.editForm.save();
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EventBus.$emit('i-project', {
|
||||||
|
action: 'new',
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
125
web2/src/views/project/Repositories.vue
Normal file
125
web2/src/views/project/Repositories.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="items != null && keys != null">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
:save-button-text="itemId === 'new' ? 'Create' : 'Save'"
|
||||||
|
:title="`${itemId === 'new' ? 'New' : 'Edit'} Repository`"
|
||||||
|
@save="loadItems()"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<RepositoryForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<YesNoDialog
|
||||||
|
title="Delete repository"
|
||||||
|
text="Are you really want to delete this repository?"
|
||||||
|
v-model="deleteItemDialog"
|
||||||
|
@yes="deleteItem(itemId)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Playbook Repositories</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="editItem('new')"
|
||||||
|
>New Repository</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
:items-per-page="Number.MAX_VALUE"
|
||||||
|
>
|
||||||
|
<template v-slot:item.ssh_key_id="{ item }">
|
||||||
|
{{ keys.find((k) => k.id === item.ssh_key_id).name }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<div style="white-space: nowrap">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
@click="askDeleteItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
class="mr-1"
|
||||||
|
@click="editItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
import RepositoryForm from '@/components/RepositoryForm.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
components: { RepositoryForm },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keys: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
this.keys = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/keys`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getHeaders() {
|
||||||
|
return [{
|
||||||
|
text: 'Name',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Git URL',
|
||||||
|
value: 'git_url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'SSH Key',
|
||||||
|
value: 'ssh_key_id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Actions',
|
||||||
|
value: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/repositories`;
|
||||||
|
},
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/repositories/${this.itemId}`;
|
||||||
|
},
|
||||||
|
getEventName() {
|
||||||
|
return 'i-repositories';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
114
web2/src/views/project/Settings.vue
Normal file
114
web2/src/views/project/Settings.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div>
|
||||||
|
<YesNoDialog
|
||||||
|
v-model="deleteProjectDialog"
|
||||||
|
title="Delete project"
|
||||||
|
text="Are you really want to delete this project?"
|
||||||
|
@yes="deleteProject()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Dashboard</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<div>
|
||||||
|
<v-tabs centered>
|
||||||
|
<v-tab key="history" :to="`/project/${projectId}/history`">History</v-tab>
|
||||||
|
<v-tab key="activity" :to="`/project/${projectId}/activity`">Activity</v-tab>
|
||||||
|
<v-tab key="settings" :to="`/project/${projectId}/settings`">Settings</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
</div>
|
||||||
|
</v-toolbar>
|
||||||
|
<div class="project-settings-form">
|
||||||
|
<div style="height: 220px;">
|
||||||
|
<ProjectForm :item-id="projectId" ref="form"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<v-btn color="primary" @click="saveProject()">Save</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-delete-form">
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col class="shrink">
|
||||||
|
<v-btn color="error" @click="deleteProjectDialog = true">Delete Project</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col class="grow">
|
||||||
|
<div style="font-size: 14px; color: #ff5252">
|
||||||
|
Once you delete a project, there is no going back. Please be certain.
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
.project-settings-form {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 80px auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-delete-form {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 80px auto auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
import ProjectForm from '@/components/ProjectForm.vue';
|
||||||
|
import { getErrorMessage } from '@/lib/error';
|
||||||
|
import axios from 'axios';
|
||||||
|
import YesNoDialog from '@/components/YesNoDialog.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { YesNoDialog, ProjectForm },
|
||||||
|
props: {
|
||||||
|
projectId: Number,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
deleteProjectDialog: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
showDrawer() {
|
||||||
|
EventBus.$emit('i-show-drawer');
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveProject() {
|
||||||
|
const item = await this.$refs.form.save();
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EventBus.$emit('i-project', {
|
||||||
|
action: 'edit',
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteProject() {
|
||||||
|
try {
|
||||||
|
await axios({
|
||||||
|
method: 'delete',
|
||||||
|
url: `/api/project/${this.projectId}`,
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
EventBus.$emit('i-project', {
|
||||||
|
action: 'delete',
|
||||||
|
item: {
|
||||||
|
id: this.projectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
EventBus.$emit('i-snackbar', {
|
||||||
|
color: 'error',
|
||||||
|
text: getErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
140
web2/src/views/project/Team.vue
Normal file
140
web2/src/views/project/Team.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="items != null">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
:save-button-text="(this.itemId === 'new' ? 'Link' : 'Save')"
|
||||||
|
:title="(this.itemId === 'new' ? 'New' : 'Edit') + ' Team Member'"
|
||||||
|
@save="loadItems()"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<TeamMemberForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<YesNoDialog
|
||||||
|
title="Delete team member"
|
||||||
|
text="Are you really want to delete the team member?"
|
||||||
|
v-model="deleteItemDialog"
|
||||||
|
@yes="deleteItem(itemId)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Team</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="editItem('new')"
|
||||||
|
>New Team Member</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
:items-per-page="Number.MAX_VALUE"
|
||||||
|
>
|
||||||
|
<template v-slot:item.admin="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-if="item.admin"
|
||||||
|
@click="refuseAdmin(item.id)"
|
||||||
|
:disabled="!isUserAdmin()"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-checkbox-marked</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-else
|
||||||
|
@click="grantAdmin(item.id)"
|
||||||
|
:disabled="!isUserAdmin()"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-checkbox-blank-outline</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
:disabled="!isUserAdmin()"
|
||||||
|
@click="askDeleteItem(item.id)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
import TeamMemberForm from '@/components/TeamMemberForm.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { TeamMemberForm },
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
methods: {
|
||||||
|
async grantAdmin(userId) {
|
||||||
|
await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: `/api/project/${this.projectId}/users/${userId}/admin`,
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
await this.loadItems();
|
||||||
|
},
|
||||||
|
async refuseAdmin(userId) {
|
||||||
|
await axios({
|
||||||
|
method: 'delete',
|
||||||
|
url: `/api/project/${this.projectId}/users/${userId}/admin`,
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
await this.loadItems();
|
||||||
|
},
|
||||||
|
getHeaders() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Name',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Username',
|
||||||
|
value: 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Email',
|
||||||
|
value: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Admin',
|
||||||
|
value: 'admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Actions',
|
||||||
|
value: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
getSingleItemUrl() {
|
||||||
|
return `/api/project/${this.projectId}/users/${this.itemId}`;
|
||||||
|
},
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/users?sort=name&order=asc`;
|
||||||
|
},
|
||||||
|
getEventName() {
|
||||||
|
return 'i-repositories';
|
||||||
|
},
|
||||||
|
isUserAdmin() {
|
||||||
|
return (this.items.find((x) => x.id === this.userId) || {}).admin;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
273
web2/src/views/project/TemplateView.vue
Normal file
273
web2/src/views/project/TemplateView.vue
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="item != null && tasks != null">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
save-button-text="Save"
|
||||||
|
title="Edit Template"
|
||||||
|
@save="loadData()"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<TemplateForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<ItemDialog
|
||||||
|
v-model="copyDialog"
|
||||||
|
save-button-text="Create"
|
||||||
|
title="New Template"
|
||||||
|
@save="loadData()"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<TemplateForm
|
||||||
|
:project-id="projectId"
|
||||||
|
:item-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<YesNoDialog
|
||||||
|
title="Delete template"
|
||||||
|
text="Are you really want to delete this template?"
|
||||||
|
v-model="deleteDialog"
|
||||||
|
@yes="remove()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title class="breadcrumbs">
|
||||||
|
<router-link
|
||||||
|
class="breadcrumbs__item breadcrumbs__item--link"
|
||||||
|
:to="`/project/${projectId}/templates/`"
|
||||||
|
>Task Templates</router-link>
|
||||||
|
<span class="breadcrumbs__separator">></span>
|
||||||
|
<span class="breadcrumbs__item">{{ item.alias }}</span>
|
||||||
|
</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
color="error"
|
||||||
|
@click="deleteDialog = true"
|
||||||
|
>
|
||||||
|
<v-icon left>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
color="black"
|
||||||
|
@click="copyDialog = true"
|
||||||
|
>
|
||||||
|
<v-icon left>mdi-content-copy</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
color="black"
|
||||||
|
@click="editDialog = true"
|
||||||
|
>
|
||||||
|
<v-icon left>mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-container class="pa-0">
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-list two-line subheader>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Playbook</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.playbook }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>SSH Key</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.ssh_key_id }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-list two-line subheader>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Inventory</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.inventory_id }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Environment</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.environment_id }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Repository</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ item.repository_id }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<h4 class="ml-4 mt-4">Task History</h4>
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="tasks"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<template v-slot:item.id="{ item }">
|
||||||
|
<a @click="showTaskLog(item.id)">#{{ item.id }}</a>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.status="{ item }">
|
||||||
|
{{ item.status }}
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
import { getErrorMessage } from '@/lib/error';
|
||||||
|
import YesNoDialog from '@/components/YesNoDialog.vue';
|
||||||
|
import ItemDialog from '@/components/ItemDialog.vue';
|
||||||
|
import TemplateForm from '@/components/TemplateForm.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
YesNoDialog, ItemDialog, TemplateForm,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
projectId: Number,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
text: 'Task ID',
|
||||||
|
value: 'id',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Status',
|
||||||
|
value: 'status',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'User',
|
||||||
|
value: 'user_name',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Start',
|
||||||
|
value: 'start',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Duration',
|
||||||
|
value: 'start',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tasks: null,
|
||||||
|
item: null,
|
||||||
|
deleteDialog: null,
|
||||||
|
editDialog: null,
|
||||||
|
copyDialog: null,
|
||||||
|
taskLogDialog: null,
|
||||||
|
taskId: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
itemId() {
|
||||||
|
return this.$route.params.templateId;
|
||||||
|
},
|
||||||
|
isNew() {
|
||||||
|
return this.itemId === 'new';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
if (this.isNew) {
|
||||||
|
await this.$router.replace({
|
||||||
|
path: `/project/${this.projectId}/templates/new/edit`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.loadData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
showTaskLog(taskId) {
|
||||||
|
EventBus.$emit('i-show-task', {
|
||||||
|
taskId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showDrawer() {
|
||||||
|
EventBus.$emit('i-show-drawer');
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove() {
|
||||||
|
try {
|
||||||
|
await axios({
|
||||||
|
method: 'delete',
|
||||||
|
url: `/api/project/${this.projectId}/templates/${this.itemId}`,
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
EventBus.$emit('i-snackbar', {
|
||||||
|
color: 'success',
|
||||||
|
text: `Template "${this.item.alias}" deleted`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.$router.push({
|
||||||
|
path: `/project/${this.projectId}/templates`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
EventBus.$emit('i-snackbar', {
|
||||||
|
color: 'error',
|
||||||
|
text: getErrorMessage(err),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.deleteDialog = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
this.item = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/templates/${this.itemId}`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.tasks = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/templates/${this.itemId}/tasks/last`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
201
web2/src/views/project/Templates.vue
Normal file
201
web2/src/views/project/Templates.vue
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<div v-if="isLoaded">
|
||||||
|
<ItemDialog
|
||||||
|
v-model="editDialog"
|
||||||
|
save-button-text="Create"
|
||||||
|
title="New template"
|
||||||
|
@save="loadItems()"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<TemplateForm
|
||||||
|
:project-id="projectId"
|
||||||
|
item-id="new"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<ItemDialog
|
||||||
|
v-model="newTaskDialog"
|
||||||
|
save-button-text="Run"
|
||||||
|
title="New Task"
|
||||||
|
@save="onTaskCreate"
|
||||||
|
>
|
||||||
|
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||||
|
<TaskForm
|
||||||
|
:project-id="projectId"
|
||||||
|
item-id="new"
|
||||||
|
:template-id="itemId"
|
||||||
|
@save="onSave"
|
||||||
|
@error="onError"
|
||||||
|
:need-save="needSave"
|
||||||
|
:need-reset="needReset"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItemDialog>
|
||||||
|
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||||
|
<v-toolbar-title>Task Templates</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="editItem('new')"
|
||||||
|
>New template</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
hide-default-footer
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<template v-slot:item.alias="{ item }">
|
||||||
|
<router-link :to="`/project/${projectId}/templates/${item.id}`">
|
||||||
|
{{ item.alias }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.ssh_key_id="{ item }">
|
||||||
|
{{ keys.find((x) => x.id === item.ssh_key_id).name }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.inventory_id="{ item }">
|
||||||
|
{{ inventory.find((x) => x.id === item.inventory_id).name }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.environment_id="{ item }">
|
||||||
|
{{ environment.find((x) => x.id === item.environment_id).name }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.repository_id="{ item }">
|
||||||
|
{{ repositories.find((x) => x.id === item.repository_id).name }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-btn text color="black" class="pl-1 pr-2" @click="createTask(item.id)">
|
||||||
|
<v-icon class="pr-1">mdi-play</v-icon>
|
||||||
|
Run
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||||
|
import TemplateForm from '@/components/TemplateForm.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import TaskForm from '@/components/TaskForm.vue';
|
||||||
|
import EventBus from '@/event-bus';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { TemplateForm, TaskForm },
|
||||||
|
mixins: [ItemListPageBase],
|
||||||
|
async created() {
|
||||||
|
await this.loadData();
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keys: null,
|
||||||
|
inventory: null,
|
||||||
|
environment: null,
|
||||||
|
repositories: null,
|
||||||
|
newTaskDialog: null,
|
||||||
|
taskId: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoaded() {
|
||||||
|
return this.items && this.keys && this.inventory && this.environment && this.repositories;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTaskCreate(e) {
|
||||||
|
EventBus.$emit('i-show-task', {
|
||||||
|
taskId: e.item.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createTask(itemId) {
|
||||||
|
this.itemId = itemId;
|
||||||
|
this.newTaskDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
getHeaders() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Alias',
|
||||||
|
value: 'alias',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Playbook',
|
||||||
|
value: 'playbook',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'SSH key',
|
||||||
|
value: 'ssh_key_id',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Inventory',
|
||||||
|
value: 'inventory_id',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Environment',
|
||||||
|
value: 'environment_id',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Repository',
|
||||||
|
value: 'repository_id',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Actions',
|
||||||
|
value: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
getItemsUrl() {
|
||||||
|
return `/api/project/${this.projectId}/templates`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
this.inventory = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/inventory`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.environment = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/environment`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.keys = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/keys`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
this.repositories = (await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `/api/project/${this.projectId}/repositories`,
|
||||||
|
responseType: 'json',
|
||||||
|
})).data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
13
web2/tests/unit/example.spec.js
Normal file
13
web2/tests/unit/example.spec.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import HelloWorld from '@/components/HelloWorld.vue';
|
||||||
|
|
||||||
|
describe('HelloWorld.vue', () => {
|
||||||
|
it('renders props.msg when passed', () => {
|
||||||
|
const msg = 'new message';
|
||||||
|
const wrapper = shallowMount(HelloWorld, {
|
||||||
|
propsData: { msg },
|
||||||
|
});
|
||||||
|
expect(wrapper.text()).to.include(msg);
|
||||||
|
});
|
||||||
|
});
|
5
web2/vue.config.js
Normal file
5
web2/vue.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
transpileDependencies: [
|
||||||
|
'vuetify',
|
||||||
|
],
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user