diff options
| author | mo khan <mo@mokhan.ca> | 2025-04-22 11:34:14 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-04-22 11:34:14 -0600 |
| commit | c11693668f7e230a21f806664b411a598bee9b10 (patch) | |
| tree | 0fecffe3b5b7a98b2134cc86402370a777fdcd4c | |
| parent | 699ef9cc2a910774d1f210ef0562496d08f04e03 (diff) | |
feat: add tiny vue.js app to list and add new sparkles
| -rw-r--r-- | app/controllers/sparkles/controller.go | 23 | ||||
| -rw-r--r-- | app/controllers/sparkles/controller_test.go | 20 | ||||
| -rw-r--r-- | app/controllers/sparkles/init.go | 20 | ||||
| -rw-r--r-- | go.sum | 9 | ||||
| -rw-r--r-- | public/application.js | 76 | ||||
| -rw-r--r-- | public/index.html | 17 |
6 files changed, 150 insertions, 15 deletions
diff --git a/app/controllers/sparkles/controller.go b/app/controllers/sparkles/controller.go index bc388db..07a21ba 100644 --- a/app/controllers/sparkles/controller.go +++ b/app/controllers/sparkles/controller.go @@ -3,7 +3,10 @@ package sparkles import ( "net/http" + "github.com/xlgmokha/x/pkg/log" + "github.com/xlgmokha/x/pkg/mapper" "github.com/xlgmokha/x/pkg/serde" + "github.com/xlgmokha/x/pkg/x" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/db" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/domain" ) @@ -26,7 +29,23 @@ func (c *Controller) Index(w http.ResponseWriter, r *http.Request) { } func (c *Controller) Create(w http.ResponseWriter, r *http.Request) { - sparkle, _ := serde.FromHTTP[*domain.Sparkle](r) - c.db.Save(sparkle) + sparkle := mapper.MapFrom[*http.Request, *domain.Sparkle](r) + + if x.IsZero(sparkle) { + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := c.db.Save(sparkle); err != nil { + log.WithFields(r.Context(), log.Fields{"error": err}) + w.WriteHeader(http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusCreated) + if err := serde.ToHTTP(w, r, sparkle); err != nil { + log.WithFields(r.Context(), log.Fields{"error": err}) + w.WriteHeader(http.StatusInternalServerError) + return + } } diff --git a/app/controllers/sparkles/controller_test.go b/app/controllers/sparkles/controller_test.go index 99fdd69..4eadd2c 100644 --- a/app/controllers/sparkles/controller_test.go +++ b/app/controllers/sparkles/controller_test.go @@ -48,7 +48,7 @@ func TestSparkles(t *testing.T) { controller := New(repository) controller.MountTo(mux) - sparkle, _ := domain.NewSparkle("@tanuki for reviewing my MR!") + sparkle, _ := domain.NewSparkle("@tanuki for reviewing my code!") request, response := test.RequestResponse( "POST", "/sparkles", @@ -59,7 +59,23 @@ func TestSparkles(t *testing.T) { mux.ServeHTTP(response, request) require.Equal(t, http.StatusCreated, response.Code) - assert.Equal(t, 1, len(repository.All())) + + t.Run("returns a JSON representation of the sparkle", func(t *testing.T) { + item, err := serde.FromJSON[*domain.Sparkle](response.Body) + require.NoError(t, err) + + assert.NotEmpty(t, item.ID) + assert.Equal(t, "@tanuki", item.Sparklee) + assert.Equal(t, "for reviewing my code!", item.Reason) + }) + + t.Run("saves the sparkle to the db", func(t *testing.T) { + assert.Equal(t, 1, len(repository.All())) + item := repository.All()[0] + + assert.Equal(t, "@tanuki", item.Sparklee) + assert.Equal(t, "for reviewing my code!", item.Reason) + }) }) }) } diff --git a/app/controllers/sparkles/init.go b/app/controllers/sparkles/init.go new file mode 100644 index 0000000..9efcac8 --- /dev/null +++ b/app/controllers/sparkles/init.go @@ -0,0 +1,20 @@ +package sparkles + +import ( + "net/http" + + "github.com/xlgmokha/x/pkg/log" + "github.com/xlgmokha/x/pkg/mapper" + "github.com/xlgmokha/x/pkg/serde" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/domain" +) + +func init() { + mapper.Register[*http.Request, *domain.Sparkle](func(r *http.Request) *domain.Sparkle { + sparkle, err := serde.FromHTTP[*domain.Sparkle](r) + if err != nil { + log.WithFields(r.Context(), log.Fields{"error": err}) + } + return sparkle + }) +} @@ -34,8 +34,6 @@ github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= -github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= @@ -50,13 +48,10 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golobby/container/v3 v3.3.2 h1:7u+RgNnsdVlhGoS8gY4EXAG601vpMMzLZlYqSp77Quw= github.com/golobby/container/v3 v3.3.2/go.mod h1:RDdKpnKpV1Of11PFBe7Dxc2C1k2KaLE4FD47FflAmj0= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -128,7 +123,6 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -138,8 +132,6 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/xlgmokha/x v0.0.0-20250417164331-acec892e974c h1:HiKA6KkHDb1zNNmKesX4EE+W/ZIP5ymK1uguGD9/GGg= -github.com/xlgmokha/x v0.0.0-20250417164331-acec892e974c/go.mod h1:axGPKzoJCNTmPJxYqN5l+Z9gGbPe0yolkT61a5p3QiI= github.com/xlgmokha/x v0.0.0-20250421190355-b1595f00ffc2 h1:MMhix29kmgcRQ/kR7beQcRhcB4XWTLZUk7WC/L8Lc44= github.com/xlgmokha/x v0.0.0-20250421190355-b1595f00ffc2/go.mod h1:axGPKzoJCNTmPJxYqN5l+Z9gGbPe0yolkT61a5p3QiI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -166,7 +158,6 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/public/application.js b/public/application.js new file mode 100644 index 0000000..dcc45f8 --- /dev/null +++ b/public/application.js @@ -0,0 +1,76 @@ +document.addEventListener('DOMContentLoaded', (event) => { + const { createApp, ref } = Vue + const regex = /\s*(?<sparklee>@\w+)\s+(?<reason>.+)/ + + const app = createApp({ + created: function() { + this.reload(); + this.intervalId = setInterval(() => this.reload(), 30000); + }, + destroyed: function() { + if (this.intervalId) + clearInterval(this.intervalId); + this.intervalId = null; + }, + computed: { + heading: function() { + return this.sparkles.length == 0 ? "No Sparkles Sent" : "Recent Sparkles"; + }, + recentSparkles: function() { + return this.sparkles.reverse(); + }, + isDisabled: function() { + return this.isSending || !this.isValid(); + }, + }, + data() { + return { + intervalId: null, + errorMessage: "", + isSending: false, + sparkle: "", + sparkles: [], + } + }, + methods: { + reload: function() { + fetch("/sparkles") + .then((response) => response.json()) + .then((json) => this.sparkles = json) + .catch((json) => console.dir(json)); + }, + isValid: function() { + return this.sparkle.length > 0; + }, + submitSparkle: function() { + this.isSending = true; + + let matches = regex.exec(this.sparkle) + let sparklee = matches.groups.sparklee + let reason = matches.groups.reason + + fetch("/sparkles", { + method: "POST", + mode: "cors", + cache: "no-cache", + headers: { "Content-Type": "application/json" }, + redirect: "follow", + body: JSON.stringify({ sparklee: sparklee, reason: reason }) + }).then((response) => { + response.json().then((json) => { + this.isSending = false; + if (response.ok) { + this.sparkles.push(json); + this.sparkle = ""; + } else { + this.errorMessage = json["error"]; + } + }) + }).catch((error) => console.error(error)); + }, + } + }) + + app.mount('#app') +}) + diff --git a/public/index.html b/public/index.html index 6231d7b..d9033a5 100644 --- a/public/index.html +++ b/public/index.html @@ -6,17 +6,30 @@ <meta name="color-scheme" content="light dark"> <title>SparkleLab</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"> + <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> + <script src="/application.js"></script> </head> <body> - <main class="container"> + <main id="app" class="container"> <nav> <ul> - <li><strong>SparkleLab</strong></li> + <li><strong>SparkleLab✨</strong></li> </ul> <ul> <li><a href="/session/new">Login</a></li> </ul> </nav> + + <form v-on:submit.prevent="submitSparkle"> + <label>/sparkle <input type="text" placeholder="@tanuki for helping me with my homework!" v-model="sparkle" /> </label> + <button type="submit" v-bind:disabled="isDisabled">✨ Sparkle</button> + </form> + <span class="error">{{ errorMessage }}</span> + + <h1>{{ heading }}</h1> + <div v-for="sparkle in recentSparkles"> + <p> <strong>{{ sparkle.sparklee }}</strong> {{ sparkle.reason }} </p> + </div> </main> </body> </html> |
