commit
5a6cf58391
38 changed files with 9715 additions and 5 deletions
14
Makefile
14
Makefile
|
@ -1,10 +1,22 @@
|
|||
build: get
|
||||
swag init
|
||||
go build
|
||||
|
||||
full: build_web build_swagger build
|
||||
|
||||
get:
|
||||
go get ./...
|
||||
|
||||
build_swagger:
|
||||
swag init
|
||||
|
||||
build_web: get_web
|
||||
cd web && npm run build
|
||||
go run api/assets.go
|
||||
|
||||
get_web:
|
||||
cd web && npm install
|
||||
|
||||
# Docker stuff
|
||||
container:
|
||||
docker build -t gempir/justlog .
|
||||
|
||||
|
|
22
api/assets.go
Normal file
22
api/assets.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/shurcooL/vfsgen"
|
||||
)
|
||||
|
||||
var assets http.FileSystem = http.Dir("web/public")
|
||||
|
||||
func main() {
|
||||
err := vfsgen.Generate(assets, vfsgen.Options{
|
||||
Filename: "api/assets_vfsgen.go",
|
||||
PackageName: "api",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
192
api/assets_vfsgen.go
Normal file
192
api/assets_vfsgen.go
Normal file
File diff suppressed because one or more lines are too long
|
@ -45,7 +45,6 @@ func (s *Server) AddChannel(channel string) {
|
|||
}
|
||||
|
||||
func (s *Server) Init() {
|
||||
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
|
||||
|
@ -66,10 +65,14 @@ func (s *Server) Init() {
|
|||
}))
|
||||
e.Use(middleware.CORSWithConfig(DefaultCORSConfig))
|
||||
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusMovedPermanently, "/index.html")
|
||||
assetHandler := http.FileServer(assets)
|
||||
e.GET("/", echo.WrapHandler(assetHandler))
|
||||
e.GET("/bundle.js", echo.WrapHandler(assetHandler))
|
||||
|
||||
e.GET("/docs", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
|
||||
})
|
||||
e.GET("/*", echoSwagger.WrapHandler)
|
||||
e.GET("/docs/*", echoSwagger.WrapHandler)
|
||||
e.GET("/channels", s.getAllChannels)
|
||||
|
||||
e.GET("/channel/:channel/user/:username/range", s.getUserLogsRangeByName)
|
||||
|
|
2
go.mod
2
go.mod
|
@ -7,6 +7,8 @@ require (
|
|||
github.com/gempir/go-twitch-irc/v2 v2.2.0
|
||||
github.com/json-iterator/go v1.1.7
|
||||
github.com/labstack/echo/v4 v4.0.0
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/swaggo/echo-swagger v0.0.0-20190329130007-1219b460a043
|
||||
github.com/swaggo/swag v1.6.2
|
||||
|
|
4
go.sum
4
go.sum
|
@ -56,6 +56,10 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd h1:ug7PpSOB5RBPK1Kg6qskGBoP3Vnj/aNYFTznWvlkGo0=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
|
3
package-lock.json
generated
Normal file
3
package-lock.json
generated
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
10
web/.babelrc
Normal file
10
web/.babelrc
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/env",
|
||||
"@babel/react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
]
|
||||
}
|
14
web/.gitignore
vendored
Normal file
14
web/.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.idea/
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*dist/
|
8807
web/package-lock.json
generated
Normal file
8807
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
web/package.json
Normal file
37
web/package.json
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "justlog-web",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"material-design-icons": "^3.0.1",
|
||||
"moment": "^2.22.2",
|
||||
"react": "^16.6.1",
|
||||
"react-dom": "^16.6.1",
|
||||
"react-md": "^1.9.0",
|
||||
"react-redux": "^5.1.0",
|
||||
"react-string-replace": "^0.4.1",
|
||||
"react-toastify": "^4.4.2",
|
||||
"redux": "^4.0.1",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"whatwg-fetch": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --mode development --content-base public",
|
||||
"build": "node_modules/.bin/webpack -p --mode production && cp public/* dist/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"css-loader": "^1.0.1",
|
||||
"mini-css-extract-plugin": "^0.4.4",
|
||||
"node-sass": "^4.10.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^4.25.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.11"
|
||||
}
|
||||
}
|
43
web/public/bundle.js
Normal file
43
web/public/bundle.js
Normal file
File diff suppressed because one or more lines are too long
18
web/public/index.html
Normal file
18
web/public/index.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
|
||||
<title>logsearch</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
<script src="bundle.js"></script>
|
||||
</html>
|
6
web/src/index.jsx
Normal file
6
web/src/index.jsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './js/App';
|
||||
import './scss/app.scss';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
25
web/src/js/App.jsx
Normal file
25
web/src/js/App.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { Component } from 'react';
|
||||
import { createStore, applyMiddleware } from "redux";
|
||||
import thunk from "redux-thunk";
|
||||
import { Provider } from "react-redux";
|
||||
import reducer from "./store/reducer";
|
||||
import createInitialState from "./store/createInitialState";
|
||||
import LogSearch from './components/LogSearch';
|
||||
|
||||
|
||||
export default class App extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.store = createStore(reducer, createInitialState(), applyMiddleware(thunk));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<LogSearch />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
25
web/src/js/actions/loadChannels.js
Normal file
25
web/src/js/actions/loadChannels.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import setChannels from "./setChannels";
|
||||
|
||||
export default function () {
|
||||
return function (dispatch, getState) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch("/channels").then((response) => {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response
|
||||
} else {
|
||||
var error = new Error(response.statusText)
|
||||
error.response = response
|
||||
throw error
|
||||
}
|
||||
}).then((response) => {
|
||||
return response.json();
|
||||
}).then((json) => {
|
||||
dispatch(setChannels(json.channels));
|
||||
|
||||
resolve();
|
||||
}).catch(() => {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
41
web/src/js/actions/loadLogs.js
Normal file
41
web/src/js/actions/loadLogs.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import setLogs from "./setLogs";
|
||||
import setLoading from "./setLoading";
|
||||
|
||||
export default function (channel, username, year, month) {
|
||||
return function (dispatch, getState) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
dispatch(setLoading(true));
|
||||
|
||||
let options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
fetch(`/channel/${channel}/user/${username}/${year}/${month}`, options).then((response) => {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response
|
||||
} else {
|
||||
var error = new Error(response.statusText)
|
||||
error.response = response
|
||||
throw error
|
||||
}
|
||||
}).then((response) => {
|
||||
return response.json()
|
||||
}).then((json) => {
|
||||
|
||||
for (let value of json.messages) {
|
||||
value.timestamp = Date.parse(value.timestamp)
|
||||
}
|
||||
|
||||
dispatch(setLogs(json));
|
||||
dispatch(setLoading(false));
|
||||
resolve();
|
||||
}).catch((error) => {
|
||||
dispatch(setLoading(false));
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
28
web/src/js/actions/loadTwitchEmotes.js
Normal file
28
web/src/js/actions/loadTwitchEmotes.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import setTwitchEmotes from "./setTwitchEmotes";
|
||||
|
||||
export default function () {
|
||||
return function (dispatch, getState) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
fetch("https://twitchemotes.com/api_cache/v3/global.json").then((response) => {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response
|
||||
} else {
|
||||
var error = new Error(response.statusText)
|
||||
error.response = response
|
||||
throw error
|
||||
}
|
||||
}).then((response) => {
|
||||
return response.json();
|
||||
}).then((json) => {
|
||||
dispatch(setTwitchEmotes(json));
|
||||
|
||||
resolve();
|
||||
}).catch(() => {
|
||||
reject();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
};
|
||||
}
|
6
web/src/js/actions/setChannels.js
Normal file
6
web/src/js/actions/setChannels.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default (channels) => (dispatch) => {
|
||||
dispatch({
|
||||
type: 'SET_CHANNELS',
|
||||
channels: channels
|
||||
});
|
||||
}
|
6
web/src/js/actions/setLoading.js
Normal file
6
web/src/js/actions/setLoading.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default (loading) => (dispatch) => {
|
||||
dispatch({
|
||||
type: 'SET_LOADING',
|
||||
loading: loading
|
||||
});
|
||||
}
|
6
web/src/js/actions/setLogs.js
Normal file
6
web/src/js/actions/setLogs.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default (logs) => (dispatch) => {
|
||||
dispatch({
|
||||
type: 'SET_LOGS',
|
||||
logs: logs
|
||||
});
|
||||
}
|
6
web/src/js/actions/setTwitchEmotes.js
Normal file
6
web/src/js/actions/setTwitchEmotes.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default (twitchEmotes) => (dispatch) => {
|
||||
dispatch({
|
||||
type: 'SET_TWITCH_EMOTES',
|
||||
twitchEmotes: twitchEmotes
|
||||
});
|
||||
}
|
77
web/src/js/components/Filter.jsx
Normal file
77
web/src/js/components/Filter.jsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Autocomplete, TextField, Button, SelectField } from 'react-md';
|
||||
import moment from "moment";
|
||||
|
||||
export default class Filter extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
channel: "",
|
||||
username: "",
|
||||
year: moment().year(),
|
||||
month: moment().format("M")
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form className="filter" autoComplete="off" onSubmit={this.onSubmit}>
|
||||
<Autocomplete
|
||||
id="channel"
|
||||
label="Channel"
|
||||
placeholder="forsen"
|
||||
onChange={this.onChannelChange}
|
||||
onAutocomplete={this.onChannelChange}
|
||||
data={this.props.channels}
|
||||
/>
|
||||
<TextField
|
||||
id="username"
|
||||
label="Username"
|
||||
lineDirection="center"
|
||||
onChange={this.onUsernameChange}
|
||||
placeholder="gempir"
|
||||
/>
|
||||
<SelectField
|
||||
id="year"
|
||||
label="Year"
|
||||
defaultValue={this.state.year}
|
||||
menuItems={[moment().year(), moment().subtract(1, "year").year(), moment().subtract(2, "year").year()]}
|
||||
onChange={this.onYearChange}
|
||||
value={this.state.year}
|
||||
/>
|
||||
<SelectField
|
||||
id="month"
|
||||
label="Month"
|
||||
defaultValue={this.state.month}
|
||||
menuItems={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]}
|
||||
onChange={this.onMonthChange}
|
||||
value={this.state.month}
|
||||
/>
|
||||
<Button flat primary swapTheming type="submit" className="show-logs">Show logs</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
onChannelChange = (value) => {
|
||||
this.setState({...this.state, channel: value});
|
||||
}
|
||||
|
||||
onUsernameChange = (value) => {
|
||||
this.setState({...this.state, username: value});
|
||||
}
|
||||
|
||||
onYearChange = (value) => {
|
||||
this.setState({...this.state, year: value});
|
||||
}
|
||||
|
||||
onMonthChange = (value) => {
|
||||
this.setState({...this.state, month: moment().month(value).format("M")});
|
||||
}
|
||||
|
||||
onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.searchLogs(this.state.channel, this.state.username, this.state.year, this.state.month);
|
||||
}
|
||||
}
|
43
web/src/js/components/LogSearch.jsx
Normal file
43
web/src/js/components/LogSearch.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React, { Component } from "react";
|
||||
import Filter from "./Filter";
|
||||
import LogView from "./LogView";
|
||||
import { connect } from "react-redux";
|
||||
import loadChannels from "../actions/loadChannels";
|
||||
import loadLogs from "../actions/loadLogs";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import 'react-toastify/dist/ReactToastify.min.css';
|
||||
|
||||
class LogSearch extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
props.dispatch(loadChannels());
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="log-search">
|
||||
<ToastContainer />
|
||||
<Filter
|
||||
channels={this.props.channels}
|
||||
searchLogs={this.searchLogs}
|
||||
/>
|
||||
<LogView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
searchLogs = (channel, username, year, month) => {
|
||||
this.props.dispatch(loadLogs(channel, username, year, month)).catch((error) => {
|
||||
toast.error("Failed to load logs: " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
channels: state.channels,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(LogSearch);
|
70
web/src/js/components/LogView.jsx
Normal file
70
web/src/js/components/LogView.jsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from "react-redux";
|
||||
import { Button, CircularProgress } from 'react-md';
|
||||
import moment from 'moment';
|
||||
import twitchEmotes from "../emotes/twitch";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
|
||||
class LogView extends Component {
|
||||
|
||||
static LOAD_LIMIT = 100;
|
||||
|
||||
state = {
|
||||
limitLoad: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={"log-view"}>
|
||||
{this.getLogs().map((value, key) =>
|
||||
<div key={key} className="line" onClick={() => this.setState({})}>
|
||||
<span id={value.timestamp} className="timestamp">{this.formatDate(value.timestamp)}</span>{this.renderMessage(value.text)}
|
||||
</div>
|
||||
)}
|
||||
{this.getLogs().length > 0 && this.state.limitLoad && <Button className={"load-all"} raised primary onClick={() => this.setState({ ...this.state, limitLoad: false })}>Load all</Button>}
|
||||
{this.props.loading && <CircularProgress className={"progress"} scale={10} id={"progress"} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getLogs = () => {
|
||||
if (this.state.limitLoad) {
|
||||
return this.props.messages.slice(this.props.messages.length - LogView.LOAD_LIMIT, this.props.messages.length).reverse();
|
||||
} else {
|
||||
return this.props.messages.reverse();
|
||||
}
|
||||
};
|
||||
|
||||
renderMessage = (message) => {
|
||||
for (let emoteCode in twitchEmotes) {
|
||||
const regex = new RegExp(`(?:^|\ )(${emoteCode})(?:$|\ )`);
|
||||
|
||||
message = reactStringReplace(message, regex, (match, i) => (
|
||||
<img key={i} src={this.buildTwitchEmote(twitchEmotes[emoteCode].id)} alt={match} />
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
{message}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
formatDate = (timestamp) => {
|
||||
return moment(timestamp).format("YYYY-MM-DD HH:mm:ss UTC");
|
||||
}
|
||||
|
||||
buildTwitchEmote = (id) => {
|
||||
return `https://static-cdn.jtvnw.net/emoticons/v1/${id}/1.0`;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
messages: state.logs.messages,
|
||||
loading: state.loading
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(LogView);
|
2
web/src/js/emotes/twitch.js
Normal file
2
web/src/js/emotes/twitch.js
Normal file
File diff suppressed because one or more lines are too long
10
web/src/js/store/createInitialState.js
Normal file
10
web/src/js/store/createInitialState.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default () => {
|
||||
return {
|
||||
channels: [],
|
||||
logs: {
|
||||
messages: [],
|
||||
},
|
||||
loading: false,
|
||||
twitchEmotes: {},
|
||||
}
|
||||
}
|
14
web/src/js/store/reducer.js
Normal file
14
web/src/js/store/reducer.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default (state, action) => {
|
||||
switch (action.type) {
|
||||
case "SET_CHANNELS":
|
||||
return { ...state, channels: action.channels };
|
||||
case "SET_LOADING":
|
||||
return { ...state, loading: action.loading };
|
||||
case "SET_LOGS":
|
||||
return { ...state, logs: action.logs };
|
||||
case "SET_TWITCH_EMOTES":
|
||||
return { ...state, twitchEmotes: action.twitchEmotes };
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
};
|
13
web/src/scss/app.scss
Normal file
13
web/src/scss/app.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
@import "vendor/react-md";
|
||||
@import "vendor/reset";
|
||||
@import "variables/colors";
|
||||
@import "tools/mixins";
|
||||
|
||||
@import "base/body";
|
||||
@import "base/forms";
|
||||
@import "base/links";
|
||||
|
||||
@import "modules/log-search";
|
||||
@import "modules/filter";
|
||||
@import "modules/log-view";
|
||||
|
12
web/src/scss/base/body.scss
Normal file
12
web/src/scss/base/body.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: $grey-dark;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
7
web/src/scss/base/forms.scss
Normal file
7
web/src/scss/base/forms.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
input {
|
||||
color: white;
|
||||
}
|
||||
|
||||
label {
|
||||
color: white;
|
||||
}
|
8
web/src/scss/base/links.scss
Normal file
8
web/src/scss/base/links.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
22
web/src/scss/modules/filter.scss
Normal file
22
web/src/scss/modules/filter.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
.filter {
|
||||
position: relative;
|
||||
flex: 0.1;
|
||||
background: $grey-medium;
|
||||
margin-right: 15px;
|
||||
height: 300px;
|
||||
padding: 1rem;
|
||||
border-radius: 3px;
|
||||
@include box-shadow(1);
|
||||
|
||||
input {
|
||||
&::-webkit-input-placeholder {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.show-logs {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
5
web/src/scss/modules/log-search.scss
Normal file
5
web/src/scss/modules/log-search.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.log-search {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
min-height: 100vh;
|
||||
}
|
63
web/src/scss/modules/log-view.scss
Normal file
63
web/src/scss/modules/log-view.scss
Normal file
|
@ -0,0 +1,63 @@
|
|||
.log-view {
|
||||
flex: 0.9;
|
||||
background: $grey-medium;
|
||||
@include box-shadow(1);
|
||||
height: 97vh;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
padding: 2px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background: $grey-light;
|
||||
}
|
||||
|
||||
.line {
|
||||
padding: 0.25rem;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
min-height: 24px;
|
||||
|
||||
p {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 16px;
|
||||
width: auto;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
position: relative;
|
||||
color: $grey-light;
|
||||
margin-right: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
color: $grey-lighter;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.load-all {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
15
web/src/scss/tools/mixins.scss
Normal file
15
web/src/scss/tools/mixins.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
@mixin box-shadow ($level) {
|
||||
|
||||
@if $level == 1 {
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
|
||||
} @else if $level == 2 {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
} @else if $level == 3 {
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
|
||||
} @else if $level == 4 {
|
||||
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
|
||||
} @else if $level == 5 {
|
||||
box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
|
||||
}
|
||||
|
||||
}
|
5
web/src/scss/variables/colors.scss
Normal file
5
web/src/scss/variables/colors.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
$grey-lighter: #919191;
|
||||
$grey-light: #616161;
|
||||
$grey-medium: #424242;
|
||||
$grey-dark: #212121;
|
28
web/webpack.config.js
Normal file
28
web/webpack.config.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.jsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'public'),
|
||||
filename: 'bundle.js',
|
||||
publicPath: "/",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "babel-loader"
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.s?css$/,
|
||||
use: ["style-loader", "css-loader", "sass-loader"]
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx'],
|
||||
}
|
||||
};
|
Loading…
Add table
Reference in a new issue