Skip to content

Commit

Permalink
feat: init ui (#1)
Browse files Browse the repository at this point in the history
Co-authored-by: Quentin Lemaire <[email protected]>
  • Loading branch information
vharny and SkYNewZ authored Jan 25, 2024
1 parent 1988884 commit 4064853
Show file tree
Hide file tree
Showing 33 changed files with 2,794 additions and 33 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ jobs:
with:
go-version: 1.21

- name: Setup task
uses: arduino/setup-task@v1

- name: Build frontend
run: task ui:build --force

- name: Lint
uses: golangci/golangci-lint-action@v3
with:
Expand Down Expand Up @@ -58,6 +64,12 @@ jobs:
- name: Install ko
uses: imjasonh/[email protected]

- name: Setup task
uses: arduino/setup-task@v1

- name: Build frontend
run: task ui:build --force

- name: Build and push snapshot Docker image
env:
KO_DOCKER_REPO: ghcr.io/skynewz/gh-stars-search-engine
Expand Down
5 changes: 1 addition & 4 deletions .slog.config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# See documentation at https://github.com/go-simpler/sloggen/blob/main/.slog.example.yml
pkg: slogx

imports:
- time
- net/http
imports: [ time ]

attrs:
- err: error
- duration: time.Duration
- request: "*http.Request"
- component: string
52 changes: 52 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
version: 3

vars:
UI_DIRECTORY: ui

tasks:
ui:deps:
desc: Install UI dependencies
dir: "{{ .UI_DIRECTORY }}"
cmd: |
docker run --rm --name yarn \
-v $(pwd):/app \
-w /app \
node:lts-alpine \
yarn install --frozen-lockfile --non-interactive --no-progress --production=false
sources:
- package.json
- yarn.lock

ui:build:
desc: Build UI
dir: "{{ .UI_DIRECTORY }}"
deps: [ ui:deps ]
cmd: |
docker run --rm --name yarn \
-v $(pwd):/app \
-w /app \
node:lts-alpine \
yarn build --mode production
sources:
- src/**/*
- .eslintrc.cjs
- index.html
- postcss.config.js
- tailwind.config.ts
- tsconfig.json
- tsconfig.node.json
- vite.config.ts
generates:
- dist/**/*

ui:setup:
desc: Start yarn container
dir: "{{ .UI_DIRECTORY }}"
deps: [ ui:deps ]
cmd: |
docker run -it --rm --name yarn \
-v $(pwd):/app \
-w /app \
node:lts-alpine \
sh
19 changes: 19 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: "3"

services:
ghs-engine:
container_name: ghs-engine
image: ghcr.io/skynewz/gh-stars-search-engine:latest
restart: unless-stopped
environment:
GITHUB_TOKEN: ${GITHUB_TOKEN}
BELVE_STORAGE_PATH: /ko-app/ghs.belve
volumes:
- /opt/ghs-search/ghs.belve:/ko-app/ghs.belve
networks:
- proxy # Allow access from our Nginx Proxy Manager

networks:
proxy:
external: true

31 changes: 27 additions & 4 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,39 @@ func (e *engine) BatchIndex(data []Indexable, batchSize int) error {
return flushBatch()
}

type SearchOption func(*bleve.SearchRequest)

// WithSearchFields sets the fields to return in the search results.
func WithSearchFields(fields ...string) SearchOption {
return func(r *bleve.SearchRequest) {
r.Fields = fields // return only the given fields
}
}

// WithSearchFrom sets the index of the first result to return.
func WithSearchFrom(from int) SearchOption {
return func(r *bleve.SearchRequest) {
r.From = from // return results starting from the given index
}
}

// WithSearchSize sets the number of results to return.
func WithSearchSize(size int) SearchOption {
return func(r *bleve.SearchRequest) {
r.Size = size // return the given number of results
}
}

// Search executes the given query and returns the results.
// TODO: if we want to sort by starredAt, we need to index it as a date field and use sort https://blevesearch.com/docs/Sorting/
func (e *engine) Search(ctx context.Context, q string, from int, size int, fields ...string) (*bleve.SearchResult, error) {
func (e *engine) Search(ctx context.Context, q string, opts ...SearchOption) (*bleve.SearchResult, error) {
// https://blevesearch.com/docs/Query-String-Query/
query := bleve.NewQueryStringQuery(q)

search := bleve.NewSearchRequest(query)
search.Fields = fields // return only the given fields
search.From = from // return results starting from the given index
search.Size = size // return the given number of results
for _, opt := range opts {
opt(search)
}

results, err := e.index.SearchInContext(ctx, search)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/engine/engine_iface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 41 additions & 10 deletions internal/http/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ package http
import (
"context"
"encoding/json"
"io/fs"
"net/http"
"strconv"
"strings"

"github.com/SkYNewZ/gh-stars-search-engine/internal/engine"
"github.com/SkYNewZ/gh-stars-search-engine/internal/slogx"
"github.com/SkYNewZ/gh-stars-search-engine/ui"
)

const defaultPageSize int = 10

func (s *server) searchHandler(w http.ResponseWriter, r *http.Request) {
// read q query param
q := r.URL.Query().Get("q")
Expand All @@ -32,20 +37,19 @@ func (s *server) searchHandler(w http.ResponseWriter, r *http.Request) {
}

// pagination
from := r.URL.Query().Get("from")
size := r.URL.Query().Get("size")
parseInt := func(s string, def int) int {
if i, err := strconv.Atoi(s); err == nil {
return i
}

return def
}
pageSize := parseQueryParamPositive(r.URL.Query().Get("size"), defaultPageSize)
from := parseQueryParamPositive(r.URL.Query().Get("from"), 0)

ctx, cancel := context.WithTimeout(r.Context(), s.searchTimeout)
defer cancel()

res, err := s.search.Search(ctx, q, parseInt(from, 0), parseInt(size, 10), searchResponseFields...)
res, err := s.search.Search(
ctx,
q,
engine.WithSearchFrom(from),
engine.WithSearchSize(pageSize),
engine.WithSearchFields(searchResponseFields...),
)
if err != nil {
s.responseErrorAsJSON(w, r, http.StatusInternalServerError, err.Error())
return
Expand All @@ -58,6 +62,17 @@ func (s *server) healthHandler(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("OK"))
}

func (s *server) uiHandler(w http.ResponseWriter, r *http.Request) {
_ui, err := fs.Sub(ui.Dist, "dist")
if err != nil {
s.logger.With(slogx.Err(err)).Error("failed to get sub filesystem")
http.Error(w, "cannot render ui", http.StatusInternalServerError)
return
}

http.FileServer(http.FS(_ui)).ServeHTTP(w, r)
}

// responseAsJSON writes the data as JSON to the response writer.
func (s *server) responseAsJSON(w http.ResponseWriter, _ *http.Request, code int, data any) {
w.Header().Set("Content-Type", "application/json")
Expand All @@ -76,3 +91,19 @@ func (s *server) responseErrorAsJSON(w http.ResponseWriter, r *http.Request, cod

s.responseAsJSON(w, r, code, body)
}

// parseQueryParamPositive parses the query param v as an int.
// If the value is not an int, returns def.
// If the value is negative, returns def.
func parseQueryParamPositive(v string, def int) int {
i, err := strconv.Atoi(v)
if err != nil {
return def
}

if i <= 0 {
return def
}

return i
}
10 changes: 9 additions & 1 deletion internal/http/middlewares.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package http

import (
"log/slog"
"net/http"

"github.com/SkYNewZ/gh-stars-search-engine/internal/slogx"
Expand Down Expand Up @@ -36,7 +37,14 @@ func (s *server) allowedMethod(methods ...string) func(http.Handler) http.Handle

func (s *server) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.logger.With(slogx.Request(r)).Debug("handling request")
s.logger.With(slog.Group(
"request",
slog.String("method", r.Method),
slog.String("url", r.URL.String()),
slog.String("remote_addr", r.RemoteAddr),
slog.String("user_agent", r.UserAgent()),
slog.String("referer", r.Referer()),
)).Debug("handling request")
next.ServeHTTP(w, r)
})
}
17 changes: 9 additions & 8 deletions internal/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package http
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
Expand All @@ -28,17 +27,12 @@ func NewServer(logger *slog.Logger, search engine.Engine, searchTimeout time.Dur
logger = slog.Default()
}

port := "8080"
if v := os.Getenv("PORT"); v != "" {
port = v
}

srv := &server{
logger: logger,
search: search,
searchTimeout: searchTimeout,
httpServer: &http.Server{
Addr: fmt.Sprintf(":%s", port),
Addr: "", // will be set by Start
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Expand All @@ -49,6 +43,7 @@ func NewServer(logger *slog.Logger, search engine.Engine, searchTimeout time.Dur
router := http.NewServeMux()
router.HandleFunc("/search", srv.searchHandler)
router.HandleFunc("/health", srv.healthHandler)
router.HandleFunc("/", srv.uiHandler)

// setup default middlewares
r := srv.recoverMiddleware(router)
Expand All @@ -61,7 +56,13 @@ func NewServer(logger *slog.Logger, search engine.Engine, searchTimeout time.Dur

// Start starts the HTTP server.
func (s *server) Start() {
s.logger.Info("starting HTTP server")
port := "8080"
if v := os.Getenv("PORT"); v != "" {
port = v
}

s.logger.Info("starting HTTP server on port " + port)
s.httpServer.Addr = ":" + port
if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.With(slogx.Err(err)).Error("failed to start HTTP server")
}
Expand Down
2 changes: 0 additions & 2 deletions internal/slogx/slogx.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions ui/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
24 changes: 24 additions & 0 deletions ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
Loading

0 comments on commit 4064853

Please sign in to comment.