mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-20 23:39:56 +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
|
||||
run:
|
||||
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_cache:
|
||||
|
@ -95,6 +95,7 @@ func main() {
|
||||
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} > 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/html/**/*.*
|
||||
web/public/fonts/*.*
|
||||
web2/dist
|
||||
config.json
|
||||
.DS_Store
|
||||
node_modules/
|
||||
|
31
Taskfile.yml
31
Taskfile.yml
@ -26,6 +26,7 @@ tasks:
|
||||
- task: deps:tools
|
||||
- task: deps:be
|
||||
- task: deps:fe
|
||||
- task: deps:fe2
|
||||
|
||||
deps:be:
|
||||
desc: Vendor application dependencies
|
||||
@ -39,6 +40,13 @@ tasks:
|
||||
- npm install
|
||||
- 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:
|
||||
desc: Installs requirements for integration testing with dredd
|
||||
dir: web
|
||||
@ -68,6 +76,7 @@ tasks:
|
||||
desc: Generates compiled frontend and backend resources (must be in this order)
|
||||
cmds:
|
||||
- task: compile:fe
|
||||
- task: compile:fe2
|
||||
- task: compile:be
|
||||
|
||||
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" }} xcopy node_modules\\font-awesome\\fonts public\\fonts /y {{ else }} cp node_modules/font-awesome/fonts/* public/fonts {{ end }}'
|
||||
- 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:
|
||||
desc: Runs Packr for static assets
|
||||
sources:
|
||||
- web2/dist/*
|
||||
- web/public/*
|
||||
- db/migrations/*
|
||||
generates:
|
||||
|
@ -1175,6 +1175,15 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
- $ref: "#/parameters/template_id"
|
||||
get:
|
||||
tags:
|
||||
- project
|
||||
summary: Get template
|
||||
responses:
|
||||
200:
|
||||
description: template object
|
||||
schema:
|
||||
$ref: "#/definitions/Template"
|
||||
put:
|
||||
tags:
|
||||
- project
|
||||
|
@ -45,6 +45,11 @@ func EnvironmentMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
// GetEnvironment retrieves sorted environments from the database
|
||||
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)
|
||||
var env []db.Environment
|
||||
|
||||
|
@ -52,7 +52,13 @@ func InventoryMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
// GetInventory returns an inventory from the database
|
||||
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)
|
||||
|
||||
var inv []db.Inventory
|
||||
|
||||
sort := r.URL.Query().Get("sort")
|
||||
|
@ -37,6 +37,11 @@ func KeyMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
// GetKeys retrieves sorted keys from the database
|
||||
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)
|
||||
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
|
||||
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)
|
||||
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
|
||||
func GetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
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)
|
||||
var users []db.User
|
||||
|
||||
|
@ -16,7 +16,15 @@ import (
|
||||
"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
|
||||
func JSONMiddleware(next http.Handler) http.Handler {
|
||||
@ -50,6 +58,8 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Route declares all routes
|
||||
func Route() *mux.Router {
|
||||
publicAssets = packr.NewBox(getPublicAssetsPath())
|
||||
|
||||
r := mux.NewRouter().StrictSlash(true)
|
||||
r.NotFoundHandler = http.HandlerFunc(servePublic)
|
||||
|
||||
@ -140,6 +150,7 @@ func Route() *mux.Router {
|
||||
projectUserManagement := projectAdminAPI.PathPrefix("/users").Subrouter()
|
||||
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("DELETE")
|
||||
projectUserManagement.HandleFunc("/{user_id}", projects.RemoveUser).Methods("DELETE")
|
||||
@ -147,24 +158,28 @@ func Route() *mux.Router {
|
||||
projectKeyManagement := projectAdminAPI.PathPrefix("/keys").Subrouter()
|
||||
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.RemoveKey).Methods("DELETE")
|
||||
|
||||
projectRepoManagement := projectUserAPI.PathPrefix("/repositories").Subrouter()
|
||||
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.RemoveRepository).Methods("DELETE")
|
||||
|
||||
projectInventoryManagement := projectUserAPI.PathPrefix("/inventory").Subrouter()
|
||||
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.RemoveInventory).Methods("DELETE")
|
||||
|
||||
projectEnvManagement := projectUserAPI.PathPrefix("/environment").Subrouter()
|
||||
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.RemoveEnvironment).Methods("DELETE")
|
||||
|
||||
@ -173,6 +188,9 @@ func Route() *mux.Router {
|
||||
|
||||
projectTmplManagement.HandleFunc("/{template_id}", projects.UpdateTemplate).Methods("PUT")
|
||||
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.Use(tasks.GetTaskMiddleware)
|
||||
@ -223,21 +241,33 @@ func debugPrintRoutes(r *mux.Router) {
|
||||
func servePublic(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
htmlPrefix := ""
|
||||
if util.Config.OldFrontend {
|
||||
htmlPrefix = "/html"
|
||||
}
|
||||
|
||||
publicAssetsPrefix := ""
|
||||
if util.Config.OldFrontend {
|
||||
publicAssetsPrefix = "public"
|
||||
}
|
||||
|
||||
webPath := "/"
|
||||
if util.WebHostURL != nil {
|
||||
webPath = util.WebHostURL.RequestURI()
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, webPath+"public") {
|
||||
if publicAssetsPrefix != "" && !strings.HasPrefix(path, webPath+publicAssetsPrefix) {
|
||||
if len(strings.Split(path, ".")) > 1 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
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, ".")
|
||||
suffix := split[len(split)-1]
|
||||
|
||||
@ -248,7 +278,7 @@ func servePublic(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 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),
|
||||
"<base href=\"/\">",
|
||||
"<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)
|
||||
|
||||
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").
|
||||
LeftJoin("user on task.user_id=user.id").
|
||||
Where("tpl.project_id=?", project.ID).
|
||||
OrderBy("task.created desc")
|
||||
LeftJoin("user on task.user_id=user.id");
|
||||
|
||||
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 {
|
||||
q = q.Limit(limit)
|
||||
|
@ -101,6 +101,8 @@ type ConfigType struct {
|
||||
TelegramAlert bool `json:"telegram_alert"`
|
||||
LdapEnable bool `json:"ldap_enable"`
|
||||
LdapNeedTLS bool `json:"ldap_needtls"`
|
||||
|
||||
OldFrontend bool `json:"old_frontend"`
|
||||
}
|
||||
|
||||
//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