Compare commits

...

11 Commits
main ... pas-ui

Author SHA1 Message Date
4e02bf9c2e Fix CDS State Batch Name 2025-05-23 21:22:19 +02:00
d332e83f29 Show CDS State 2025-05-23 21:18:05 +02:00
bc79e0ac2c Correctly Place Pager 2025-05-23 19:54:44 +02:00
3e07e2f06e Fix Locking 2025-05-23 19:48:22 +02:00
f8ab5c8358 Print ListenAndServe Error 2025-05-22 14:08:46 +02:00
f4f3a4b304 Scroll Expander Content 2025-05-20 21:44:09 +02:00
005b47d0ee Update PAS UI 2025-05-20 21:41:08 +02:00
03156ff06d Update PAS UI 2025-05-20 21:26:20 +02:00
7adc97c1c9 Fix Building For Docker 2025-05-20 21:09:14 +02:00
22181fb56b Go Tidy 2025-05-20 20:50:56 +02:00
6b8ae2f236 Use PAS UI 2025-05-20 20:41:26 +02:00
21 changed files with 639 additions and 108 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
_* _*
*.sqlite *.sqlite
api-cds-search

View File

@ -5,8 +5,5 @@ FROM golang:latest
WORKDIR /app WORKDIR /app
ADD . /app ADD . /app
RUN go mod download
RUN go build -o ./api-cds-search
EXPOSE 8080 EXPOSE 8080
CMD ["./api-cds-search"] CMD ["./api-cds-search"]

3
build.sh Executable file
View File

@ -0,0 +1,3 @@
go build -o ./api-cds-search
sudo docker build -t code.achtarmig.org/sap/api-cds-search:pas-ui-latest .

View File

@ -33,9 +33,9 @@ type CDSViewFieldJSON struct {
//TODO: "https://api.sap.com/api/1.0/container/CDSViews/artifacts?containerType=contentType&$filter=Type%20eq%20'CDSVIEW'&$top=" + fmt.Sprint(top) + "&$skip=" + fmt.Sprint(skip) //TODO: "https://api.sap.com/api/1.0/container/CDSViews/artifacts?containerType=contentType&$filter=Type%20eq%20'CDSVIEW'&$top=" + fmt.Sprint(top) + "&$skip=" + fmt.Sprint(skip)
func GetCDSViewField(CDSViewTechnicalName string) error { func GetCDSViewField(CDSViewTechnicalName table.TechnicalName) error {
var json CDSViewFieldJSON var json CDSViewFieldJSON
path := "https://api.sap.com/odata/1.0/catalog.svc/CdsViewsContent.CdsViews('" + CDSViewTechnicalName + "')/$value" path := string("https://api.sap.com/odata/1.0/catalog.svc/CdsViewsContent.CdsViews('" + CDSViewTechnicalName + "')/$value")
fmt.Println("Getting: ", CDSViewTechnicalName) fmt.Println("Getting: ", CDSViewTechnicalName)
err := RequestJSON(path, &json) err := RequestJSON(path, &json)
if err != nil { if err != nil {
@ -60,7 +60,7 @@ func GetCDSViewFields() {
for _, cdsView := range *cdsViews { for _, cdsView := range *cdsViews {
var json CDSViewFieldJSON var json CDSViewFieldJSON
path := "https://api.sap.com/odata/1.0/catalog.svc/CdsViewsContent.CdsViews('" + cdsView + "')/$value" path := string("https://api.sap.com/odata/1.0/catalog.svc/CdsViewsContent.CdsViews('" + cdsView + "')/$value")
fmt.Println("Getting: ", cdsView) fmt.Println("Getting: ", cdsView)
err := RequestJSON(path, &json) err := RequestJSON(path, &json)
if err != nil { if err != nil {

View File

@ -23,19 +23,19 @@ import (
) )
type CDSView struct { type CDSView struct {
TechnicalName string `json:"Name"` TechnicalName `json:"Name"`
DisplayName string `json:"DisplayName"` DisplayName `json:"DisplayName"`
Description string `json:"Description"` Description `json:"Description"`
Version string `json:"Version"` Version `json:"Version"`
State string `json:"State"` State `json:"State"`
CreatedAt int `json:"CreatedAt"` CreatedAt `json:"CreatedAt"`
ModifiedAt int `json:"ModifiedAt"` ModifiedAt `json:"ModifiedAt"`
} }
//go:embed sql/query_cds_view.sql //go:embed sql/query_cds_view.sql
var query_cds_view string var query_cds_view string
func GetCDSView(TechnicalName string) (*CDSView, error) { func GetCDSView(TechnicalName TechnicalName) (*CDSView, error) {
row := database.DB.QueryRow(query_cds_view, TechnicalName) row := database.DB.QueryRow(query_cds_view, TechnicalName)
var CDSView CDSView var CDSView CDSView
@ -50,15 +50,15 @@ func GetCDSView(TechnicalName string) (*CDSView, error) {
//go:embed sql/query_all_cds_view_technical_names.sql //go:embed sql/query_all_cds_view_technical_names.sql
var query_all_cds_view_technical_names string var query_all_cds_view_technical_names string
func QueryAllCDSViewTechnicalNames() (*[]string, error) { func QueryAllCDSViewTechnicalNames() (*[]TechnicalName, error) {
rows, err := database.DB.Query(query_all_cds_view_technical_names) rows, err := database.DB.Query(query_all_cds_view_technical_names)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var technicalNames []string var technicalNames []TechnicalName
for rows.Next() { for rows.Next() {
var technicalName string var technicalName TechnicalName
err := rows.Scan(&technicalName) err := rows.Scan(&technicalName)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -23,11 +23,11 @@ import (
) )
type CDSViewField struct { type CDSViewField struct {
CDSViewTechnicalName string CDSViewTechnicalName TechnicalName
FieldName string `json:"fieldname"` FieldName `json:"fieldname"`
Description string `json:"description"` Description `json:"description"`
DataType string `json:"datatype"` DataType `json:"datatype"`
FieldLength string `json:"fieldlength"` FieldLength `json:"fieldlength"`
} }
//go:embed sql/query_cds_view_fields.sql //go:embed sql/query_cds_view_fields.sql
@ -53,7 +53,7 @@ func GetCDSViewFields(CDSViewTechnicalName string) (*[]CDSViewField, error) {
//go:embed sql/query_cds_view_number_of_fields.sql //go:embed sql/query_cds_view_number_of_fields.sql
var query_cds_view_number_of_fields string var query_cds_view_number_of_fields string
func GetCDSViewNumberOfFields(CDSViewTechnicalName string) (int, error) { func GetCDSViewNumberOfFields(CDSViewTechnicalName TechnicalName) (int, error) {
rows, err := database.DB.Query(query_cds_view_number_of_fields, CDSViewTechnicalName) rows, err := database.DB.Query(query_cds_view_number_of_fields, CDSViewTechnicalName)
if err != nil { if err != nil {
return 0, err return 0, err

135
cmd/database/table/field.go Normal file
View File

@ -0,0 +1,135 @@
package table
import "code.achtarmig.org/pas/ui/field"
// CDS View
type (
TechnicalName string
DisplayName string
Description string
Version string
State string
CreatedAt int
ModifiedAt int
)
// CDS View Field
type (
FieldName string
DataType string
FieldLength string
)
func (t TechnicalName) Info(language string) field.Info {
return field.Info{
Title: "Technical Name",
Details: "Technical Name of the CDS View",
}
}
func (t TechnicalName) String() string {
return string(t)
}
func (t DisplayName) Info(language string) field.Info {
return field.Info{
Title: "Name",
Details: "Name of the CDS View",
}
}
func (t DisplayName) String() string {
return string(t)
}
func (t Description) Info(language string) field.Info {
return field.Info{
Title: "Description",
Details: "Discription",
}
}
func (t Description) String() string {
return string(t)
}
func (t Version) Info(language string) field.Info {
return field.Info{
Title: "Version",
Details: "Version of the CDS View",
Justify: field.JustifyRight,
}
}
func (t Version) String() string {
return string(t)
}
func (t State) Info(language string) field.Info {
return field.Info{
Title: "Release State",
Details: "Release State of the CDS View. Non-Released views may not be used",
}
}
func (t State) String() string {
return string(t)
}
func (t CreatedAt) Info(language string) field.Info {
return field.Info{
Title: "Created At",
Details: "Timestamp of creation of the dataset",
Justify: field.JustifyRight,
}
}
func (t CreatedAt) Int() int {
return int(t)
}
func (t ModifiedAt) Info(language string) field.Info {
return field.Info{
Title: "Modified At",
Details: "Timestamp of last modification of the dataset",
Justify: field.JustifyRight,
}
}
func (t ModifiedAt) Int() int {
return int(t)
}
func (t FieldName) Info(language string) field.Info {
return field.Info{
Title: "Field",
Details: "Name of the CDS View field",
}
}
func (t FieldName) String() string {
return string(t)
}
func (t DataType) Info(language string) field.Info {
return field.Info{
Title: "Type",
Details: "Data type of the CDS View field",
}
}
func (t DataType) String() string {
return string(t)
}
func (t FieldLength) Info(language string) field.Info {
return field.Info{
Title: "Length",
Details: "Length of the CDS View field",
Justify: field.JustifyRight,
}
}
func (t FieldLength) String() string {
return string(t)
}

View File

@ -23,7 +23,7 @@ import (
) )
type Keyword struct { type Keyword struct {
CDSViewTechnicalName string CDSViewTechnicalName TechnicalName
Keyword string Keyword string
} }

View File

@ -29,15 +29,16 @@ import (
type CDSViewFieldModel struct { type CDSViewFieldModel struct {
table.CDSViewField table.CDSViewField
DataTypeTitle string FieldNameOut table.FieldName
FieldLengthOut string DataTypeTitle table.DataType
DescriptionOut string FieldLengthOut table.FieldLength
DescriptionOut table.Description
} }
type CDSViewModel struct { type CDSViewModel struct {
*table.CDSView *table.CDSView
StateTitle string StateTitle table.State
TechnicalNameEncoded string TechnicalNameEncoded table.TechnicalName
NumberOfFields int NumberOfFields int
} }
@ -53,8 +54,9 @@ func GetCDSViewModelFields(TechnicalName string) (*[]CDSViewFieldModel, error) {
var fieldsModel []CDSViewFieldModel var fieldsModel []CDSViewFieldModel
for _, field := range *fields { for _, field := range *fields {
fieldModel.CDSViewField = field fieldModel.CDSViewField = field
fieldModel.DataTypeTitle = englishCases.String(field.DataType) fieldModel.FieldNameOut = field.FieldName
fieldModel.FieldLengthOut = strings.TrimLeft(field.FieldLength, "0") fieldModel.DataTypeTitle = table.DataType(englishCases.String(field.DataType.String()))
fieldModel.FieldLengthOut = table.FieldLength(strings.TrimLeft(field.FieldLength.String(), "0"))
fieldModel.DescriptionOut = field.Description fieldModel.DescriptionOut = field.Description
if fieldModel.DescriptionOut == "" { if fieldModel.DescriptionOut == "" {
fieldModel.DescriptionOut = "-" fieldModel.DescriptionOut = "-"
@ -65,7 +67,7 @@ func GetCDSViewModelFields(TechnicalName string) (*[]CDSViewFieldModel, error) {
return &fieldsModel, nil return &fieldsModel, nil
} }
func GetCDSViewModel(TechnicalName string) (*CDSViewModel, error) { func GetCDSViewModel(TechnicalName table.TechnicalName) (*CDSViewModel, error) {
var model CDSViewModel var model CDSViewModel
cdsView, err := table.GetCDSView(TechnicalName) cdsView, err := table.GetCDSView(TechnicalName)
@ -75,8 +77,8 @@ func GetCDSViewModel(TechnicalName string) (*CDSViewModel, error) {
model.CDSView = cdsView model.CDSView = cdsView
model.StateTitle = englishCases.String(model.State) model.StateTitle = table.State(englishCases.String(model.State.String()))
model.TechnicalNameEncoded = strings.Replace(base64.StdEncoding.EncodeToString([]byte(model.TechnicalName)), "=", "", -1) model.TechnicalNameEncoded = table.TechnicalName(strings.Replace(base64.StdEncoding.EncodeToString([]byte(model.TechnicalName)), "=", "", -1))
model.NumberOfFields, err = table.GetCDSViewNumberOfFields(TechnicalName) model.NumberOfFields, err = table.GetCDSViewNumberOfFields(TechnicalName)
if err != nil { if err != nil {

View File

@ -18,37 +18,23 @@
package router package router
import ( import (
"api-cds-search/cmd/handler"
"api-cds-search/cmd/search"
"api-cds-search/cmd/ui"
"fmt"
"net/http" "net/http"
"code.achtarmig.org/pas/ui/context"
"code.achtarmig.org/pas/ui/route"
"github.com/go-chi/chi" "github.com/go-chi/chi"
) )
func Load() *chi.Mux { func Load() *chi.Mux {
r := chi.NewRouter() r := chi.NewRouter()
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
ui.Load() r.Use(context.Middleware)
r.Get("/", handleRoot) route.Routes(r)
r.Route("/search", func(r chi.Router) { })
r.Get("/", search.HandleSearch)
}) r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
r.Route("/cds", func(r chi.Router) { http.Redirect(w, r, "/search", http.StatusFound)
r.Get("/field/", handler.GetCDSField)
})
}) })
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 return r
} }
func handleRoot(w http.ResponseWriter, r *http.Request) {
err := ui.Template.ExecuteTemplate(w, "main", nil)
if err != nil {
fmt.Println(err)
return
}
}

View File

@ -18,8 +18,8 @@
package search package search
import ( import (
"api-cds-search/cmd/database/table"
"api-cds-search/cmd/model" "api-cds-search/cmd/model"
"api-cds-search/cmd/ui"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -33,7 +33,7 @@ import (
) )
type fuzzyResult struct { type fuzzyResult struct {
CDSViewTechnicalName string CDSViewTechnicalName table.TechnicalName
hits int hits int
perfectHits int perfectHits int
averageDistance float32 averageDistance float32
@ -43,7 +43,6 @@ type fuzzyResult struct {
type resultBuffer struct { type resultBuffer struct {
createdAt time.Time createdAt time.Time
hits int hits int
mu sync.Mutex
results []fuzzyResult results []fuzzyResult
} }
@ -54,8 +53,9 @@ var (
ErrNoResults = errors.New("no results") ErrNoResults = errors.New("no results")
) )
var searchTargets map[string][]string = make(map[string][]string) var searchTargets map[table.TechnicalName][]string = make(map[table.TechnicalName][]string)
var resultsBuffer map[string]*resultBuffer = make(map[string]*resultBuffer) var resultsBuffer map[string]*resultBuffer = make(map[string]*resultBuffer)
var resultsBufferMu sync.Mutex
func Load() error { func Load() error {
keywords, err := model.GetKeywordsModel() keywords, err := model.GetKeywordsModel()
@ -82,8 +82,8 @@ func Load() error {
return nil return nil
} }
func GetSearchTerm(r *http.Request) string { func GetSearchTerm(q string) string {
return strings.ToLower(r.URL.Query().Get("q")) return strings.ToLower(q)
} }
func GetPage(r *http.Request) int { func GetPage(r *http.Request) int {
@ -95,6 +95,9 @@ func GetPage(r *http.Request) int {
} }
func GetBuffer(searchTerm string) (*resultBuffer, error) { func GetBuffer(searchTerm string) (*resultBuffer, error) {
resultsBufferMu.Lock()
defer resultsBufferMu.Unlock()
buffer, exists := resultsBuffer[searchTerm] buffer, exists := resultsBuffer[searchTerm]
if !exists { if !exists {
@ -186,35 +189,18 @@ func BuildBuffer(searchTerm string) error {
return 0 return 0
}) })
resultsBufferMu.Lock()
resultsBuffer[searchTerm] = &resultBuffer{ resultsBuffer[searchTerm] = &resultBuffer{
createdAt: time.Now(), createdAt: time.Now(),
hits: hits, hits: hits,
results: fuzzyResults, results: fuzzyResults,
} }
resultsBufferMu.Unlock()
return nil return nil
} }
func HandleSearch(w http.ResponseWriter, r *http.Request) { func GetPages(buffer *resultBuffer, p int) (int, int) {
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)
var maxPage int var maxPage int
if buffer.hits <= pageStep { if buffer.hits <= pageStep {
maxPage = 0 maxPage = 0
@ -222,10 +208,15 @@ func HandleSearch(w http.ResponseWriter, r *http.Request) {
maxPage = (buffer.hits / pageStep) maxPage = (buffer.hits / pageStep)
} }
if page+1 > maxPage { if p+1 > maxPage {
page = maxPage return maxPage, maxPage
} else if p < 0 {
return 0, maxPage
} }
return p, maxPage
}
func Search(buffer *resultBuffer, page int, maxPage int) *model.ResultsModel {
resultsModel := model.NewResultsModel() resultsModel := model.NewResultsModel()
for i := page * pageStep; i < page*pageStep+pageStep; i++ { for i := page * pageStep; i < page*pageStep+pageStep; i++ {
if i > len(buffer.results)-1 { if i > len(buffer.results)-1 {
@ -241,23 +232,77 @@ func HandleSearch(w http.ResponseWriter, r *http.Request) {
resultsModel.AppendResultsModelViews(cdsView, result.score) resultsModel.AppendResultsModelViews(cdsView, result.score)
} }
resultsModel.SearchTerm = searchTerm
resultsModel.CurrentPage = page resultsModel.CurrentPage = page
resultsModel.MaxPage = maxPage resultsModel.MaxPage = maxPage
err = ui.Template.ExecuteTemplate(w, "results", resultsModel) return resultsModel
if err != nil {
fmt.Println(err)
return
}
} }
/*
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)
var maxPage int
if buffer.hits <= pageStep {
maxPage = 0
} else {
maxPage = (buffer.hits / pageStep)
}
if page+1 > maxPage {
page = maxPage
}
resultsModel := model.NewResultsModel()
for i := page * pageStep; i < page*pageStep+pageStep; i++ {
if i > len(buffer.results)-1 {
break
}
result := buffer.results[i]
cdsView, err := model.GetCDSViewModel(result.CDSViewTechnicalName)
if err != nil {
fmt.Println(err)
continue
}
resultsModel.AppendResultsModelViews(cdsView, result.score)
}
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() { func ClearOldBuffers() {
resultsBufferMu.Lock()
defer resultsBufferMu.Unlock()
for key, buffer := range resultsBuffer { for key, buffer := range resultsBuffer {
if time.Since(buffer.createdAt) >= time.Minute*10 { if time.Since(buffer.createdAt) >= time.Minute*10 {
buffer.mu.Lock()
delete(resultsBuffer, key) delete(resultsBuffer, key)
buffer.mu.Unlock()
} }
} }
} }

View File

@ -20,6 +20,12 @@
--yellow-5: #e5a50a; --yellow-5: #e5a50a;
--default-margin: 8px var(--base_margin) 8px var(--base_margin); --default-margin: 8px var(--base_margin) 8px var(--base_margin);
--default-outline: transparent solid 2px;
--default-outline-offset: 1px;
--transiton-outline:
outline-offset 0.1s ease-in-out, outline-color 0.1s ease-in-out;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@ -313,13 +319,9 @@ button:active {
} }
* { * {
outline-offset: 1px; outline-offset: var(--default-outline-offset);
outline-style: solid; outline: var(--default-outline);
outline-width: 2px; transition: var(--transiton-outline);
outline-color: transparent;
transition:
outline-offset 0.1s ease-in-out,
outline-color 0.1s ease-in-out;
} }
*:focus-visible { *:focus-visible {
@ -412,17 +414,33 @@ button:active {
width: 0%; width: 0%;
height: 0%; height: 0%;
margin: 0%; margin: 0%;
visibility: hidden; border: none;
outline: none;
} }
.boxed-list > .expander { .boxed-list > .expander {
border-bottom-color: transparent !important; border-bottom-color: transparent !important;
transition: border-bottom 0.15s steps(1, end); transition:
var(--transiton-outline),
border-bottom 0.15s steps(1, end),
border-bottom-right-radius 0.05s 0.1s,
border-bottom-left-radius 0.05s 0.1s;
}
.boxed-list > .active.expander:focus-within {
outline-offset: -1px;
outline-color: color-mix(in srgb, var(--accent) 50%, transparent);
z-index: 1;
} }
.expander:has(> .expander-state:checked) { .expander:has(> .expander-state:checked) {
border-bottom: 1px solid var(--border) !important; border-bottom: 1px solid var(--border) !important;
transition: border-bottom 0.15s steps(1, start); transition:
var(--transiton-outline),
border-bottom 0.15s steps(1, start),
border-bottom-right-radius 0.05s,
border-bottom-left-radius 0.05s;
transition-delay: 0s;
} }
.expander-content { .expander-content {

View File

@ -11,6 +11,7 @@
{{end}} {{block "result" .}} {{end}} {{block "result" .}}
<label class="active expander" for="{{.TechnicalNameEncoded}}"> <label class="active expander" for="{{.TechnicalNameEncoded}}">
<input <input
tabindex="0"
class="expander-state" class="expander-state"
type="checkbox" type="checkbox"
id="{{.TechnicalNameEncoded}}" id="{{.TechnicalNameEncoded}}"
@ -25,7 +26,7 @@
{{.StateTitle}} {{.StateTitle}}
</div> </div>
</label> </label>
<div class="expander-content"> <div class="expander-content" tabindex="none">
<div class="label heading">Field Properties</div> <div class="label heading">Field Properties</div>
<div class="container-center"> <div class="container-center">
{{template "fields-placeholder" dict "Result" .}} {{template "fields-placeholder" dict "Result" .}}

89
cmd/view/cdsDetails.go Normal file
View File

@ -0,0 +1,89 @@
package view
import (
"api-cds-search/cmd/model"
"fmt"
"net/http"
db_table "api-cds-search/cmd/database/table"
"code.achtarmig.org/pas/ui/element"
"code.achtarmig.org/pas/ui/element/container"
"code.achtarmig.org/pas/ui/element/form"
"code.achtarmig.org/pas/ui/element/input"
"code.achtarmig.org/pas/ui/element/option"
"code.achtarmig.org/pas/ui/element/table"
"code.achtarmig.org/pas/ui/element/view"
"code.achtarmig.org/pas/ui/route"
)
func cdsDetails() {
view.New("CDSDetails", func(p *view.Element) {
route.Register("/cds", p)
p.Title = "CDSDetails"
p.OnRequest = onRequestDetails
container.New(p, "Details", func(p *container.Element) {
p.Justify = option.JustifyCenter
form.New(p, "qForm", func(p *form.Element) {
p.Method = http.MethodGet
p.Target = "#DetailsTable"
p.URL = "/cds"
p.PushURL = true
input.New(p, "q", input.TypeText, func(p *input.Element) {
p.ValidateInput = true
p.RequestDelay = 200
p.Parameter = true
p.Placeholder = "CDSViewTechnicalName"
p.Shape = option.ShapeRound
p.OnValidate = func(e *input.Element, v any) error {
_, err := db_table.GetCDSView(db_table.TechnicalName(v.(string)))
return err
}
})
})
})
container.New(p, "", func(p *container.Element) {
p.Justify = option.JustifyCenter
table.New(p, "DetailsTable", func(p *table.Element) {
p.MinWidth = 20
p.MaxWidth = 32
})
})
})
}
func onRequestDetails(e *view.Element, w http.ResponseWriter, r *http.Request) view.ProcessElements {
cds, _ := e.GetElement("q").(*input.Element).GetDataString()
e.Title = fmt.Sprintf("CDSDetails - %s", cds)
fields, err := model.GetCDSViewModelFields(cds)
if err != nil {
fmt.Println(err)
return view.ProcessElements{}
}
table := e.GetElement("DetailsTable").(*table.Element)
table.SetData(*fields)
for _, header := range table.Data.Header {
switch header.Name {
case "FieldNameOut":
header.Option.Font = option.FontMonospaced
case "DataTypeTitle":
case "FieldLengthOut":
header.Option.Font = option.FontMonospaced
case "DescriptionOut":
default:
header.Option.Hidden = true
}
}
table.Data.SetFieldOrder("FieldNameOut", "DescriptionOut", "DataTypeTitle", "FieldLengthOut")
return view.ProcessElements{
Partial: []element.ElementInterface{e.GetElement("DetailsTable")},
}
}

6
cmd/view/load.go Normal file
View File

@ -0,0 +1,6 @@
package view
func Load() {
searchView()
cdsDetails()
}

67
cmd/view/results.go Normal file
View File

@ -0,0 +1,67 @@
package view
import (
"api-cds-search/cmd/model"
"errors"
"net/http"
"net/url"
"code.achtarmig.org/pas/ui/element"
"code.achtarmig.org/pas/ui/element/boxedlist"
"code.achtarmig.org/pas/ui/element/boxedlist/expander"
"code.achtarmig.org/pas/ui/element/input"
"code.achtarmig.org/pas/ui/element/option"
"code.achtarmig.org/pas/ui/element/placeholder"
"code.achtarmig.org/pas/ui/element/view"
)
func results(v *view.Element, m *model.ResultsModel) element.ElementInterface {
cont := v.GetElement("SearchResultsContainer")
cont.DeleteAllChildren()
bl, _ := boxedlist.New(cont, "", func(p *boxedlist.Element) {
p.MaxWidth = 42
p.MinWidth = 20
for _, result := range m.Views {
expander.New(p, func(p *expander.Element) {
p.Title = result.DisplayName.String()
p.Subtitle = result.TechnicalName.String()
p.PlaceholderHeight = 21 * result.NumberOfFields
p.CallbackLazyLoad = func(e *expander.Element, w http.ResponseWriter, r *http.Request) error {
http.Redirect(w, r, "/cds?q="+url.QueryEscape(result.TechnicalName.String()), http.StatusSeeOther)
return errors.New("redirect")
}
p.HasSuffix = true
input.New(p, "", input.TypeText, func(p *input.Element) {
p.Shape = option.ShapePill
p.MaxWidth = 6
p.Disabled = true
p.Justify = option.JustifyCenter
p.SetData(result.StateTitle.String())
if result.State == "RELEASED" {
p.DisplayStyle = option.DisplayStyleSuccess
} else {
p.DisplayStyle = option.DisplayStyleError
}
})
})
}
})
return bl
}
func noResults(v *view.Element) element.ElementInterface {
cont := v.GetElement("SearchResultsContainer")
cont.DeleteAllChildren()
pl := placeholder.New(cont, "", func(p *placeholder.Element) {
p.Icon = option.IconLoupe
p.Title = "No Results"
p.Subtitle = "Try refining your search term"
})
return pl
}

155
cmd/view/search.go Normal file
View File

@ -0,0 +1,155 @@
package view
import (
"api-cds-search/cmd/search"
"net/http"
"code.achtarmig.org/pas/ui/element"
"code.achtarmig.org/pas/ui/element/button"
"code.achtarmig.org/pas/ui/element/container"
"code.achtarmig.org/pas/ui/element/form"
"code.achtarmig.org/pas/ui/element/input"
"code.achtarmig.org/pas/ui/element/option"
"code.achtarmig.org/pas/ui/element/placeholder"
"code.achtarmig.org/pas/ui/element/view"
"code.achtarmig.org/pas/ui/route"
)
func noSearch(v *view.Element) element.ElementInterface {
cont := v.GetElement("SearchResultsContainer")
cont.DeleteAllChildren()
pl := placeholder.New(cont, "", func(p *placeholder.Element) {
p.Icon = option.IconLoupe
p.Title = "Search CDS-Views"
p.Subtitle = "Find CDS-Views from the Business Accelerator Hub"
})
return pl
}
func searchView() {
view.New("Search", func(p *view.Element) {
route.Register("/search", p)
p.Title = "CDS-Search"
p.OnRequest = func(v *view.Element, w http.ResponseWriter, r *http.Request) view.ProcessElements {
inputQuery := v.GetElement("q").(*input.Element)
inputQueryValue, _ := v.GetElement("q").(*input.Element).GetDataString()
searchTerm := search.GetSearchTerm(inputQueryValue)
if inputQueryValue == "" {
noSearch(v)
}
inputPage := v.GetElement("p").(*input.Element)
inputMaxPage := v.GetElement("MaxPage").(*input.Element)
currentPage, _ := inputPage.GetDataInt()
if inputQuery.GetDataChanged() && inputQueryValue != "" {
buf, err := search.GetBuffer(searchTerm)
if err != nil {
search.BuildBuffer(searchTerm)
buf, err = search.GetBuffer(searchTerm)
}
if err != nil {
noResults(v)
} else {
currentPage, maxPage := search.GetPages(buf, currentPage)
m := search.Search(buf, currentPage, maxPage)
inputPage.Max = m.MaxPage
inputMaxPage.Min = m.MaxPage
inputMaxPage.Max = m.MaxPage
inputMaxPage.SetData(m.MaxPage)
results(v, m)
}
}
return view.ProcessElements{
Partial: []element.ElementInterface{v.GetElement("SearchResultsContainer")},
OOBUpdate: []element.ElementInterface{v.GetElement("pagerForm")},
}
}
container.New(p, "MainContainer", func(p *container.Element) {
p.Justify = option.JustifyCenter
form.New(p, "SearchForm", func(p *form.Element) {
p.Method = http.MethodGet
p.Target = "#SearchResultsContainer>*"
p.Swap = "outerHTML"
p.URL = "/search"
p.PushURL = true
p.Params = "q, p"
container.New(p, "SearchFormLinked", func(p *container.Element) {
p.Justify = option.JustifyCenter
p.LinkedHorizontal = true
input.New(p, "q", input.TypeText, func(p *input.Element) {
p.Parameter = true
p.Placeholder = "Search here"
p.Shape = option.ShapeRound
p.MinWidth = 10
p.MaxWidth = 36
})
button.New(p, "SearchButton", func(p *button.Element) {
p.Label = "Search"
p.DisplayStyle = option.DisplayStyleSuggestedAction
p.Shape = option.ShapeRound
p.MaxWidth = 6
})
})
})
})
container.New(p, "CenterPager", func(p *container.Element) {
p.Justify = option.JustifyCenter
container.New(p, "RightCenterPager", func(p *container.Element) {
p.MaxWidth = 42
p.MinWidth = 10
p.Justify = option.JustifyRight
form.New(p, "pagerForm", func(p *form.Element) {
p.Method = http.MethodGet
p.Include = "[name='q'], [name='p']"
p.Target = "#SearchResultsContainer>*"
p.URL = "/search"
p.Swap = "outerHTML"
p.Trigger = "change from:#p"
p.PushURL = true
container.New(p, "LinkPager", func(p *container.Element) {
p.LinkedHorizontal = true
p.Justify = option.JustifyRight
input.New(p, "p", input.TypeNumber, func(p *input.Element) {
p.Parameter = true
p.Shape = option.ShapeRound
p.SetMin = true
p.Min = 0
p.SetMax = true
p.Max = 0
p.MinWidth = 4
})
input.New(p, "MaxPage", input.TypeNumber, func(p *input.Element) {
p.Disabled = true
p.Shape = option.ShapeRound
p.MinWidth = 4
p.SetMin = true
p.SetMax = true
})
})
})
})
})
container.New(p, "SearchResultsContainer", func(p *container.Element) {
p.Justify = option.JustifyCenter
})
})
}

View File

@ -1,6 +1,6 @@
services: services:
web: web:
image: "code.achtarmig.org/sap/api-cds-search" image: "code.achtarmig.org/sap/api-cds-search:pas-ui-latest"
volumes: volumes:
- ./db.sqlite:/app/db.sqlite - ./db.sqlite:/app/db.sqlite
ports: ports:

3
go.mod
View File

@ -3,6 +3,7 @@ module api-cds-search
go 1.23.7 go 1.23.7
require ( require (
code.achtarmig.org/pas/ui v0.0.0-20250523191647-bc6f223e5725
github.com/glebarez/go-sqlite v1.22.0 github.com/glebarez/go-sqlite v1.22.0
github.com/go-chi/chi v1.5.5 github.com/go-chi/chi v1.5.5
github.com/lithammer/fuzzysearch v1.1.8 github.com/lithammer/fuzzysearch v1.1.8
@ -11,7 +12,7 @@ require (
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.5.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.15.0 // indirect golang.org/x/sys v0.15.0 // indirect

16
go.sum
View File

@ -1,3 +1,15 @@
code.achtarmig.org/pas/ui v0.0.0-20250520194351-de0cc4fbbcfa h1:rMY1ksN8wtFO8ago/jMEgz52F/pFQIZLfN1YFSyTeEg=
code.achtarmig.org/pas/ui v0.0.0-20250520194351-de0cc4fbbcfa/go.mod h1:4SnZejrC0bLr2Kq1yt4TBis8lo2hHA8K0m/0B2l6Uo0=
code.achtarmig.org/pas/ui v0.0.0-20250523170703-92fa955c1776 h1:VM1tir3yk+4iuJbDRVPmsLjrk1rJ+gTRPJh6LFTMMh0=
code.achtarmig.org/pas/ui v0.0.0-20250523170703-92fa955c1776/go.mod h1:2stpDl/L6Zgd4a/r+fDMsMMuF59aKQbAIeByX1UqiQo=
code.achtarmig.org/pas/ui v0.0.0-20250523172517-f2c3e6308b7d h1:/vt1BgqVi8fW7cdZWGMOfMarIRkrQuBMBdKhwEziaGI=
code.achtarmig.org/pas/ui v0.0.0-20250523172517-f2c3e6308b7d/go.mod h1:2stpDl/L6Zgd4a/r+fDMsMMuF59aKQbAIeByX1UqiQo=
code.achtarmig.org/pas/ui v0.0.0-20250523174346-c02bf81e9cf3 h1:Lsq0qH31XZG5b9j51y9vmXx+ZBmLD5H64aPlNBVjDfc=
code.achtarmig.org/pas/ui v0.0.0-20250523174346-c02bf81e9cf3/go.mod h1:2stpDl/L6Zgd4a/r+fDMsMMuF59aKQbAIeByX1UqiQo=
code.achtarmig.org/pas/ui v0.0.0-20250523190341-a0f627648964 h1:/VlNxxqZZRBuvMFbvQN6qlyrR4RooFxUkansW3T0XG8=
code.achtarmig.org/pas/ui v0.0.0-20250523190341-a0f627648964/go.mod h1:2stpDl/L6Zgd4a/r+fDMsMMuF59aKQbAIeByX1UqiQo=
code.achtarmig.org/pas/ui v0.0.0-20250523191647-bc6f223e5725 h1:VWlyudT8UDzl5Ns+NYO7xrUsKQcKvmkRkwsnHmRO+a4=
code.achtarmig.org/pas/ui v0.0.0-20250523191647-bc6f223e5725/go.mod h1:2stpDl/L6Zgd4a/r+fDMsMMuF59aKQbAIeByX1UqiQo=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 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/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 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
@ -6,8 +18,8 @@ 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/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 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.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 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=

15
main.go
View File

@ -5,20 +5,33 @@ import (
"api-cds-search/cmd/router" "api-cds-search/cmd/router"
"api-cds-search/cmd/search" "api-cds-search/cmd/search"
"api-cds-search/cmd/ui" "api-cds-search/cmd/ui"
"api-cds-search/cmd/view"
"fmt" "fmt"
"net/http" "net/http"
"code.achtarmig.org/pas/ui/initialize"
"code.achtarmig.org/pas/ui/log"
) )
func main() { func main() {
initialize.Init()
database.Load() database.Load()
err := search.Load() err := search.Load()
if err != nil { if err != nil {
panic(err) panic(err)
} }
ui.Load() ui.Load()
view.Load()
log.SetPrintError(true)
r := router.Load() r := router.Load()
fmt.Println("Listening on :8080") fmt.Println("Listening on :8080")
http.ListenAndServe(":8080", r) err = http.ListenAndServe(":8080", r)
if err != nil {
fmt.Println(err)
}
} }