summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-04-22 11:34:14 -0600
committermo khan <mo@mokhan.ca>2025-04-22 11:34:14 -0600
commitc11693668f7e230a21f806664b411a598bee9b10 (patch)
tree0fecffe3b5b7a98b2134cc86402370a777fdcd4c
parent699ef9cc2a910774d1f210ef0562496d08f04e03 (diff)
feat: add tiny vue.js app to list and add new sparkles
-rw-r--r--app/controllers/sparkles/controller.go23
-rw-r--r--app/controllers/sparkles/controller_test.go20
-rw-r--r--app/controllers/sparkles/init.go20
-rw-r--r--go.sum9
-rw-r--r--public/application.js76
-rw-r--r--public/index.html17
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
+ })
+}
diff --git a/go.sum b/go.sum
index 264adc1..c3ee243 100644
--- a/go.sum
+++ b/go.sum
@@ -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>