Skip to content

Commit

Permalink
shared query UI (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
breadchris committed Apr 10, 2024
1 parent 18cdcf2 commit e8d9b79
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 61 deletions.
8 changes: 7 additions & 1 deletion pkg/api/router.go
Expand Up @@ -43,6 +43,7 @@ func CreateMux(
})

r.Get("/share/{uuid}/data.{format}", apiFunctions.ShareData)
view.RegisterShareView(r, storageServices, destinationManager, c.Dashboard)

api := chi.NewRouter()
api.Use(apiFunctions.AuthMiddleware)
Expand Down Expand Up @@ -82,7 +83,12 @@ func CreateMux(
})

if c.Dashboard.Enabled {
d, err := view.New(storageServices, c.Dashboard, destinationManager, apiFunctions.Authenticator(apiFunctions.tokenAuth))
d, err := view.New(
storageServices,
c.Dashboard,
destinationManager,
apiFunctions.Authenticator(apiFunctions.tokenAuth),
)
if err != nil {
panic(err)
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/api/share.go
Expand Up @@ -19,6 +19,7 @@ func (a *ScratchDataAPIStruct) CreateQuery(w http.ResponseWriter, r *http.Reques
var requestBody struct {
Query string `json:"query"`
Duration int `json:"duration"` // Duration in seconds
Name string `json:"name"`
}

if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
Expand All @@ -34,9 +35,15 @@ func (a *ScratchDataAPIStruct) CreateQuery(w http.ResponseWriter, r *http.Reques
return
}

if requestBody.Name == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Name cannot be empty"))
return
}

destId := a.AuthGetDatabaseID(r.Context())
expires := time.Duration(requestBody.Duration) * time.Second
sharedQueryId, err := a.storageServices.Database.CreateShareQuery(r.Context(), destId, requestBody.Query, expires)
sharedQueryId, err := a.storageServices.Database.CreateShareQuery(r.Context(), destId, requestBody.Name, requestBody.Query, expires)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
Expand Down
4 changes: 2 additions & 2 deletions pkg/storage/database/database.go
Expand Up @@ -29,8 +29,8 @@ type Database interface {
AddAPIKey(ctx context.Context, destId int64, hashedAPIKey string) error
GetAPIKeyDetails(ctx context.Context, hashedAPIKey string) (models.APIKey, error)

CreateShareQuery(ctx context.Context, destId int64, query string, expires time.Duration) (queryId uuid.UUID, err error)
GetShareQuery(ctx context.Context, queryId uuid.UUID) (models.SharedQuery, bool)
CreateShareQuery(ctx context.Context, destId int64, name, query string, expires time.Duration) (queryId uuid.UUID, err error)
GetShareQuery(ctx context.Context, queryId uuid.UUID) (models.ShareQuery, bool)

CreateTeam(name string) (*models.Team, error)
AddUserToTeam(userId uint, teamId uint) error
Expand Down
31 changes: 15 additions & 16 deletions pkg/storage/database/gorm/gorm.go
Expand Up @@ -51,7 +51,7 @@ func NewGorm(
rc.db = db

err = db.AutoMigrate(
&models.ShareLink{},
&models.ShareQuery{},
&models.Team{},
&models.User{},
&models.Destination{},
Expand Down Expand Up @@ -98,11 +98,18 @@ func (s *Gorm) GetConnectionRequest(ctx context.Context, requestId uuid.UUID) (m
return req, nil
}

func (s *Gorm) CreateShareQuery(ctx context.Context, destId int64, query string, expires time.Duration) (queryId uuid.UUID, err error) {
func (s *Gorm) CreateShareQuery(
ctx context.Context,
destId int64,
name,
query string,
expires time.Duration,
) (queryId uuid.UUID, err error) {
id := uuid.New()
link := models.ShareLink{
link := models.ShareQuery{
UUID: id.String(),
DestinationID: destId,
Name: name,
Query: query,
ExpiresAt: time.Now().Add(expires),
}
Expand All @@ -115,25 +122,17 @@ func (s *Gorm) CreateShareQuery(ctx context.Context, destId int64, query string,
return id, nil
}

func (s *Gorm) GetShareQuery(ctx context.Context, queryId uuid.UUID) (models.SharedQuery, bool) {
var link models.ShareLink
res := s.db.First(&link, "uuid = ? AND expires_at > ?", queryId.String(), time.Now())
func (s *Gorm) GetShareQuery(ctx context.Context, queryId uuid.UUID) (models.ShareQuery, bool) {
var query models.ShareQuery
res := s.db.First(&query, "uuid = ? AND expires_at > ?", queryId.String(), time.Now())
if res.Error != nil {
if !errors.Is(res.Error, gorm.ErrRecordNotFound) {
log.Error().Err(res.Error).Str("query_id", queryId.String()).Msg("Unable to find shared query")
}

return models.SharedQuery{}, false
}

rc := models.SharedQuery{
ID: link.UUID,
Query: link.Query,
ExpiresAt: link.ExpiresAt,
DestinationID: link.DestinationID,
return models.ShareQuery{}, false
}

return rc, true
return query, true
}

func (s *Gorm) GetTeamId(userId uint) (uint, error) {
Expand Down
12 changes: 3 additions & 9 deletions pkg/storage/database/models/models.go
Expand Up @@ -8,17 +8,11 @@ import (
"gorm.io/gorm"
)

type SharedQuery struct {
ID string
Query string
DestinationID int64
ExpiresAt time.Time
}

type ShareLink struct {
type ShareQuery struct {
gorm.Model
UUID string `gorm:"index:idx_uuid,unique"`
UUID string `gorm:"index:idx_share_query_uuid,unique"`
DestinationID int64
Name string
Query string
ExpiresAt time.Time
}
Expand Down
56 changes: 34 additions & 22 deletions pkg/view/router.go
Expand Up @@ -49,6 +49,12 @@ type Connect struct {
APIUrl string
}

type ShareQuery struct {
Expires string
Name string
ID string
}

type FlashType string

const (
Expand Down Expand Up @@ -78,6 +84,7 @@ type Model struct {
UpsertConnection UpsertConnection
Data map[string]any
Request Request
ShareQuery ShareQuery
}

func init() {
Expand All @@ -89,6 +96,32 @@ func embeddedFH(config goview.Config, tmpl string) (string, error) {
return string(bytes), err
}

func newViewEngine(liveReload bool) *goview.ViewEngine {
gv := goview.New(goview.Config{
Root: "pkg/view/templates",
Extension: ".html",
Master: "layout/base",
Partials: []string{"partials/flash"},
DisableCache: true,
Funcs: map[string]any{
"prettyPrint": func(data any) string {
bytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err.Error()
}
return string(bytes)
},
"title": func(a string) string {
return cases.Title(language.AmericanEnglish).String(a)
},
},
})
if !liveReload {
gv.SetFileHandler(embeddedFH)
}
return gv
}

func New(
storageServices *storage.Services,
c config.DashboardConfig,
Expand All @@ -113,28 +146,7 @@ func New(
formDecoder := schema.NewDecoder()
formDecoder.IgnoreUnknownKeys(true)

gv := goview.New(goview.Config{
Root: "pkg/view/templates",
Extension: ".html",
Master: "layout/base",
Partials: []string{"partials/flash"},
DisableCache: true,
Funcs: map[string]any{
"prettyPrint": func(data any) string {
bytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err.Error()
}
return string(bytes)
},
"title": func(a string) string {
return cases.Title(language.AmericanEnglish).String(a)
},
},
})
if !c.LiveReload {
gv.SetFileHandler(embeddedFH)
}
gv := newViewEngine(c.LiveReload)

getUser := func(r *http.Request) (*models.User, bool) {
userAny := r.Context().Value("user")
Expand Down
88 changes: 88 additions & 0 deletions pkg/view/share.go
@@ -0,0 +1,88 @@
package view

import (
"fmt"
"net/http"
"strings"

"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/scratchdata/scratchdata/pkg/config"
"github.com/scratchdata/scratchdata/pkg/destinations"
"github.com/scratchdata/scratchdata/pkg/storage"
)

func RegisterShareView(
r *chi.Mux,
storageServices *storage.Services,
destinationManager *destinations.DestinationManager,
c config.DashboardConfig,
) {
gv := newViewEngine(c.LiveReload)
r.Get("/share/{uuid}", func(w http.ResponseWriter, r *http.Request) {
queryUUID := chi.URLParam(r, "uuid")

id, err := uuid.Parse(queryUUID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

cachedQuery, found := storageServices.Database.GetShareQuery(r.Context(), id)
if !found {
http.Error(w, "Query not found", http.StatusNotFound)
return
}

year, month, day := cachedQuery.ExpiresAt.Date()

m := Model{
HideSidebar: true,
ShareQuery: ShareQuery{
Expires: fmt.Sprintf("%s %d, %d", month.String(), day, year),
ID: id.String(),
Name: cachedQuery.Name,
},
}
if err := gv.Render(w, http.StatusOK, "pages/share", m); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})

r.Get("/share/{uuid}/download", func(w http.ResponseWriter, r *http.Request) {
format := strings.ToLower(r.URL.Query().Get("format"))

queryUUID := chi.URLParam(r, "uuid")

id, err := uuid.Parse(queryUUID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

cachedQuery, found := storageServices.Database.GetShareQuery(r.Context(), id)
if !found {
http.Error(w, "Query not found", http.StatusNotFound)
return
}

dest, err := destinationManager.Destination(r.Context(), cachedQuery.DestinationID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

switch format {
case "csv":
w.Header().Set("Content-Type", "text/csv")
if err := dest.QueryCSV(cachedQuery.Query, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
default:
w.Header().Set("Content-Type", "application/json")
if err := dest.QueryJSON(cachedQuery.Query, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
})
}
2 changes: 1 addition & 1 deletion pkg/view/templates/layout/base.html
Expand Up @@ -74,7 +74,7 @@
</div>
<div class="py-5">
{{template "content" .}}
</divc>
</div>
</div>
</div>

Expand Down
27 changes: 18 additions & 9 deletions pkg/view/templates/pages/connections/upsert.html
Expand Up @@ -39,19 +39,28 @@
{{ if not $isNew }}
<p>{{$title}}: {{ .UpsertConnection.Destination.Name }}</p>
<div class="flex flex-col space-y-3">
<form action="/dashboard/connections/delete" method="POST" class="mt-6 flex items-center gap-x-6">
<div class="flex flex-row">
<form action="/dashboard/connections/keys" method="POST" class="mt-6 flex items-center gap-x-6">
{{ .CSRFToken }}
<input type="hidden" name="id" value="{{ .UpsertConnection.Destination.ID }}">
<button type="submit" class="w-fit rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
New API Key
</button>
</form>
</div>
<div class="relative">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t border-gray-200"></div>
</div>
<div class="relative flex justify-center text-sm font-medium leading-6">
<span class="bg-white px-6 text-gray-900">Danger</span>
</div>
</div>
<form action="/dashboard/connections/delete" method="POST" class="mt-6 flex items-center gap-x-6 justify-end">
<input type="hidden" name="id" value="{{ .UpsertConnection.Destination.ID }}">
{{ .CSRFToken }}
<button type="submit" class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">Delete</button>
</form>

<form action="/dashboard/connections/keys" method="POST" class="mt-6 flex items-center gap-x-6">
{{ .CSRFToken }}
<input type="hidden" name="id" value="{{ .UpsertConnection.Destination.ID }}">
<button type="submit" class="w-fit rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
New API Key
</button>
</form>
</div>
{{else}}
{{ if not $isRequest }}
Expand Down

0 comments on commit e8d9b79

Please sign in to comment.