Skip to content

Commit

Permalink
close #11 fix #4 Split home and search into pages
Browse files Browse the repository at this point in the history
Search is now a separate page from the home page which shows recent
searches. The search page accepts a few query parameters:
* query - the string to search for
* startTime - the absolute start time of the search
* endTime - the absolute end time of the search
* relativeTime - the relative duration of the search, or "ALL" to search
without filtering by time
* jobId - The ID of a previously started job to use the results from

The really big win here is that you can now link Logsuck searches to
someone else, whereas previously the URL would always just be "/".

The current design does not use client side routing. I don't feel the
complexity of adding client side routing at this stage is worth it.
Currently each page also gets its own bundle, meaning the entire Preact
library is downloaded for each page. This is a microscopic performance
issue that I might fix at some point but it doesn't seem worth
prioritizing.

A fairly simple improvement that can be made is to save the current
page from the pagination into the URL so you can also link to a specific
result page.

I'm not sure the behavior with the jobId param leading to job results
being reused is intuitive - maybe it will lead to confusion when you
get stale results, but I'm trying it out for now.
  • Loading branch information
JackBister committed Jan 27, 2021
1 parent 0d1ee27 commit 0be0767
Show file tree
Hide file tree
Showing 15 changed files with 1,232 additions and 740 deletions.
52 changes: 45 additions & 7 deletions internal/web/Web.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
package web

import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"text/template"
"time"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -60,8 +63,33 @@ func NewWeb(cfg *config.Config, eventRepo events.Repository, jobRepo jobs.Reposi
func (wi webImpl) Serve() error {
r := gin.Default()

g := r.Group("api/v1")
var fs http.FileSystem
if wi.cfg.Web.UsePackagedFiles {
fs = Assets
} else {
fs = http.Dir("web/static/dist")
}

tpl, err := parseTemplate(fs)
if err != nil {
return err
}

r.GET("/", func(c *gin.Context) {
tpl.Execute(c.Writer, gin.H{
"scriptSrc": "home.js",
})
c.Status(200)
})

r.GET("/search", func(c *gin.Context) {
tpl.Execute(c.Writer, gin.H{
"scriptSrc": "search.js",
})
c.Status(200)
})

g := r.Group("api/v1")
g.POST("/startJob", func(c *gin.Context) {
searchString := c.Query("searchString")
startTime, endTime, wErr := parseTimeParametersGin(c)
Expand Down Expand Up @@ -179,12 +207,6 @@ func (wi webImpl) Serve() error {
c.JSON(200, values)
})

var fs http.FileSystem
if wi.cfg.Web.UsePackagedFiles {
fs = Assets
} else {
fs = http.Dir("web/static/dist")
}
r.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
c.FileFromFS(path, fs)
Expand All @@ -194,6 +216,22 @@ func (wi webImpl) Serve() error {
return r.Run(wi.cfg.Web.Address)
}

func parseTemplate(fs http.FileSystem) (*template.Template, error) {
f, err := fs.Open("template.html")
if err != nil {
return nil, fmt.Errorf("failed to open template.html: %w", err)
}
b, err := ioutil.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read data from template.html: %ww", err)
}
tpl, err := template.New("template.html").Parse(string(b))
if err != nil {
return nil, fmt.Errorf("failed to parse template.html: %w", err)
}
return tpl, nil
}

func parseTimeParametersGin(c *gin.Context) (*time.Time, *time.Time, *webError) {
relativeTime, hasRelativeTime := c.GetQuery("relativeTime")
absoluteStart, hasAbsoluteStart := c.GetQuery("startTime")
Expand Down
63 changes: 63 additions & 0 deletions web/static/src/components/EventTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Copyright 2021 The Logsuck Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { h } from "preact";
import { LogEvent } from "../models/Event";

export interface EventTableProps {
events: LogEvent[];
}

export const EventTable = ({ events }: EventTableProps) => (
<table class="table table-hover search-result-table">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">Event</th>
</tr>
</thead>
<tbody>
{events.map((e) => (
<tr key={e.raw}>
<td class="event-timestamp">{e.timestamp.toLocaleString()}</td>
<td>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div class="event-raw">{e.raw}</div>
<hr
style={{
width: "100%",
marginTop: "0.75rem",
marginBottom: "0.5rem",
}}
/>
<div class="event-additional">
<dl class="row no-gutters" style={{ marginBottom: 0 }}>
<dt class="col-1">source</dt>
<dd class="col-1">{e.source}</dd>
</dl>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
);
50 changes: 50 additions & 0 deletions web/static/src/components/FieldTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright 2021 The Logsuck Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { h } from "preact";

export interface FieldTableProps {
fields: { [key: string]: number };
onFieldClicked: (str: string) => void;
}

export const FieldTable = ({ fields, onFieldClicked }: FieldTableProps) => {
const keys = Object.keys(fields);
return (
<div>
{keys.length === 0 && <div>No fields extracted</div>}
{keys.length > 0 && (
<table class="table table-sm table-hover">
<tbody>
{keys.map((k) => (
<tr
key={k}
onClick={(evt) => {
evt.stopPropagation();
onFieldClicked(k);
}}
class="test field-row"
>
<td>{k}</td>
<td style={{ textAlign: "right" }}>{fields[k]}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
48 changes: 48 additions & 0 deletions web/static/src/components/FieldValueTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright 2021 The Logsuck Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { h } from "preact";
import { TopFieldValueInfo } from "../models/TopFieldValueInfo";

export interface FieldValueTableProps {
values: TopFieldValueInfo[];
onFieldValueClicked: (value: string) => void;
}

export const FieldValueTable = ({
values,
onFieldValueClicked,
}: FieldValueTableProps) => (
<table class="table table-sm table-hover">
<tbody>
{values.map((f) => (
<tr
key={f.value}
onClick={() => onFieldValueClicked(f.value)}
style={{ cursor: "pointer" }}
>
<td class="field-value">{f.value}</td>
<td class="field-value-count" style={{ textAlign: "right" }}>
{f.count}
</td>
<td class="field-value-percentage" style={{ textAlign: "right" }}>
{(f.percentage * 100).toFixed(2)} %
</td>
</tr>
))}
</tbody>
</table>
);
28 changes: 28 additions & 0 deletions web/static/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright 2021 The Logsuck Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { h } from "preact";

export const Navbar = () => (
<header>
<nav class="navbar navbar-dark bg-dark">
<a href="/" class="navbar-brand">
logsuck
</a>
<a href="/search">Search</a>
</nav>
</header>
);
108 changes: 108 additions & 0 deletions web/static/src/components/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Copyright 2021 The Logsuck Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { h, Component } from "preact";
import { TimeSelect } from "./TimeSelect";
import { TimeSelection } from "../models/TimeSelection";
import { createSearchUrl } from "../createSearchUrl";

export interface SearchInputProps {
isButtonDisabled: boolean;
searchString: string;
setSearchString: (str: string) => void;
selectedTime: TimeSelection;
setSelectedTime: (ts: TimeSelection) => void;

onSearch: () => void;
}

export const SearchInput = (props: SearchInputProps) => (
<div class="search-container">
<form
onSubmit={(evt) => {
evt.preventDefault();
props.onSearch();
}}
>
<label htmlFor="searchinput">Search</label>
<div class="input-group mb-3">
<input
id="searchinput"
type="text"
class="form-control"
onInput={(evt) => props.setSearchString((evt.target as any).value)}
value={props.searchString}
/>
<div class="input-group-append">
<TimeSelect
selection={props.selectedTime}
onTimeSelected={(ts) => props.setSelectedTime(ts)}
/>
<button
disabled={props.isButtonDisabled}
type="submit"
class="btn btn-primary"
>
Search
</button>
</div>
</div>
</form>
</div>
);

export interface RedirectSearchInputProps {
navigateTo: (url: string) => void;
}

interface RedirectSearchInputState {
searchString: string;
timeSelection: TimeSelection;
}

/**
* RedirectSearchInput is an easier to use version of SearchInput which can be used on pages which don't need to do anything special with the input.
* RedirectSearchInput will navigate to the resulting search URL when the search button is clicked.
*/
export class RedirectSearchInput extends Component<
RedirectSearchInputProps,
RedirectSearchInputState
> {
constructor(props: RedirectSearchInputProps) {
super(props);
this.state = {
searchString: "",
timeSelection: {},
};
}

render() {
return (
<SearchInput
isButtonDisabled={false}
searchString={this.state.searchString}
setSearchString={(str) => this.setState({ searchString: str })}
selectedTime={this.state.timeSelection}
setSelectedTime={(ts) => this.setState({ timeSelection: ts })}
onSearch={() =>
this.props.navigateTo(
createSearchUrl(this.state.searchString, this.state.timeSelection)
)
}
/>
);
}
}
Loading

0 comments on commit 0be0767

Please sign in to comment.