Add error to GetSession return values
- Add button to copy instance SSH access
This commit is contained in:
8
Gopkg.lock
generated
8
Gopkg.lock
generated
@@ -353,6 +353,12 @@
|
|||||||
revision = "fde5e16d32adc7ad637e9cd9ad21d4ebc6192535"
|
revision = "fde5e16d32adc7ad637e9cd9ad21d4ebc6192535"
|
||||||
version = "v0.2.0"
|
version = "v0.2.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/zbindenren/negroni-prometheus"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "b4860d1377680423b4c7f957ef4fce828deacf65"
|
||||||
|
version = "v0.1.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
@@ -434,6 +440,6 @@
|
|||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "e9fd1884fe1cc9d2801810e3934b9caa124434efda447ee2f93435737b2e735f"
|
inputs-digest = "2468168ab5c826aef27405ea9a3e72c6a99fbea6639099f2600470330509f8d9"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|||||||
@@ -84,3 +84,7 @@
|
|||||||
[[constraint]]
|
[[constraint]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/satori/go.uuid"
|
name = "github.com/satori/go.uuid"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/zbindenren/negroni-prometheus"
|
||||||
|
version = "0.1.1"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/play-with-docker/play-with-docker/config"
|
"github.com/play-with-docker/play-with-docker/config"
|
||||||
"github.com/play-with-docker/play-with-docker/event"
|
"github.com/play-with-docker/play-with-docker/event"
|
||||||
"github.com/play-with-docker/play-with-docker/pwd"
|
"github.com/play-with-docker/play-with-docker/pwd"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
@@ -27,8 +28,19 @@ var core pwd.PWDApi
|
|||||||
var e event.EventApi
|
var e event.EventApi
|
||||||
var landings = map[string][]byte{}
|
var landings = map[string][]byte{}
|
||||||
|
|
||||||
|
var latencyHistogramVec = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Name: "pwd_handlers_duration_ms",
|
||||||
|
Help: "How long it took to process a specific handler, in a specific host",
|
||||||
|
Buckets: []float64{300, 1200, 5000},
|
||||||
|
}, []string{"action"})
|
||||||
|
|
||||||
type HandlerExtender func(h *mux.Router)
|
type HandlerExtender func(h *mux.Router)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
prometheus.MustRegister(latencyHistogramVec)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func Bootstrap(c pwd.PWDApi, ev event.EventApi) {
|
func Bootstrap(c pwd.PWDApi, ev event.EventApi) {
|
||||||
core = c
|
core = c
|
||||||
e = ev
|
e = ev
|
||||||
@@ -53,6 +65,10 @@ func Register(extend HandlerExtender) {
|
|||||||
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", DeleteInstance).Methods("DELETE")
|
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", DeleteInstance).Methods("DELETE")
|
||||||
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/exec", Exec).Methods("POST")
|
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/exec", Exec).Methods("POST")
|
||||||
|
|
||||||
|
r.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/editor.html", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(rw, r, "www/editor.html")
|
||||||
|
})
|
||||||
|
|
||||||
r.HandleFunc("/ooc", func(rw http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/ooc", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(rw, r, "./www/ooc.html")
|
http.ServeFile(rw, r, "./www/ooc.html")
|
||||||
}).Methods("GET")
|
}).Methods("GET")
|
||||||
@@ -64,9 +80,6 @@ func Register(extend HandlerExtender) {
|
|||||||
r.HandleFunc("/robots.txt", func(rw http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/robots.txt", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(rw, r, "www/robots.txt")
|
http.ServeFile(rw, r, "www/robots.txt")
|
||||||
})
|
})
|
||||||
r.HandleFunc("/sdk.js", func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(rw, r, "www/sdk.js")
|
|
||||||
})
|
|
||||||
|
|
||||||
corsRouter.HandleFunc("/sessions/{sessionId}/ws/", WSH)
|
corsRouter.HandleFunc("/sessions/{sessionId}/ws/", WSH)
|
||||||
r.Handle("/metrics", promhttp.Handler())
|
r.Handle("/metrics", promhttp.Handler())
|
||||||
@@ -90,6 +103,7 @@ func Register(extend HandlerExtender) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
n := negroni.Classic()
|
n := negroni.Classic()
|
||||||
|
|
||||||
r.PathPrefix("/").Handler(negroni.New(negroni.Wrap(corsHandler(corsRouter))))
|
r.PathPrefix("/").Handler(negroni.New(negroni.Wrap(corsHandler(corsRouter))))
|
||||||
n.UseHandler(r)
|
n.UseHandler(r)
|
||||||
|
|
||||||
@@ -128,7 +142,11 @@ func Register(extend HandlerExtender) {
|
|||||||
rr.HandleFunc("/ping", Ping).Methods("GET")
|
rr.HandleFunc("/ping", Ping).Methods("GET")
|
||||||
rr.Handle("/metrics", promhttp.Handler())
|
rr.Handle("/metrics", promhttp.Handler())
|
||||||
rr.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
rr.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(rw, r, fmt.Sprintf("https://%s", r.Host), http.StatusMovedPermanently)
|
target := fmt.Sprintf("https://%s%s", r.Host, r.URL.Path)
|
||||||
|
if len(r.URL.RawQuery) > 0 {
|
||||||
|
target += "?" + r.URL.RawQuery
|
||||||
|
}
|
||||||
|
http.Redirect(rw, r, target, http.StatusMovedPermanently)
|
||||||
})
|
})
|
||||||
nr := negroni.Classic()
|
nr := negroni.Classic()
|
||||||
nr.UseHandler(rr)
|
nr.UseHandler(rr)
|
||||||
|
|||||||
@@ -5,16 +5,20 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/play-with-docker/play-with-docker/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CloseSession(rw http.ResponseWriter, req *http.Request) {
|
func CloseSession(rw http.ResponseWriter, req *http.Request) {
|
||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
sessionId := vars["sessionId"]
|
sessionId := vars["sessionId"]
|
||||||
|
|
||||||
session := core.SessionGet(sessionId)
|
session, err := core.SessionGet(sessionId)
|
||||||
if session == nil {
|
if err == storage.NotFoundError {
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := core.SessionClose(session); err != nil {
|
if err := core.SessionClose(session); err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/play-with-docker/play-with-docker/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DeleteInstance(rw http.ResponseWriter, req *http.Request) {
|
func DeleteInstance(rw http.ResponseWriter, req *http.Request) {
|
||||||
@@ -11,7 +12,7 @@ func DeleteInstance(rw http.ResponseWriter, req *http.Request) {
|
|||||||
sessionId := vars["sessionId"]
|
sessionId := vars["sessionId"]
|
||||||
instanceName := vars["instanceName"]
|
instanceName := vars["instanceName"]
|
||||||
|
|
||||||
s := core.SessionGet(sessionId)
|
s, err := core.SessionGet(sessionId)
|
||||||
if s != nil {
|
if s != nil {
|
||||||
i := core.InstanceGet(s, instanceName)
|
i := core.InstanceGet(s, instanceName)
|
||||||
err := core.InstanceDelete(s, i)
|
err := core.InstanceDelete(s, i)
|
||||||
@@ -19,8 +20,11 @@ func DeleteInstance(rw http.ResponseWriter, req *http.Request) {
|
|||||||
rw.WriteHeader(http.StatusInternalServerError)
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else if err == storage.NotFoundError {
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func Exec(rw http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := core.SessionGet(sessionId)
|
s, _ := core.SessionGet(sessionId)
|
||||||
if s == nil {
|
if s == nil {
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/play-with-docker/play-with-docker/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FileUpload(rw http.ResponseWriter, req *http.Request) {
|
func FileUpload(rw http.ResponseWriter, req *http.Request) {
|
||||||
@@ -14,7 +15,14 @@ func FileUpload(rw http.ResponseWriter, req *http.Request) {
|
|||||||
sessionId := vars["sessionId"]
|
sessionId := vars["sessionId"]
|
||||||
instanceName := vars["instanceName"]
|
instanceName := vars["instanceName"]
|
||||||
|
|
||||||
s := core.SessionGet(sessionId)
|
s, err := core.SessionGet(sessionId)
|
||||||
|
if err == storage.NotFoundError {
|
||||||
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
i := core.InstanceGet(s, instanceName)
|
i := core.InstanceGet(s, instanceName)
|
||||||
|
|
||||||
// allow up to 32 MB which is the default
|
// allow up to 32 MB which is the default
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/play-with-docker/play-with-docker/pwd/types"
|
"github.com/play-with-docker/play-with-docker/pwd/types"
|
||||||
|
"github.com/play-with-docker/play-with-docker/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionInfo struct {
|
type SessionInfo struct {
|
||||||
@@ -18,8 +19,11 @@ func GetSession(rw http.ResponseWriter, req *http.Request) {
|
|||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
sessionId := vars["sessionId"]
|
sessionId := vars["sessionId"]
|
||||||
|
|
||||||
session := core.SessionGet(sessionId)
|
session, err := core.SessionGet(sessionId)
|
||||||
if session == nil {
|
if err == storage.NotFoundError {
|
||||||
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,21 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/play-with-docker/play-with-docker/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Home(w http.ResponseWriter, r *http.Request) {
|
func Home(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
sessionId := vars["sessionId"]
|
sessionId := vars["sessionId"]
|
||||||
|
|
||||||
s := core.SessionGet(sessionId)
|
s, err := core.SessionGet(sessionId)
|
||||||
if s == nil {
|
if err == storage.NotFoundError {
|
||||||
// Session doesn't exist (can happen if closing the sessions an reloading the page, or similar).
|
// Session doesn't exist (can happen if closing the sessions an reloading the page, or similar).
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if s.Stack != "" {
|
if s.Stack != "" {
|
||||||
go core.SessionDeployStack(s)
|
go core.SessionDeployStack(s)
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ func NewInstance(rw http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
json.NewDecoder(req.Body).Decode(&body)
|
json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
|
||||||
s := core.SessionGet(sessionId)
|
s, err := core.SessionGet(sessionId)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
playground := core.PlaygroundGet(s.PlaygroundId)
|
playground := core.PlaygroundGet(s.PlaygroundId)
|
||||||
if playground == nil {
|
if playground == nil {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Ping(rw http.ResponseWriter, req *http.Request) {
|
func Ping(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
defer latencyHistogramVec.WithLabelValues("ping").Observe(float64(time.Since(time.Now()).Nanoseconds()) / 1000000)
|
||||||
// Get system load average of the last 5 minutes and compare it against a threashold.
|
// Get system load average of the last 5 minutes and compare it against a threashold.
|
||||||
|
|
||||||
c, err := docker.NewEnvClient()
|
c, err := docker.NewEnvClient()
|
||||||
@@ -27,6 +28,7 @@ func Ping(rw http.ResponseWriter, req *http.Request) {
|
|||||||
if _, err := c.Info(ctx); err != nil && err == context.DeadlineExceeded {
|
if _, err := c.Info(ctx); err != nil && err == context.DeadlineExceeded {
|
||||||
log.Printf("Docker info took to long to respond\n")
|
log.Printf("Docker info took to long to respond\n")
|
||||||
rw.WriteHeader(http.StatusGatewayTimeout)
|
rw.WriteHeader(http.StatusGatewayTimeout)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := load.Avg()
|
a, err := load.Avg()
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ func SessionSetup(rw http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
json.NewDecoder(req.Body).Decode(&body)
|
json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
|
||||||
s := core.SessionGet(sessionId)
|
s, err := core.SessionGet(sessionId)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := core.SessionSetup(s, body)
|
err = core.SessionSetup(s, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if pwd.SessionNotEmpty(err) {
|
if pwd.SessionNotEmpty(err) {
|
||||||
log.Println("Cannot setup a session that contains instances")
|
log.Println("Cannot setup a session that contains instances")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/play-with-docker/play-with-docker/event"
|
"github.com/play-with-docker/play-with-docker/event"
|
||||||
|
"github.com/play-with-docker/play-with-docker/storage"
|
||||||
"github.com/satori/go.uuid"
|
"github.com/satori/go.uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,8 +145,8 @@ func ws(so *socket) {
|
|||||||
|
|
||||||
sessionId := vars["sessionId"]
|
sessionId := vars["sessionId"]
|
||||||
|
|
||||||
session := core.SessionGet(sessionId)
|
session, err := core.SessionGet(sessionId)
|
||||||
if session == nil {
|
if err == storage.NotFoundError {
|
||||||
log.Printf("Session with id [%s] does not exist!\n", sessionId)
|
log.Printf("Session with id [%s] does not exist!\n", sessionId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ type PWDApi interface {
|
|||||||
SessionClose(session *types.Session) error
|
SessionClose(session *types.Session) error
|
||||||
SessionGetSmallestViewPort(sessionId string) types.ViewPort
|
SessionGetSmallestViewPort(sessionId string) types.ViewPort
|
||||||
SessionDeployStack(session *types.Session) error
|
SessionDeployStack(session *types.Session) error
|
||||||
SessionGet(id string) *types.Session
|
SessionGet(id string) (*types.Session, error)
|
||||||
SessionSetup(session *types.Session, conf SessionSetupConf) error
|
SessionSetup(session *types.Session, conf SessionSetupConf) error
|
||||||
|
|
||||||
InstanceNew(session *types.Session, conf types.InstanceConfig) (*types.Instance, error)
|
InstanceNew(session *types.Session, conf types.InstanceConfig) (*types.Instance, error)
|
||||||
|
|||||||
@@ -205,17 +205,17 @@ func (p *pwd) SessionDeployStack(s *types.Session) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pwd) SessionGet(sessionId string) *types.Session {
|
func (p *pwd) SessionGet(sessionId string) (*types.Session, error) {
|
||||||
defer observeAction("SessionGet", time.Now())
|
defer observeAction("SessionGet", time.Now())
|
||||||
|
|
||||||
s, err := p.storage.SessionGet(sessionId)
|
s, err := p.storage.SessionGet(sessionId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pwd) SessionSetup(session *types.Session, sconf SessionSetupConf) error {
|
func (p *pwd) SessionSetup(session *types.Session, sconf SessionSetupConf) error {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var app = angular.module('DockerPlay', ['ngMaterial', 'ngFileUpload']);
|
var app = angular.module('DockerPlay', ['ngMaterial', 'ngFileUpload', 'ngclipboard']);
|
||||||
|
|
||||||
// Automatically redirects user to a new session when bypassing captcha.
|
// Automatically redirects user to a new session when bypassing captcha.
|
||||||
// Controller keeps code/logic separate from the HTML
|
// Controller keeps code/logic separate from the HTML
|
||||||
|
|||||||
@@ -116,6 +116,9 @@
|
|||||||
</md-card-content>
|
</md-card-content>
|
||||||
<md-card-actions>
|
<md-card-actions>
|
||||||
<md-button class="md-warn md-raised" ng-click="deleteInstance(instance)" ng-disabled="isInstanceBeingDeleted">{{deleteInstanceBtnText}}</md-button>
|
<md-button class="md-warn md-raised" ng-click="deleteInstance(instance)" ng-disabled="isInstanceBeingDeleted">{{deleteInstanceBtnText}}</md-button>
|
||||||
|
<md-button class="md-raised" ngclipboard data-clipboard-text="ssh {{instance.proxy_host}}@direct.{{instance.session_host}}">SSH
|
||||||
|
<md-tooltip md-direction="top">Copied</md-tooltip>
|
||||||
|
</md-button>
|
||||||
</md-card-actions>
|
</md-card-actions>
|
||||||
</md-card>
|
</md-card>
|
||||||
<md-card flex md-theme="default" md-theme-watch >
|
<md-card flex md-theme="default" md-theme-watch >
|
||||||
@@ -291,6 +294,8 @@
|
|||||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
|
||||||
|
<script src="https://cdn.rawgit.com/zenorocha/clipboard.js/master/dist/clipboard.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ngclipboard/1.1.2/ngclipboard.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/danialfarid-angular-file-upload/12.2.13/ng-file-upload-all.min.js" integrity="sha256-LrZq3efIkFX0BooX7x/rjWyYDvMKfFV2HJpy6HBw7cE=" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/danialfarid-angular-file-upload/12.2.13/ng-file-upload-all.min.js" integrity="sha256-LrZq3efIkFX0BooX7x/rjWyYDvMKfFV2HJpy6HBw7cE=" crossorigin="anonymous"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user