Initial Commit
This commit is contained in:
commit
30611aa45d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
_*
|
||||
*.sqlite
|
83
cmd/api/api.go
Normal file
83
cmd/api/api.go
Normal file
@ -0,0 +1,83 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sap-cds-search/cmd/database/table"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CDSViewFieldJSON struct {
|
||||
Fields []table.CDSViewField `json:"fields"`
|
||||
}
|
||||
|
||||
func GetCDSViewField(CDSViewTechnicalName string) error {
|
||||
var json CDSViewFieldJSON
|
||||
path := "https://api.sap.com/odata/1.0/catalog.svc/CdsViewsContent.CdsViews('" + CDSViewTechnicalName + "')/$value"
|
||||
fmt.Println("Getting: ", CDSViewTechnicalName)
|
||||
err := RequestJSON(path, &json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, field := range json.Fields {
|
||||
field.CDSViewTechnicalName = CDSViewTechnicalName
|
||||
|
||||
err := table.InsertOrReplaceCDSViewField(field)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetCDSViewFields() {
|
||||
|
||||
cdsViews, _ := table.QueryAllCDSViewTechnicalNames()
|
||||
|
||||
for _, cdsView := range *cdsViews {
|
||||
var json CDSViewFieldJSON
|
||||
path := "https://api.sap.com/odata/1.0/catalog.svc/CdsViewsContent.CdsViews('" + cdsView + "')/$value"
|
||||
fmt.Println("Getting: ", cdsView)
|
||||
err := RequestJSON(path, &json)
|
||||
if err != nil {
|
||||
fmt.Println(cdsView, err)
|
||||
continue
|
||||
}
|
||||
for _, field := range json.Fields {
|
||||
field.CDSViewTechnicalName = cdsView
|
||||
|
||||
err := table.InsertOrReplaceCDSViewField(field)
|
||||
if err != nil {
|
||||
fmt.Println(field.CDSViewTechnicalName, field.FieldName, err)
|
||||
}
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func RequestJSON(url string, v any) error {
|
||||
response, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseData, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(responseData) == 0 {
|
||||
return errors.New("empty response")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(responseData, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
36
cmd/database/database.go
Normal file
36
cmd/database/database.go
Normal file
@ -0,0 +1,36 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/glebarez/go-sqlite"
|
||||
)
|
||||
|
||||
type database struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
const (
|
||||
database_path = "./db.sqlite"
|
||||
)
|
||||
|
||||
var DB database
|
||||
|
||||
//go:embed sql/create_tables.sql
|
||||
var create_tables string
|
||||
|
||||
func Load() {
|
||||
db, err := sql.Open("sqlite", database_path+"?_pragma=busy_timeout(5000)&foreign_keys(1)")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(create_tables)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
DB.DB = db
|
||||
}
|
17
cmd/database/sql/create_tables.sql
Normal file
17
cmd/database/sql/create_tables.sql
Normal file
@ -0,0 +1,17 @@
|
||||
CREATE TABLE CDSView (
|
||||
TechnicalName TEXT PRIMARY KEY,
|
||||
DisplayName TEXT,
|
||||
Description TEXT,
|
||||
Version TEXT,
|
||||
State TEXT,
|
||||
CreatedAt INTEGER,
|
||||
ModifiedAt INTEGER
|
||||
)
|
||||
CREATE TABLE CDSViewField (
|
||||
CDSViewTechnicalName TEXT PRIMARY KEY,
|
||||
FieldName TEXT PRIMARY KEY,
|
||||
Description TEXT,
|
||||
DataType TEXT,
|
||||
FieldLength TEXT,
|
||||
FOREIGN KEY (CDSViewTechnicalName) REFERENCES CDSView (TechnicalName)
|
||||
)
|
53
cmd/database/table/cdsView.go
Normal file
53
cmd/database/table/cdsView.go
Normal file
@ -0,0 +1,53 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sap-cds-search/cmd/database"
|
||||
)
|
||||
|
||||
type CDSView struct {
|
||||
TechnicalName string `json:"Name"`
|
||||
DisplayName string `json:"DisplayName"`
|
||||
Description string `json:"Description"`
|
||||
Version string `json:"Version"`
|
||||
State string `json:"State"`
|
||||
CreatedAt int `json:"CreatedAt"`
|
||||
ModifiedAt int `json:"ModifiedAt"`
|
||||
}
|
||||
|
||||
//go:embed sql/query_cds_view.sql
|
||||
var query_cds_view string
|
||||
|
||||
func GetCDSView(TechnicalName string) (*CDSView, error) {
|
||||
row := database.DB.QueryRow(query_cds_view, TechnicalName)
|
||||
|
||||
var CDSView CDSView
|
||||
err := row.Scan(&CDSView.TechnicalName, &CDSView.DisplayName, &CDSView.Description, &CDSView.Version, &CDSView.State, &CDSView.CreatedAt, &CDSView.ModifiedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CDSView, nil
|
||||
}
|
||||
|
||||
//go:embed sql/query_all_cds_view_technical_names.sql
|
||||
var query_all_cds_view_technical_names string
|
||||
|
||||
func QueryAllCDSViewTechnicalNames() (*[]string, error) {
|
||||
rows, err := database.DB.Query(query_all_cds_view_technical_names)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var technicalNames []string
|
||||
for rows.Next() {
|
||||
var technicalName string
|
||||
err := rows.Scan(&technicalName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
technicalNames = append(technicalNames, technicalName)
|
||||
}
|
||||
|
||||
return &technicalNames, nil
|
||||
}
|
45
cmd/database/table/cdsViewField.go
Normal file
45
cmd/database/table/cdsViewField.go
Normal file
@ -0,0 +1,45 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sap-cds-search/cmd/database"
|
||||
)
|
||||
|
||||
type CDSViewField struct {
|
||||
CDSViewTechnicalName string
|
||||
FieldName string `json:"fieldname"`
|
||||
Description string `json:"description"`
|
||||
DataType string `json:"datatype"`
|
||||
FieldLength string `json:"fieldlength"`
|
||||
}
|
||||
|
||||
//go:embed sql/query_cds_view_fields.sql
|
||||
var query_cds_view_fields string
|
||||
|
||||
func GetCDSViewFields(CDSViewTechnicalName string) (*[]CDSViewField, error) {
|
||||
rows, err := database.DB.Query(query_cds_view_fields, CDSViewTechnicalName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fields []CDSViewField = make([]CDSViewField, 0)
|
||||
for rows.Next() {
|
||||
var field CDSViewField
|
||||
err := rows.Scan(&field.CDSViewTechnicalName, &field.FieldName, &field.Description, &field.DataType, &field.FieldLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return &fields, nil
|
||||
}
|
||||
|
||||
//go:embed sql/insert_or_replace_cds_view_field.sql
|
||||
var insert_or_replace_cds_view_field string
|
||||
|
||||
func InsertOrReplaceCDSViewField(field CDSViewField) error {
|
||||
_, err := database.DB.Exec(insert_or_replace_cds_view_field, field.CDSViewTechnicalName, field.FieldName, field.Description, field.DataType, field.FieldLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
42
cmd/database/table/keyword.go
Normal file
42
cmd/database/table/keyword.go
Normal file
@ -0,0 +1,42 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sap-cds-search/cmd/database"
|
||||
)
|
||||
|
||||
type Keyword struct {
|
||||
CDSViewTechnicalName string
|
||||
Keyword string
|
||||
}
|
||||
|
||||
//go:embed sql/query_all_keywords.sql
|
||||
var query_all_keywords string
|
||||
|
||||
func GetAllKeywords() (*[]Keyword, error) {
|
||||
rows, err := database.DB.Query(query_all_keywords)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var keywords []Keyword
|
||||
for rows.Next() {
|
||||
var keyword Keyword
|
||||
err := rows.Scan(&keyword.CDSViewTechnicalName, &keyword.Keyword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keywords = append(keywords, keyword)
|
||||
}
|
||||
return &keywords, nil
|
||||
}
|
||||
|
||||
//go:embed sql/insert_or_replace_keyword.sql
|
||||
var insert_or_replace_keyword string
|
||||
|
||||
func InsertOrReplaceKeyword(CDSViewTechnicalName string, Keywords string) error {
|
||||
_, err := database.DB.Exec(insert_or_replace_keyword, CDSViewTechnicalName, Keywords)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
INSERT
|
||||
OR REPLACE INTO CDSViewField
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?)
|
4
cmd/database/table/sql/insert_or_replace_keyword.sql
Normal file
4
cmd/database/table/sql/insert_or_replace_keyword.sql
Normal file
@ -0,0 +1,4 @@
|
||||
INSERT
|
||||
OR REPLACE INTO Keyword
|
||||
VALUES
|
||||
(?, ?)
|
@ -0,0 +1,4 @@
|
||||
SELECT
|
||||
TechnicalName
|
||||
FROM
|
||||
CDSView
|
29
cmd/database/table/sql/query_all_keywords.sql
Normal file
29
cmd/database/table/sql/query_all_keywords.sql
Normal file
@ -0,0 +1,29 @@
|
||||
select
|
||||
CDSViewTechnicalName as CDSViewTechnicalName,
|
||||
lower(FieldName) as keyword
|
||||
from
|
||||
CDSViewField
|
||||
UNION
|
||||
select
|
||||
CDSViewTechnicalName as CDSViewTechnicalName,
|
||||
lower(Description) as keyword
|
||||
from
|
||||
CDSViewField
|
||||
UNION
|
||||
select
|
||||
TechnicalName as CDSViewTechnicalName,
|
||||
lower(TechnicalName) as keyword
|
||||
from
|
||||
CDSView
|
||||
UNION
|
||||
select
|
||||
TechnicalName as CDSViewTechnicalName,
|
||||
lower(Description) as keyword
|
||||
from
|
||||
CDSView
|
||||
UNION
|
||||
select
|
||||
TechnicalName as CDSViewTechnicalName,
|
||||
lower(DisplayName) as keyword
|
||||
from
|
||||
CDSView
|
2
cmd/database/table/sql/query_cds_view.sql
Normal file
2
cmd/database/table/sql/query_cds_view.sql
Normal file
@ -0,0 +1,2 @@
|
||||
SELECT * FROM CDSView
|
||||
WHERE TechnicalName = ?
|
2
cmd/database/table/sql/query_cds_view_fields.sql
Normal file
2
cmd/database/table/sql/query_cds_view_fields.sql
Normal file
@ -0,0 +1,2 @@
|
||||
SELECT * FROM CDSViewField
|
||||
WHERE CDSViewTechnicalName = ?
|
80
cmd/model/cdsView.go
Normal file
80
cmd/model/cdsView.go
Normal file
@ -0,0 +1,80 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sap-cds-search/cmd/database/table"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type CDSViewFieldModel struct {
|
||||
table.CDSViewField
|
||||
DataTypeTitle string
|
||||
FieldLengthOut string
|
||||
DescriptionOut string
|
||||
}
|
||||
|
||||
type CDSViewModel struct {
|
||||
*table.CDSView
|
||||
StateTitle string
|
||||
Fields *[]CDSViewFieldModel
|
||||
}
|
||||
|
||||
var englishCases = cases.Title(language.English)
|
||||
|
||||
func GetCDSViewModel(TechnicalName string) (*CDSViewModel, error) {
|
||||
var model CDSViewModel
|
||||
|
||||
cdsView, err := table.GetCDSView(TechnicalName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
model.CDSView = cdsView
|
||||
|
||||
model.StateTitle = englishCases.String(model.State)
|
||||
|
||||
fields, err := table.GetCDSViewFields(TechnicalName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fieldModel CDSViewFieldModel
|
||||
var fieldsModel []CDSViewFieldModel
|
||||
for _, field := range *fields {
|
||||
fieldModel.CDSViewField = field
|
||||
fieldModel.DataTypeTitle = englishCases.String(field.DataType)
|
||||
fieldModel.FieldLengthOut = strings.TrimLeft(field.FieldLength, "0")
|
||||
fieldModel.DescriptionOut = field.Description
|
||||
if fieldModel.DescriptionOut == "" {
|
||||
fieldModel.DescriptionOut = "-"
|
||||
}
|
||||
fieldsModel = append(fieldsModel, fieldModel)
|
||||
}
|
||||
|
||||
model.Fields = &fieldsModel
|
||||
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
func GetAllCDSViewModels() (*[]CDSViewModel, error) {
|
||||
cdsViewTechnicalNames, err := table.QueryAllCDSViewTechnicalNames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cdsViewModels []CDSViewModel
|
||||
for _, cdsViewTechnicalName := range *cdsViewTechnicalNames {
|
||||
cdsViewModel, err := GetCDSViewModel(cdsViewTechnicalName)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
cdsViewModels = append(cdsViewModels, *cdsViewModel)
|
||||
}
|
||||
|
||||
return &cdsViewModels, nil
|
||||
}
|
12
cmd/model/keyword.go
Normal file
12
cmd/model/keyword.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "sap-cds-search/cmd/database/table"
|
||||
|
||||
func GetKeywordsModel() (*[]table.Keyword, error) {
|
||||
keywords, err := table.GetAllKeywords()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keywords, nil
|
||||
}
|
20
cmd/model/results.go
Normal file
20
cmd/model/results.go
Normal file
@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
type ResultsModel struct {
|
||||
SearchTerm string
|
||||
CurrentPage int
|
||||
MaxPage int
|
||||
Views []CDSViewModel
|
||||
}
|
||||
|
||||
type ResultsModelBuffer struct {
|
||||
Views []CDSViewModel
|
||||
}
|
||||
|
||||
func NewResultsModel() *ResultsModel {
|
||||
return &ResultsModel{}
|
||||
}
|
||||
|
||||
func (r *ResultsModel) AppendResultsModelViews(cdsView *CDSViewModel) {
|
||||
r.Views = append(r.Views, *cdsView)
|
||||
}
|
33
cmd/router/router.go
Normal file
33
cmd/router/router.go
Normal file
@ -0,0 +1,33 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sap-cds-search/cmd/search"
|
||||
"sap-cds-search/cmd/ui"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func Load() *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
r.Group(func(r chi.Router) {
|
||||
ui.Load()
|
||||
r.Get("/", handleRoot)
|
||||
r.Route("/search", func(r chi.Router) {
|
||||
r.Get("/", search.HandleSearch)
|
||||
})
|
||||
})
|
||||
|
||||
r.Handle("/ui/css/*", http.StripPrefix("/ui/css", http.FileServer(http.Dir("./cmd/ui/css"))))
|
||||
r.Handle("/ui/svg/*", http.StripPrefix("/ui/svg", http.FileServer(http.Dir("./cmd/ui/svg"))))
|
||||
return r
|
||||
}
|
||||
|
||||
func handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
err := ui.Template.ExecuteTemplate(w, "main", nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
215
cmd/search/search.go
Normal file
215
cmd/search/search.go
Normal file
@ -0,0 +1,215 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sap-cds-search/cmd/model"
|
||||
"sap-cds-search/cmd/ui"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
)
|
||||
|
||||
type fuzzyResult struct {
|
||||
CDSViewTechnicalName string
|
||||
hits int
|
||||
averageDistance float32
|
||||
score float32
|
||||
}
|
||||
|
||||
type resultBuffer struct {
|
||||
createdAt time.Time
|
||||
hits int
|
||||
mu sync.Mutex
|
||||
results []fuzzyResult
|
||||
}
|
||||
|
||||
const pageStep = 12
|
||||
|
||||
var (
|
||||
ErrNoBuffer = errors.New("no buffer")
|
||||
ErrNoResults = errors.New("no results")
|
||||
)
|
||||
|
||||
var searchTargets map[string][]string = make(map[string][]string)
|
||||
var resultsBuffer map[string]*resultBuffer = make(map[string]*resultBuffer)
|
||||
|
||||
func Load() error {
|
||||
keywords, err := model.GetKeywordsModel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, keyword := range *keywords {
|
||||
table := searchTargets[keyword.CDSViewTechnicalName]
|
||||
table = append(table, keyword.Keyword)
|
||||
searchTargets[keyword.CDSViewTechnicalName] = table
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ClearOldBuffers()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSearchTerm(r *http.Request) string {
|
||||
return strings.ToLower(r.URL.Query().Get("q"))
|
||||
}
|
||||
|
||||
func GetPage(r *http.Request) int {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("p"))
|
||||
if page < 0 {
|
||||
page = 0
|
||||
}
|
||||
return page
|
||||
}
|
||||
|
||||
func GetBuffer(searchTerm string) (*resultBuffer, error) {
|
||||
buffer, exists := resultsBuffer[searchTerm]
|
||||
|
||||
if !exists {
|
||||
return nil, ErrNoBuffer
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
func BuildBuffer(searchTerm string) error {
|
||||
if searchTerm == "" {
|
||||
return ErrNoResults
|
||||
}
|
||||
|
||||
splitSearchTerms := strings.Split(searchTerm, " ")
|
||||
|
||||
splitSearchTerms = append(splitSearchTerms, searchTerm)
|
||||
splitSearchTerms = append(splitSearchTerms, strings.ReplaceAll(searchTerm, " ", searchTerm))
|
||||
|
||||
var fuzzyResults []fuzzyResult
|
||||
|
||||
for viewName, targets := range searchTargets {
|
||||
totalHits := 0
|
||||
var totalDistance float32
|
||||
for i, splitSearchTerm := range splitSearchTerms {
|
||||
ranks := fuzzy.RankFindFold(splitSearchTerm, targets)
|
||||
|
||||
hits := len(ranks)
|
||||
if hits == 0 {
|
||||
if i < len(splitSearchTerm)-3 {
|
||||
totalDistance += 500
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
totalHits += hits
|
||||
|
||||
var averageDistance float32 = 0
|
||||
for _, rank := range ranks {
|
||||
averageDistance += float32(rank.Distance)
|
||||
}
|
||||
|
||||
totalDistance += averageDistance / float32(hits)
|
||||
}
|
||||
|
||||
if totalHits > 0 {
|
||||
fuzzyResults = append(fuzzyResults, fuzzyResult{
|
||||
CDSViewTechnicalName: viewName,
|
||||
hits: totalHits,
|
||||
averageDistance: totalDistance,
|
||||
score: totalDistance / float32(totalHits),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hits := len(fuzzyResults)
|
||||
if hits == 0 {
|
||||
return ErrNoResults
|
||||
}
|
||||
|
||||
slices.SortFunc(fuzzyResults, func(a, b fuzzyResult) int {
|
||||
if a.score > b.score {
|
||||
return 1
|
||||
} else if a.score < b.score {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
resultsBuffer[searchTerm] = &resultBuffer{
|
||||
createdAt: time.Now(),
|
||||
hits: hits,
|
||||
results: fuzzyResults,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
searchTerm := GetSearchTerm(r)
|
||||
|
||||
buffer, err := GetBuffer(searchTerm)
|
||||
if err != nil {
|
||||
err := BuildBuffer(searchTerm)
|
||||
if err != nil {
|
||||
err := ui.Template.ExecuteTemplate(w, "search-placeholder-no-result", nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
buffer, _ = GetBuffer(searchTerm)
|
||||
}
|
||||
|
||||
buffer.mu.Lock()
|
||||
defer buffer.mu.Unlock()
|
||||
|
||||
page := GetPage(r)
|
||||
|
||||
maxPage := (buffer.hits / pageStep) - 1
|
||||
if page+1 > maxPage {
|
||||
page = maxPage
|
||||
}
|
||||
|
||||
resultsModel := model.NewResultsModel()
|
||||
for i := page * pageStep; i < page*pageStep+pageStep; i++ {
|
||||
result := buffer.results[i]
|
||||
cdsView, err := model.GetCDSViewModel(result.CDSViewTechnicalName)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
resultsModel.AppendResultsModelViews(cdsView)
|
||||
}
|
||||
|
||||
resultsModel.SearchTerm = searchTerm
|
||||
resultsModel.CurrentPage = page
|
||||
resultsModel.MaxPage = maxPage
|
||||
|
||||
err = ui.Template.ExecuteTemplate(w, "results", resultsModel)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ClearOldBuffers() {
|
||||
for key, buffer := range resultsBuffer {
|
||||
if time.Since(buffer.createdAt) >= time.Minute*10 {
|
||||
buffer.mu.Lock()
|
||||
delete(resultsBuffer, key)
|
||||
buffer.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
511
cmd/ui/css/styles.css
Normal file
511
cmd/ui/css/styles.css
Normal file
@ -0,0 +1,511 @@
|
||||
:root {
|
||||
--base_length: 35px;
|
||||
--base_margin: 15px;
|
||||
--radius_large: 12px;
|
||||
--shadow: 0px 3px 5px #0003;
|
||||
|
||||
--accent: #78aeed;
|
||||
|
||||
--blue-1: #99c1f1;
|
||||
--blue-2: #62a0ea;
|
||||
--blue-3: #3584e4;
|
||||
--blue-4: #1c71d8;
|
||||
--blue-5: #1a5fb4;
|
||||
|
||||
--green-3: #33d17a;
|
||||
--green-4: #2ec27e;
|
||||
--green-5: #26a269;
|
||||
|
||||
--yellow-4: #f5c211;
|
||||
--yellow-5: #e5a50a;
|
||||
|
||||
--default-margin: 8px var(--base_margin) 8px var(--base_margin);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--success: var(--green-4);
|
||||
--warning: var(--yellow-5);
|
||||
--border: #ffffff17;
|
||||
|
||||
--brightness-mixin: #fff;
|
||||
--brightness-hover: 110%;
|
||||
--brightness-active: 130%;
|
||||
|
||||
--background: #242424;
|
||||
--on_background: #fff;
|
||||
|
||||
--card: #363636;
|
||||
--on_card: #fff;
|
||||
|
||||
--osd: #000;
|
||||
--on_osd: #fff;
|
||||
|
||||
--raised: #4a4a4a;
|
||||
--on_raised: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--success: var(--green-5);
|
||||
--warning: var(--yellow-4);
|
||||
|
||||
--border: #0000001f;
|
||||
|
||||
--brightness-mixin: #000;
|
||||
--brightness-hover: 90%;
|
||||
--brightness-active: 70%;
|
||||
|
||||
--background: #fafafa;
|
||||
--on_background: #323232;
|
||||
|
||||
--card: #fff;
|
||||
--on_card: #333333;
|
||||
|
||||
--osd: #4c4c4c;
|
||||
--on_osd: #fff;
|
||||
|
||||
--raised: #ebebeb;
|
||||
--on_raised: #2f2f2f;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
"Open Sans",
|
||||
"Helvetica Neue",
|
||||
sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--on_background);
|
||||
}
|
||||
|
||||
.min-w2 {
|
||||
min-width: calc(2 * var(--base_length));
|
||||
}
|
||||
|
||||
.min-w3 {
|
||||
min-width: calc(3 * var(--base_length));
|
||||
}
|
||||
|
||||
.min-w5 {
|
||||
min-width: calc(5 * var(--base_length));
|
||||
}
|
||||
|
||||
.min-w6 {
|
||||
min-width: calc(6 * var(--base_length));
|
||||
}
|
||||
|
||||
.min-w8 {
|
||||
min-width: calc(8 * var(--base_length));
|
||||
}
|
||||
|
||||
.min-w10 {
|
||||
min-width: calc(10 * var(--base_length));
|
||||
}
|
||||
|
||||
.min-w12 {
|
||||
min-width: calc(12 * var(--base_length));
|
||||
}
|
||||
|
||||
.min-w14 {
|
||||
min-width: calc(14 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w1 {
|
||||
max-width: calc(1 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w2 {
|
||||
max-width: calc(2 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w3 {
|
||||
max-width: calc(3 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w6 {
|
||||
max-width: calc(6 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w8 {
|
||||
max-width: calc(8 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w10 {
|
||||
max-width: calc(10 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w16 {
|
||||
max-width: calc(16 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w21 {
|
||||
max-width: calc(20 * var(--base_length));
|
||||
}
|
||||
|
||||
.max-w24 {
|
||||
max-width: calc(24 * var(--base_length));
|
||||
}
|
||||
|
||||
.boxed-list {
|
||||
background-color: var(--card);
|
||||
color: var(--on_card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
border-radius: var(--radius_large);
|
||||
min-width: fit-content !important;
|
||||
border: 1px;
|
||||
border-color: var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.boxed-list > * {
|
||||
width: 100%;
|
||||
display: inline;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.boxed-list > * > * {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.boxed-list > * > .title {
|
||||
padding-right: 15px;
|
||||
margin-left: 15px;
|
||||
margin-right: auto;
|
||||
color: var(--on_card);
|
||||
}
|
||||
|
||||
.boxed-list > * > .title > * {
|
||||
color: color-mix(in srgb, var(--on_card) 50%, var(--card) 50%);
|
||||
}
|
||||
|
||||
.boxed-list > * > .prefix {
|
||||
margin: 0px 0px 0px var(--base_margin);
|
||||
}
|
||||
|
||||
.boxed-list > * > .suffix {
|
||||
margin: 0px var(--base_margin) 0px var(--base_margin);
|
||||
}
|
||||
|
||||
.boxed-list > *:not(:last-child):not(.expander),
|
||||
.boxed-list > .expander:has(> .expander-state:checked),
|
||||
.boxed-list
|
||||
> .expander:not(:has(> .expander-state:checked)):has(
|
||||
+ .expander-content:not(:last-child)
|
||||
) {
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.boxed-list > *:not(:first-child) {
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
}
|
||||
|
||||
.boxed-list > *:focus {
|
||||
z-index: 1;
|
||||
outline: solid blue 2px;
|
||||
}
|
||||
|
||||
.boxed-list > * {
|
||||
height: 55px;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.boxed-list > .active:hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--card) 98%,
|
||||
var(--brightness-mixin) 2%
|
||||
);
|
||||
}
|
||||
|
||||
.boxed-list > .active:active {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--card) 95%,
|
||||
var(--brightness-mixin) 5%
|
||||
);
|
||||
}
|
||||
|
||||
.boxed-list > * > .prefix {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.round {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.pill {
|
||||
border-radius: 1000px;
|
||||
}
|
||||
|
||||
.raised {
|
||||
background-color: var(--raised);
|
||||
color: var(--on_raised);
|
||||
}
|
||||
|
||||
.flat {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.suggested-action {
|
||||
background-color: var(--blue-3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: color-mix(in srgb, currentColor 15%, transparent);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: color-mix(in srgb, currentColor 15%, transparent);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
}
|
||||
|
||||
button {
|
||||
height: var(--base_length);
|
||||
width: inherit;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
filter 0.15s,
|
||||
outline-offset 0.1s ease-in-out,
|
||||
outline-color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(var(--brightness-hover));
|
||||
}
|
||||
|
||||
button:active {
|
||||
filter: brightness(var(--brightness-active));
|
||||
}
|
||||
|
||||
* {
|
||||
outline-offset: 1px;
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
outline-color: transparent;
|
||||
transition:
|
||||
outline-offset 0.1s ease-in-out,
|
||||
outline-color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline-offset: -1px;
|
||||
outline-color: color-mix(in srgb, var(--accent) 50%, transparent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.adw-radio input[type="radio"] {
|
||||
appearance: none;
|
||||
height: calc(var(--base_length) * 0.6);
|
||||
width: calc(var(--base_length) * 0.6);
|
||||
border-radius: 50%;
|
||||
border: 2px var(--raised) solid;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.adw-radio input[type="radio"]:checked {
|
||||
border: 2px var(--blue-3) solid;
|
||||
background-color: var(--blue-3);
|
||||
}
|
||||
|
||||
.adw-radio input[type="radio"]:before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
margin: 20% auto;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.adw-radio input[type="radio"]:checked:before {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card {
|
||||
overflow: hidden;
|
||||
margin: var(--base_margin);
|
||||
border-radius: var(--radius_large);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--on_card);
|
||||
background-color: var(--card);
|
||||
box-shadow: var(--shadow);
|
||||
margin: var(--base_length);
|
||||
}
|
||||
|
||||
.navigation {
|
||||
min-width: fit-content;
|
||||
max-height: calc(var(--base_length) * 1.5);
|
||||
width: 80%;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
background-color: var(--background);
|
||||
color: var(--on_background);
|
||||
border-radius: var(--radius_large);
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.navigation > * {
|
||||
margin: 10px;
|
||||
max-height: 32px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.tag {
|
||||
height: var(--base_length);
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: var(--default-margin);
|
||||
font-size: var(--base_margin);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: bold;
|
||||
font-size: var(--base_margin);
|
||||
}
|
||||
|
||||
.title-1 {
|
||||
font-size: x-large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.expander-state {
|
||||
position: relative;
|
||||
width: 0%;
|
||||
height: 0%;
|
||||
margin: 0%;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.boxed-list > .expander {
|
||||
border-bottom-color: transparent !important;
|
||||
transition: border-bottom 0.15s steps(1, end);
|
||||
}
|
||||
|
||||
.expander:has(> .expander-state:checked) {
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
transition: border-bottom 0.15s steps(1, start);
|
||||
}
|
||||
|
||||
.expander-content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
max-height: 0;
|
||||
transition: max-height 0.15s ease-in-out;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
.expander:has(> .expander-state:checked) + .expander-content {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.container-center {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: var(--default-margin);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
color: color-mix(in srgb, var(--on_card) 50%, var(--card) 50%);
|
||||
font-size: var(--base_margin);
|
||||
}
|
||||
|
||||
.table-field-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-field-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.monospaced {
|
||||
font-family: monospace;
|
||||
font-size: var(--base_margin);
|
||||
}
|
||||
|
||||
.sans-serif {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.placeholder-image {
|
||||
color: color-mix(in srgb, currentColor 60%, transparent);
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
mask-position: center;
|
||||
background-color: currentColor;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100%;
|
||||
margin: var(--base_margin);
|
||||
}
|
||||
|
||||
.placeholder-image.loupe {
|
||||
mask-image: url("/ui/svg/loupe.svg");
|
||||
}
|
||||
|
||||
.linked-horizontal > *:not(:first-child) {
|
||||
margin-left: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
}
|
||||
.linked-horizontal > *:not(:last-child) {
|
||||
margin-right: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
|
||||
input {
|
||||
color: var(--on_background);
|
||||
background-color: color-mix(in srgb, currentColor 10%, transparent);
|
||||
min-height: var(--base_length);
|
||||
padding-left: var(--base_margin);
|
||||
margin: var(--default-margin);
|
||||
font-size: 15px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: var(--default-margin);
|
||||
position: relative;
|
||||
}
|
38
cmd/ui/html/main.html
Normal file
38
cmd/ui/html/main.html
Normal file
@ -0,0 +1,38 @@
|
||||
{{block "main" .}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/ui/css/styles.css" />
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<title>SAP CDS-View Search</title>
|
||||
</head>
|
||||
<body>
|
||||
{{template "search-bar" .}}
|
||||
<div id="search-results" class="container-center">
|
||||
{{template "search-placeholder-info" . }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}} {{block "search-bar" .}}
|
||||
<form class="container-center linked-horizontal" style="margin-bottom: 30px">
|
||||
<input
|
||||
class="round min-w4 max-w21"
|
||||
style="flex-grow: 1"
|
||||
placeholder="Search here"
|
||||
id="search-bar"
|
||||
name="q"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="round raised suggested-action max-w3"
|
||||
style="flex-grow: 1"
|
||||
hx-get="/search"
|
||||
hx-params="*"
|
||||
hx-include="#search-bar"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
126
cmd/ui/html/search-result.html
Normal file
126
cmd/ui/html/search-result.html
Normal file
@ -0,0 +1,126 @@
|
||||
{{block "results" .}}
|
||||
<div>
|
||||
{{template "paging" .}}
|
||||
<div class="boxed-list max-w24">
|
||||
{{range .Views}} {{template "result" .}} {{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{end}} {{block "result" .}}
|
||||
<label class="active expander" for="{{.TechnicalName}}">
|
||||
<input class="expander-state" type="checkbox" id="{{.TechnicalName}}" />
|
||||
<div class="title sans-serif">
|
||||
{{.DisplayName}}
|
||||
<div class="monospaced">{{.TechnicalName}}</div>
|
||||
</div>
|
||||
<div
|
||||
class='tag pill suffix {{if eq .State "RELEASED"}} success {{else}} warning {{end}} min-w3'
|
||||
>
|
||||
{{.StateTitle}}
|
||||
</div>
|
||||
</label>
|
||||
<div class="expander-content">
|
||||
<div class="label heading">Field Properties</div>
|
||||
<div class="container-center">{{template "fields" .Fields}}</div>
|
||||
</div>
|
||||
{{end}} {{block "fields" .}}
|
||||
<table>
|
||||
<tr>
|
||||
<th class="table-field-left">Name</th>
|
||||
<th class="table-field-left">Description</th>
|
||||
<th class="table-field-left">Type</th>
|
||||
<th class="table-field-right">Length</th>
|
||||
</tr>
|
||||
{{range .}} {{template "field" .}} {{end}}
|
||||
</table>
|
||||
{{end}} {{block "field" .}}
|
||||
<tr>
|
||||
<td class="table-field-left monospaced">{{.FieldName}}</td>
|
||||
<td class="table-field-left">{{.DescriptionOut}}</td>
|
||||
<td class="table-field-left">{{.DataTypeTitle}}</td>
|
||||
<td class="table-field-right monospaced">{{.FieldLengthOut}}</td>
|
||||
</tr>
|
||||
{{end}} {{block "search-placeholder-no-result" .}}
|
||||
<div>
|
||||
<div class="container-center">
|
||||
<div class="placeholder-image loupe"></div>
|
||||
</div>
|
||||
<div class="container-center">
|
||||
<div class="label title-1">No Results Found</div>
|
||||
</div>
|
||||
<div class="container-center">
|
||||
<div class="label">Try refining your search term</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}} {{block "search-placeholder-info" .}}
|
||||
<div>
|
||||
<div class="container-center">
|
||||
<div class="placeholder-image loupe"></div>
|
||||
</div>
|
||||
<div class="container-center">
|
||||
<div class="label title-1">Search CDS-Views</div>
|
||||
</div>
|
||||
<div class="container-center">
|
||||
<div class="label">Find CDS-Views from the Business Acceleator Hub</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}} {{block "paging" .}}
|
||||
<div>
|
||||
<form style="float: right">
|
||||
<input name="q" disabled hidden value="{{.SearchTerm}}" />
|
||||
<div class="linked-horizontal">
|
||||
<!--button
|
||||
type="submit"
|
||||
class="raised round"
|
||||
style="margin-top: 9px"
|
||||
hx-get="/search"
|
||||
hx-params="*"
|
||||
hx-include="#search-bar"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML"
|
||||
{{if
|
||||
eq
|
||||
.CurrentPage
|
||||
0}}
|
||||
disabled
|
||||
{{end}}
|
||||
>
|
||||
←</button
|
||||
--><input
|
||||
class="round max-w2"
|
||||
type="number"
|
||||
name="p"
|
||||
value="{{.CurrentPage}}"
|
||||
min="0"
|
||||
max="{{.MaxPage}}"
|
||||
hx-get="/search"
|
||||
hx-params="*"
|
||||
hx-include="#search-bar"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML"
|
||||
/><input
|
||||
class="round raised max-w2"
|
||||
value="{{.MaxPage}}"
|
||||
disabled
|
||||
/><!--button
|
||||
type="submit"
|
||||
class="raised round"
|
||||
style="margin-top: 9px"
|
||||
hx-get="/search"
|
||||
hx-params="*"
|
||||
hx-include="#search-bar"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML"
|
||||
{{if
|
||||
eq
|
||||
.CurrentPage
|
||||
.MaxPage}}
|
||||
disabled
|
||||
{{end}}
|
||||
>
|
||||
→
|
||||
</button-->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
2
cmd/ui/svg/loupe.svg
Normal file
2
cmd/ui/svg/loupe.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 10.875 10.0625 c -0.8125 0.148438 -1.105469 1.160156 -0.5 1.71875 l 3 3 c 0.957031 0.9375 2.363281 -0.5 1.40625 -1.4375 l -3 -3 c -0.234375 -0.238281 -0.574219 -0.347656 -0.90625 -0.28125 z m 0 0"/><path d="m 6.570312 0.0625 c -3.578124 0 -6.4999995 2.921875 -6.4999995 6.5 s 2.9218755 6.5 6.4999995 6.5 c 3.578126 0 6.5 -2.921875 6.5 -6.5 s -2.921874 -6.5 -6.5 -6.5 z m 0 2 c 2.5 0 4.5 2.003906 4.5 4.5 c 0 2.5 -2 4.5 -4.5 4.5 c -2.496093 0 -4.5 -2 -4.5 -4.5 c 0 -2.496094 2.003907 -4.5 4.5 -4.5 z m 0 0"/></g></svg>
|
After Width: | Height: | Size: 672 B |
12
cmd/ui/ui.go
Normal file
12
cmd/ui/ui.go
Normal file
@ -0,0 +1,12 @@
|
||||
package ui
|
||||
|
||||
import "text/template"
|
||||
|
||||
var Template *template.Template
|
||||
|
||||
func Load() {
|
||||
Template = template.Must(template.New("ui").ParseFiles(
|
||||
"./cmd/ui/html/main.html",
|
||||
"./cmd/ui/html/search-result.html",
|
||||
))
|
||||
}
|
22
go.mod
Normal file
22
go.mod
Normal file
@ -0,0 +1,22 @@
|
||||
module sap-cds-search
|
||||
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/go-chi/chi v1.5.5
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
modernc.org/libc v1.37.6 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.28.0 // indirect
|
||||
)
|
59
go.sum
Normal file
59
go.sum
Normal file
@ -0,0 +1,59 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
|
||||
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
24
main.go
Normal file
24
main.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sap-cds-search/cmd/database"
|
||||
"sap-cds-search/cmd/router"
|
||||
"sap-cds-search/cmd/search"
|
||||
"sap-cds-search/cmd/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
database.Load()
|
||||
|
||||
err := search.Load()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ui.Load()
|
||||
r := router.Load()
|
||||
|
||||
fmt.Println("Listening on :8080")
|
||||
http.ListenAndServe(":8080", r)
|
||||
}
|
Loading…
Reference in New Issue
Block a user