Detail view (#47)

* Adding the ability to get lock data and show it using the detail view in the ui for boltdb

* adding modal style

* Adding new modal based discard ui

* Adding detail view and get lock funtionality with unlocking with the UI

* lots of clean up after review

* using jquery most places now

* missed in merge

* this should cause a build failure

* moving e2e test as part of the test override step so they run if unit tests fail

* fixing boltdb tests

* turns out you can't fail fast in circleci

* don't compare locks
This commit is contained in:
Anubhav Mishra
2017-06-25 15:24:07 -07:00
committed by GitHub
parent 57e18399d7
commit 7f8f85a6a7
14 changed files with 457 additions and 160 deletions

View File

@@ -1,6 +1,6 @@
BUILD_ID := $(shell git rev-parse --short HEAD 2>/dev/null || echo no-commit-id)
WORKSPACE := $(shell pwd)
PKG := $(shell go list ./... | grep -v e2e | grep -v vendor)
PKG := $(shell go list ./... | grep -v e2e | grep -v vendor | grep -v static)
.PHONY: test

View File

@@ -21,9 +21,9 @@ test:
override:
# Run tests
- cd "${WORKDIR}" && ./scripts/build.sh
# Run e2e tests
post:
# Run e2e tests
- cd "${WORKDIR}" && ./scripts/e2e-deps.sh
# Start atlantis server
- cd "${WORKDIR}/e2e" && ./atlantis server --gh-user="$GITHUB_USERNAME" --gh-password="$GITHUB_PASSWORD" --data-dir="/tmp" --require-approval=false --plan-backend="file" --log-level="debug" &> /tmp/atlantis-server.log:

View File

@@ -4,12 +4,13 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/boltdb/bolt"
"github.com/hootsuite/atlantis/models"
"github.com/pkg/errors"
"os"
"path"
"time"
"github.com/boltdb/bolt"
"github.com/hootsuite/atlantis/models"
"github.com/pkg/errors"
)
type Backend struct {
@@ -139,7 +140,7 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec
for k, v := c.Seek([]byte(repoFullName)); k != nil && bytes.HasPrefix(k, []byte(repoFullName)); k, v = c.Next() {
var lock models.ProjectLock
if err := json.Unmarshal(v, &lock); err != nil {
return errors.Wrapf(err, "failed to deserialize lock at key %q", string(k))
return errors.Wrapf(err, "deserializing lock at key %q", string(k))
}
if lock.Pull.Num == pullNum {
locks = append(locks, lock)
@@ -157,6 +158,26 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec
return locks, nil
}
func (b Backend) GetLock(p models.Project, env string) (models.ProjectLock, error) {
key := b.key(p, env)
var lockBytes []byte
err := b.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(b.bucket)
lockBytes = b.Get([]byte(key))
return nil
})
if err != nil {
return models.ProjectLock{}, errors.Wrap(err, "getting lock data")
}
var lock models.ProjectLock
if err := json.Unmarshal(lockBytes, &lock); err != nil {
return models.ProjectLock{}, errors.Wrapf(err, "deserializing lock at key %q", key)
}
return lock, nil
}
func (b Backend) key(p models.Project, env string) string {
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, env)
}

View File

@@ -3,15 +3,17 @@ package boltdb_test
import (
. "github.com/hootsuite/atlantis/testing_util"
"github.com/boltdb/bolt"
"github.com/pkg/errors"
"io/ioutil"
"os"
"testing"
"github.com/boltdb/bolt"
"github.com/pkg/errors"
"time"
"github.com/hootsuite/atlantis/locking/boltdb"
"github.com/hootsuite/atlantis/models"
"time"
)
var lockBucket = "bucket"
@@ -25,9 +27,9 @@ var lock = models.ProjectLock{
User: models.User{
Username: "lkysow",
},
Env: env,
Env: env,
Project: project,
Time: time.Now(),
Time: time.Now(),
}
func TestListNoLocks(t *testing.T) {
@@ -158,8 +160,9 @@ func TestUnlockingNoLocks(t *testing.T) {
t.Log("unlocking with no locks should succeed")
db, b := newTestDB()
defer cleanupDB(db)
_, err := b.Unlock(project, env)
Ok(t, b.Unlock(project, env))
Ok(t, err)
}
func TestUnlocking(t *testing.T) {
@@ -168,7 +171,8 @@ func TestUnlocking(t *testing.T) {
defer cleanupDB(db)
b.TryLock(lock)
Ok(t, b.Unlock(project, env))
_, err := b.Unlock(project, env)
Ok(t, err)
// should be no locks listed
ls, err := b.List()
@@ -204,10 +208,14 @@ func TestUnlockingMultiple(t *testing.T) {
b.TryLock(new3)
// now try and unlock them
Ok(t, b.Unlock(new3.Project, new3.Env))
Ok(t, b.Unlock(new2.Project, env))
Ok(t, b.Unlock(new.Project, env))
Ok(t, b.Unlock(project, env))
_, err := b.Unlock(new3.Project, new3.Env)
Ok(t, err)
_, err = b.Unlock(new2.Project, env)
Ok(t, err)
_, err = b.Unlock(new.Project, env)
Ok(t, err)
_, err = b.Unlock(project, env)
Ok(t, err)
// should be none left
ls, err := b.List()
@@ -220,7 +228,7 @@ func TestUnlockByPullNone(t *testing.T) {
db, b := newTestDB()
defer cleanupDB(db)
err := b.UnlockByPull("any/repo", 1)
_, err := b.UnlockByPull("any/repo", 1)
Ok(t, err)
}
@@ -233,7 +241,7 @@ func TestUnlockByPullOne(t *testing.T) {
t.Log("...delete nothing when its the same repo but a different pull")
{
err := b.UnlockByPull(project.RepoFullName, pullNum+1)
_, err := b.UnlockByPull(project.RepoFullName, pullNum+1)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
@@ -241,7 +249,7 @@ func TestUnlockByPullOne(t *testing.T) {
}
t.Log("...delete nothing when its the same pull but a different repo")
{
err := b.UnlockByPull("different/repo", pullNum)
_, err := b.UnlockByPull("different/repo", pullNum)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
@@ -249,7 +257,7 @@ func TestUnlockByPullOne(t *testing.T) {
}
t.Log("...delete the lock when its the same repo and pull")
{
err := b.UnlockByPull(project.RepoFullName, pullNum)
_, err := b.UnlockByPull(project.RepoFullName, pullNum)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
@@ -263,9 +271,10 @@ func TestUnlockByPullAfterUnlock(t *testing.T) {
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
Ok(t, b.Unlock(project, env))
_, err = b.Unlock(project, env)
Ok(t, err)
err = b.UnlockByPull(project.RepoFullName, pullNum)
_, err = b.UnlockByPull(project.RepoFullName, pullNum)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
@@ -295,13 +304,25 @@ func TestUnlockByPullMatching(t *testing.T) {
Equals(t, 3, len(ls))
// should all be unlocked
err = b.UnlockByPull(project.RepoFullName, pullNum)
_, err = b.UnlockByPull(project.RepoFullName, pullNum)
Ok(t, err)
ls, err = b.List()
Ok(t, err)
Equals(t, 0, len(ls))
}
// todo: Write more tests for getting lock data
func TestGetLock(t *testing.T) {
t.Log("get data for a existing lock")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
_, err = b.GetLock(project, env)
Ok(t, err)
}
// newTestDB returns a TestDB using a temporary path.
func newTestDB() (*bolt.DB, *boltdb.Backend) {
// Retrieve a temporary path.

View File

@@ -2,6 +2,9 @@ package dynamodb
import (
"fmt"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/service/dynamodb"
@@ -9,8 +12,6 @@ import (
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
"github.com/hootsuite/atlantis/models"
"github.com/pkg/errors"
"strconv"
"time"
)
type Backend struct {
@@ -23,18 +24,18 @@ type Backend struct {
// and also so any changes to models won't affect
// how we're storing our data (or will at least cause a compile error)
type dynamoLock struct {
LockKey string
RepoFullName string
Path string
LockKey string
RepoFullName string
Path string
PullNum int
PullHeadCommit string
PullBaseCommit string
PullURL string
PullBranch string
PullAuthor string
UserUsername string
Env string
Time time.Time
UserUsername string
Env string
Time time.Time
}
func New(lockTable string, p client.ConfigProvider) Backend {
@@ -142,6 +143,30 @@ func (b Backend) List() ([]models.ProjectLock, error) {
return locks, errors.Wrap(err, "scanning dynamodb")
}
func (b Backend) GetLock(p models.Project, env string) (models.ProjectLock, error) {
key := b.key(p, env)
params := &dynamodb.GetItemInput{
Key: map[string]*dynamodb.AttributeValue{
"LockKey": {
S: aws.String(key),
},
},
TableName: aws.String(b.LockTable),
ConsistentRead: aws.Bool(true),
}
item, err := b.DB.GetItem(params)
if err != nil {
return models.ProjectLock{}, errors.Wrapf(err, "getting item %q", item)
}
var dynamoDBLock dynamoLock
if err := dynamodbattribute.UnmarshalMap(item.Item, &dynamoDBLock); err != nil {
return models.ProjectLock{}, errors.Wrap(err, "found a lock at that key but it could not be deserialized. We suggest manually deleting this key from DynamoDB")
}
return b.fromDynamo(dynamoDBLock), nil
}
func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {
params := &dynamodb.ScanInput{
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
@@ -186,39 +211,39 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec
func (b Backend) toDynamo(key string, l models.ProjectLock) dynamoLock {
return dynamoLock{
LockKey: key,
RepoFullName: l.Project.RepoFullName,
Path: l.Project.Path,
PullNum: l.Pull.Num,
LockKey: key,
RepoFullName: l.Project.RepoFullName,
Path: l.Project.Path,
PullNum: l.Pull.Num,
PullHeadCommit: l.Pull.HeadCommit,
PullBaseCommit: l.Pull.BaseCommit,
PullURL: l.Pull.URL,
PullBranch: l.Pull.Branch,
PullAuthor: l.Pull.Author,
UserUsername: l.User.Username,
Env: l.Env,
Time: time.Now(),
PullURL: l.Pull.URL,
PullBranch: l.Pull.Branch,
PullAuthor: l.Pull.Author,
UserUsername: l.User.Username,
Env: l.Env,
Time: time.Now(),
}
}
func (b Backend) fromDynamo(d dynamoLock) models.ProjectLock {
return models.ProjectLock{
Pull: models.PullRequest{
Author: d.PullAuthor,
Branch: d.PullBranch,
URL: d.PullURL,
Author: d.PullAuthor,
Branch: d.PullBranch,
URL: d.PullURL,
BaseCommit: d.PullBaseCommit,
HeadCommit: d.PullHeadCommit,
Num: d.PullNum,
Num: d.PullNum,
},
User: models.User{
Username: d.UserUsername,
},
Project: models.Project{
RepoFullName: d.RepoFullName,
Path: d.Path,
Path: d.Path,
},
Time: d.Time,
Env: d.Env,
Env: d.Env,
}
}

View File

@@ -3,22 +3,24 @@ package locking
import (
"errors"
"fmt"
"github.com/hootsuite/atlantis/models"
"regexp"
"time"
"github.com/hootsuite/atlantis/models"
)
type Backend interface {
TryLock(lock models.ProjectLock) (bool, models.ProjectLock, error)
Unlock(project models.Project, env string) (*models.ProjectLock, error)
List() ([]models.ProjectLock, error)
GetLock(project models.Project, env string) (models.ProjectLock, error)
UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error)
}
type TryLockResponse struct {
LockAcquired bool
CurrLock models.ProjectLock
LockKey string
LockAcquired bool
CurrLock models.ProjectLock
LockKey string
}
type Client struct {
@@ -36,11 +38,11 @@ var keyRegex = regexp.MustCompile(`^(.*?\/.*?)\/(.*)\/(.*)$`)
func (c *Client) TryLock(p models.Project, env string, pull models.PullRequest, user models.User) (TryLockResponse, error) {
lock := models.ProjectLock{
Env: env,
Time: time.Now(),
Env: env,
Time: time.Now(),
Project: p,
User: user,
Pull: pull,
User: user,
Pull: pull,
}
lockAcquired, currLock, err := c.backend.TryLock(lock)
if err != nil {
@@ -50,11 +52,12 @@ func (c *Client) TryLock(p models.Project, env string, pull models.PullRequest,
}
func (c *Client) Unlock(key string) (*models.ProjectLock, error) {
matches := keyRegex.FindStringSubmatch(key)
if len(matches) != 4 {
return nil, errors.New("invalid key format")
project, env, err := c.lockKeyToProjectEnv(key)
if err != nil {
return nil, err
}
return c.backend.Unlock(models.Project{matches[1], matches[2]}, matches[3])
return c.backend.Unlock(project, env)
}
func (c *Client) List() (map[string]models.ProjectLock, error) {
@@ -73,6 +76,29 @@ func (c *Client) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec
return c.backend.UnlockByPull(repoFullName, pullNum)
}
func (c *Client) GetLock(key string) (models.ProjectLock, error) {
project, env, err := c.lockKeyToProjectEnv(key)
if err != nil {
return models.ProjectLock{}, err
}
projectLock, err := c.backend.GetLock(project, env)
if err != nil {
return models.ProjectLock{}, err
}
return projectLock, nil
}
func (c *Client) key(p models.Project, env string) string {
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, env)
}
func (c *Client) lockKeyToProjectEnv(key string) (models.Project, string, error) {
matches := keyRegex.FindStringSubmatch(key)
if len(matches) != 4 {
return models.Project{}, "", errors.New("invalid key format")
}
return models.Project{matches[1], matches[2]}, matches[3], nil
}

View File

@@ -1,5 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
run_unit_test() {
echo "Running unit tests: 'make test'"
make test

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@ package server
import (
"fmt"
"github.com/hootsuite/atlantis/logging"
"github.com/hootsuite/atlantis/recovery"
)
@@ -73,8 +74,8 @@ func (s *CommandHandler) ExecuteCommand(ctx *CommandContext) {
}
}
func (s *CommandHandler) SetDeleteLockURL(f func(id string) (url string)) {
s.planExecutor.DeleteLockURL = f
func (s *CommandHandler) SetLockURL(f func(id string) (url string)) {
s.planExecutor.LockURL = f
}
// recover logs and creates a comment on the pull request for panics

View File

@@ -2,17 +2,18 @@ package server
import (
"fmt"
"github.com/hootsuite/atlantis/locking"
"github.com/hootsuite/atlantis/logging"
"github.com/hootsuite/atlantis/models"
"github.com/hootsuite/atlantis/plan"
"github.com/pkg/errors"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/hootsuite/atlantis/locking"
"github.com/hootsuite/atlantis/logging"
"github.com/hootsuite/atlantis/models"
"github.com/hootsuite/atlantis/plan"
"github.com/pkg/errors"
)
// PlanExecutor handles everything related to running the Terraform plan including integration with S3, Terraform, and GitHub
@@ -26,9 +27,9 @@ type PlanExecutor struct {
terraform *TerraformClient
githubCommentRenderer *GithubCommentRenderer
lockingClient *locking.Client
// DeleteLockURL is a function that given a lock id will return a url for deleting the lock
DeleteLockURL func(id string) (url string)
planBackend plan.Backend
// LockURL is a function that given a lock id will return a url for detail view
LockURL func(id string) (url string)
planBackend plan.Backend
}
/** Result Types **/
@@ -314,7 +315,7 @@ func (p *PlanExecutor) plan(
Status: Success,
Result: PlanSuccess{
TerraformOutput: output,
LockURL: p.DeleteLockURL(lockAttempt.LockKey),
LockURL: p.LockURL(lockAttempt.LockKey),
},
}
}

View File

@@ -11,6 +11,8 @@ import (
"time"
"io/ioutil"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/elazarl/go-bindata-assetfs"
"github.com/google/go-github/github"
@@ -27,7 +29,6 @@ import (
"github.com/pkg/errors"
"github.com/urfave/cli"
"github.com/urfave/negroni"
"io/ioutil"
)
const (
@@ -220,17 +221,12 @@ func (s *Server) Start() error {
s.router.PathPrefix("/static/").Handler(http.FileServer(&assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo}))
s.router.HandleFunc("/hooks", s.postHooks).Methods("POST")
s.router.HandleFunc("/locks", s.deleteLock).Methods("DELETE").Queries("id", "{id:.*}")
// todo: remove this route when there is a detail view
// right now we need this route because from the pull request comment in GitHub only a GET request can be made
// in the future, the pull discard link will link to the detail view which will have a Delete button which will
// make an real DELETE call but we don't have a detail view right now
deleteLockRoute := s.router.HandleFunc("/locks", s.deleteLock).Queries("id", "{id}", "method", "DELETE").Methods("GET").Name(deleteLockRoute)
// function that planExecutor can use to construct delete lock urls
lockRoute := s.router.HandleFunc("/lock", s.lock).Methods("GET").Queries("id", "{id}")
// function that planExecutor can use to construct detail view url
// injecting this here because this is the earliest routes are created
s.commandHandler.SetDeleteLockURL(func(lockID string) string {
s.commandHandler.SetLockURL(func(lockID string) string {
// ignoring error since guaranteed to succeed if "id" is specified
u, _ := deleteLockRoute.URL("id", url.QueryEscape(lockID))
u, _ := lockRoute.URL("id", url.QueryEscape(lockID))
return s.atlantisURL + u.RequestURI()
})
n := negroni.New(&negroni.Recovery{
@@ -253,16 +249,16 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
}
type lock struct {
UnlockURL string
LockURL string
RepoFullName string
PullNum int
Time time.Time
}
var results []lock
for id, v := range locks {
u, _ := s.router.Get(deleteLockRoute).URL("id", url.QueryEscape(id))
results = append(results, lock{
UnlockURL: u.String(),
// todo: make LockURL use the router to get /lock endpoint
LockURL: fmt.Sprintf("/lock?id=%s", url.QueryEscape(id)),
RepoFullName: v.Project.RepoFullName,
PullNum: v.Pull.Num,
Time: v.Time,
@@ -271,6 +267,54 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
indexTemplate.Execute(w, results)
}
func (s *Server) lock(w http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "no lock id in request")
}
// get details for lock id
idUnencoded, err := url.QueryUnescape(id)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "invalid lock id")
}
// for the given lock key get lock data
lock, err := s.lockingClient.GetLock(idUnencoded)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
}
type lockData struct {
UnlockURL string
LockKeyEncoded string
LockKey string
RepoOwner string
RepoName string
PullRequestLink string
LockedBy string
Environment string
Time time.Time
}
// extract the repo owner and repo name
repo := strings.Split(lock.Project.RepoFullName, "/")
l := lockData{
LockKeyEncoded: id,
LockKey: idUnencoded,
RepoOwner: repo[0],
RepoName: repo[1],
PullRequestLink: lock.Pull.URL,
LockedBy: lock.Pull.Author,
Environment: lock.Env,
}
lockTemplate.Execute(w, l)
}
func (s *Server) deleteLock(w http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {

View File

@@ -13,6 +13,15 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="/static/js/jquery-3.2.1.min.js"></script>
<script>
$(document).ready(function () {
$("p.js-discard-success").toggle(document.URL.indexOf("discard=true") !== -1);
});
setTimeout(function() {
$("p.js-discard-success").fadeOut('slow');
}, 5000); // <-- time in milliseconds
</script>
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/custom.css">
@@ -22,7 +31,8 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
<div class="container">
<section class="header">
<a title="atlantis" href="/"><img src="/static/images/atlantis-icon.png"/></a>
<p style="font-family: monospace, monospace; font-size: 1.1em; text-align: center;">atlantis</p>
<p class="title-heading">atlantis</p>
<p class="js-discard-success"><strong>Plan discarded and unlocked!</strong></p>
</section>
<nav class="navbar">
<div class="container">
@@ -31,15 +41,16 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
<div class="navbar-spacer"></div>
<br>
<section>
<p style="font-family: monospace, monospace; font-size: 1.0em; text-align: center;"><strong>Environments</strong></p>
<p class="title-heading small"><strong>Environments</strong></p>
{{ if . }}
{{ range . }}
<div class="twelve columns button content lock-row">
<a href="{{.LockURL}}">
<div class="twelve columns button content lock-row">
<div class="list-title">{{.RepoFullName}} - <span class="heading-font-size">#{{.PullNum}}</span></div>
<div class="list-unlock"><button class="unlock"><a class="unlock-link" href="{{.UnlockURL}}">Unlock</a></button></div>
<div class="list-status"><code>Locked</code></div>
<div class="list-timestamp"><span class="heading-font-size">{{.Time}}</span></div>
</div>
</div>
</a>
{{ end }}
{{ else }}
<p class="placeholder">No environments found.</p>
@@ -49,3 +60,102 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
</body>
</html>
`))
var lockTemplate = template.Must(template.New("lock.html.tmpl").Parse(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>atlantis</title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/custom.css">
<link rel="icon" type="image/png" href="/static/images/atlantis-icon.png">
<script src="/static/js/jquery-3.2.1.min.js"></script>
</head>
<body>
<div class="container">
<section class="header">
<a title="atlantis" href="/"><img src="/static/images/atlantis-icon.png"/></a>
<p class="title-heading">atlantis</p>
<p class="title-heading"><strong>{{.LockKey}}</strong> <code>Locked</code></p>
</section>
<div class="navbar-spacer"></div>
<br>
<section>
<div class="eight columns">
<h6><code>Repo Owner</code>: <strong>{{.RepoOwner}}</strong></h6>
<h6><code>Repo Name</code>: <strong>{{.RepoName}}</strong></h6>
<h6><code>Pull Request Link</code>: <a href="{{.PullRequestLink}}" target="_blank"><strong>{{.PullRequestLink}}</strong></a></h6>
<h6><code>Locked By</code>: <strong>{{.LockedBy}}</strong></h6>
<h6><code>Environment</code>: <strong>{{.Environment}}</strong></h6>
<br>
</div>
<div class="four columns">
<a class="button button-default" id="discardPlanUnlock">Discard Plan & Unlock</a>
</div>
</section>
</div>
<div id="discardMessageModal" class="modal">
<!-- Modal content -->
<div class="modal-content">
<div class="modal-header">
<span class="close">&times;</span>
</div>
<div class="modal-body">
<p><strong>Are you sure you want to discard the plan and unlock?</strong></p>
<input class="button-primary" id="discardYes" type="submit" value="Yes" data="{{.LockKeyEncoded}}">
<input type="button" class="cancel" value="Cancel">
</div>
</div>
</div>
<script>
// Get the modal
var modal = $("#discardMessageModal");
// Get the button that opens the modal
var btn = $("#discardPlanUnlock");
var btnDiscard = $("#discardYes");
var lockId = btnDiscard.attr('data');
// Get the <span> element that closes the modal
// using document.getElementsByClassName since jquery $("close") doesn't seem to work for btn click events
var span = document.getElementsByClassName("close")[0];
var cancelBtn = document.getElementsByClassName("cancel")[0];
// When the user clicks the button, open the modal
btn.click(function() {
modal.css("display", "block");
});
// When the user clicks on <span> (x), close the modal
span.onclick = function() {
modal.css("display", "none");
}
cancelBtn.onclick = function() {
modal.css("display", "none");
}
btnDiscard.click(function() {
$.ajax({
url: '/locks?id='+lockId,
type: 'DELETE',
success: function(result) {
window.location.replace("/?discard=true");
}
});
});
// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
if (event.target == modal) {
modal.css("display", "none");
}
}
</script>
</body>
</html>
`))

View File

@@ -246,7 +246,66 @@ tbody {
.lock-row{
cursor: default;
}
.unlock-link{
color: #555;
text-decoration: none;
/* The Modal (background) */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Header */
.modal-header {
padding: 8px 12px;
background-color: #222222;
color: white;
}
/* Modal Body */
.modal-body {padding: 18px 16px;}
/* Modal Content */
.modal-content {
position: relative;
background-color: #fefefe;
margin: auto;
padding: 0;
border: 1px solid #888;
width: 50%;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;
animation-duration: 0.4s
}
/* Add Animation */
@-webkit-keyframes animatetop {
from {top: -300px; opacity: 0}
to {top: 0; opacity: 1}
}
@keyframes animatetop {
from {top: -300px; opacity: 0}
to {top: 0; opacity: 1}
}
.js-discard-success {
font-family: monospace, monospace; font-size: 1.1em; text-align: center;
}
.title-heading {
font-family: monospace, monospace; font-size: 1.1em; text-align: center;
}
.small {
font-size: 1.0em;
}

4
static/js/jquery-3.2.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long