Browse Source

logs: Added filtering.

1.0
Gisle Aune 6 years ago
parent
commit
31251d5c7b
  1. 8
      marko/components/menu-link/component.js
  2. 2
      marko/page/logs-content/components/logs-content-menu/index.marko
  3. 147
      marko/page/logs/components/add-filter-modal-body/component.js
  4. 12
      marko/page/logs/components/add-filter-modal-body/index.marko
  5. 16
      marko/page/logs/components/add-filter-modal/component.js
  6. 7
      marko/page/logs/components/add-filter-modal/index.marko
  7. 16
      marko/page/logs/components/add-filter-modal/style.less
  8. 64
      marko/page/logs/components/logs-menu/component.js
  9. 6
      marko/page/logs/components/logs-menu/index.marko
  10. 37
      marko/page/logs/components/page/component.js
  11. 5
      marko/page/logs/components/page/index.marko
  12. 2
      marko/page/logs/list.marko
  13. 22
      routes/logs/index.js
  14. 49
      rpdata/api/Character.js
  15. 39
      rpdata/api/LogHeader.js

8
marko/components/menu-link/component.js

@ -1,8 +1,12 @@
module.exports = class { module.exports = class {
onCreate(input) {
onCreate() {
this.state = { this.state = {
classes: ["menu-link", "color-menu"],
classes: [],
} }
}
onInput(input) {
this.state.classes = ["menu-link", "color-menu"]
if (input.dark) { if (input.dark) {
this.state.classes.push(input) this.state.classes.push(input)

2
marko/page/logs-content/components/logs-content-menu/index.marko

@ -5,7 +5,7 @@
<menu-gap /> <menu-gap />
</if-permitted> </if-permitted>
<menu-header>Links</menu-header> <menu-header>Links</menu-header>
<menu-link href=("/logs/?channels="+input.log.channel.name) icon="#">${input.log.channel.name}</menu-link>
<menu-link href=("/logs/?channels="+encodeURIComponent(input.log.channel.name)) icon="#">${input.log.channel.name}</menu-link>
<menu-link if(input.log.channel.eventName) href=("/logs/?channels="+input.log.channel.eventName) icon="E">${input.log.channel.eventName}</menu-link> <menu-link if(input.log.channel.eventName) href=("/logs/?channels="+input.log.channel.eventName) icon="E">${input.log.channel.eventName}</menu-link>
<for(character in input.log.characters)> <for(character in input.log.characters)>
<menu-link href=("/logs/?characters="+character.id) icon="C">${character.name} <span class="weak">(${character.author})</span></menu-link> <menu-link href=("/logs/?characters="+character.id) icon="C">${character.name} <span class="weak">(${character.author})</span></menu-link>

147
marko/page/logs/components/add-filter-modal-body/component.js

@ -0,0 +1,147 @@
const {channelApi} = require("../../../../../rpdata/api/Channel")
const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader")
module.exports = class {
onCreate() {
this.state = {
search: "",
filters: [],
channels: [],
eventNames: [],
}
this.first = true
}
onInput(input) {
if (input.filters) {
if (this.timeout != null) {
setTimeout(() => {
this.searchFilters()
}, 1)
}
}
console.log(input.eventNames)
}
onMount() {
this.timeout = null
if (this.state.channels.length === 0) {
channelApi.list().then(channels => {
this.state.channels = channels
this.searchFilters()
}).catch(err => {
console.warn("Failed to fetch channels:", err)
})
}
if (this.state.eventNames.length === 0) {
logHeaderApi.eventNames().then(eventNames => {
this.state.eventNames = eventNames
this.searchFilters()
}).catch(err => {
console.warn("Failed to fetch eventNames:", err)
})
}
this.getEl("search").addEventListener("keydown", ev => {
if (this.waiting) {
return
}
if (this.timeout != null) {
clearTimeout(this.timeout)
}
this.timeout = setTimeout(() => {
this.timeout = null
this.state.search = ev.target.value
this.searchFilters()
}, 200)
})
}
onUnmount() {
clearTimeout(this.timeout)
this.timeout = null
this.state.search = ""
this.state.filters = []
}
add(type, value, filter) {
this.emit("add", type, value)
this.state.filters = this.state.filters.filter(f => f != filter)
}
searchFilters() {
const filters = []
const search = this.state.search.toLowerCase()
for (const channel of this.state.channels) {
if (search == "" || channel.name.toLowerCase().includes(search)) {
if (this.input.filter.channels && this.input.filter.channels.includes(channel.name)) {
continue
}
filters.push({
colorClass: "color-tag-location",
text: channel.name,
type: "channels",
value: channel.name,
})
}
}
for (const name of this.state.eventNames) {
if (search == "" || name.toLowerCase().includes(search)) {
if (this.input.filter.events && this.input.filter.events.includes(name)) {
continue
}
filters.push({
colorClass: "color-tag-event",
text: name,
type: "events",
value: name,
})
}
}
for (const character of this.input.characters) {
if (search == "" || character.name.toLowerCase().includes(search)) {
if (this.input.filter.characters && this.input.filter.characters.includes(character.id)) {
continue
}
filters.push({
colorClass: "color-tag-character",
text: character.name,
type: "characters",
value: character.id,
})
}
}
filters.sort((a, b) => {
let aCompare = a.text
let bCompare = b.text
if (a.text.charAt(0) === "#") {
aCompare = a.text.slice(1)
}
if (b.text.charAt(0) === "#") {
bCompare = b.text.slice(1)
}
return aCompare.localeCompare(bCompare)
})
this.state.filters = filters
}
}

12
marko/page/logs/components/add-filter-modal-body/index.marko

@ -0,0 +1,12 @@
<label>Search</label>
<input key="search" placeholder="" class="big" value=state.search />
<label>Filters</label>
<div if(state.search.length > 0) class="filter-row" >
<div class=["content", "color-text"]>Search: ${state.search}</div>
<button disabled=state.loading on-click("emit", "add", "search", state.search)>${input.filter.search ? "Replace" : "Add"}</button>
</div>
<div for(filter in state.filters) class="filter-row" >
<div class=["content", filter.colorClass]>${filter.text}</div>
<button disabled=state.loading on-click("add", filter.type, filter.value, filter)>Add</button>
</div>

16
marko/page/logs/components/add-filter-modal/component.js

@ -0,0 +1,16 @@
const moment = require("moment")
const {logsApi} = require("../../../../../rpdata/api/Log")
module.exports = class {
onCreate(input) {
this.state = {
filters: [],
search: "",
}
}
close() {
this.emit("close")
}
}

7
marko/page/logs/components/add-filter-modal/index.marko

@ -0,0 +1,7 @@
import moment from "moment"
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") >
<h1 key="stuff">Add Filter</h1>
<add-filter-modal-body characters=input.characters filter=input.filter on-add("emit", "add") />
</modal>

16
marko/page/logs/components/add-filter-modal/style.less

@ -0,0 +1,16 @@
div.filter-row {
padding: 0.25em;
border-bottom: 1px solid rgba(0, 220, 255, 0.125);
div.content {
width: 80%;
display: inline-block;
vertical-align: middle;
}
button {
width: 20% !important;
display: inline-block !important;
margin: 0 !important;
}
}

64
marko/page/logs/components/logs-menu/component.js

@ -1,4 +1,68 @@
module.exports = class { module.exports = class {
onCreate() {
this.state = {
filters: [],
filtered: false,
}
}
onInput(input) {
if (!input.filter) {
this.state.filters = []
this.state.filtered = false
return
}
const filters = []
const {characters, channels, events, search} = input.filter
if (search != null) {
filters.push({
type: "search",
key: "search",
icon: "T",
color: "color-text",
text: search,
})
}
for (const character of (characters || [])) {
filters.push({
type: "characters",
key: character,
icon: "C",
color: "color-tag-character",
id: character,
text: (input.characters.find(c => c.id === character) || {name: "Unknown ("+character+")"}).name,
})
}
for (const channel of (channels || [])) {
filters.push({
type: "channels",
key: channel.replace(/'/g, "__"),
icon: "#",
color: "color-tag-location",
id: channel,
text: channel,
})
}
for (const event of (events || [])) {
filters.push({
type: "events",
key: event.replace(/['\s\.]/g, "__"),
icon: "E",
color: "color-tag-event",
id: event,
text: event,
})
}
this.state.filters = filters
this.state.filtered = (filters.length > 0)
}
select(value) { select(value) {
this.emit("select", value) this.emit("select", value)
} }

6
marko/page/logs/components/logs-menu/index.marko

@ -1,10 +1,12 @@
<menu user=input.user> <menu user=input.user>
<menu-header key="logs">Logs</menu-header> <menu-header key="logs">Logs</menu-header>
<menu-link key="logs" selected=input.selected.index icon="L" href="/logs/">All</menu-link>
<menu-link key="logs" selected=(input.selected.index && !state.filtered) icon="L" href="/logs/">All</menu-link>
<if-permitted key="if-permitted" user=input.user permission="log.add"> <if-permitted key="if-permitted" user=input.user permission="log.add">
<menu-link key="add_log" dark on-click("select", "add") icon="+">Add Log</menu-link>
<menu-link key="add_log" dark on-click("select", "log.add") icon="+">Add Log</menu-link>
</if-permitted> </if-permitted>
<menu-gap /> <menu-gap />
<menu-header key="filters">Filters</menu-header> <menu-header key="filters">Filters</menu-header>
<menu-link for(filter in state.filters) textClass=filter.color unselectable icon=filter.icon on-click("emit", "removefilter", filter.type, filter.id)>${filter.text}</menu-link>
<menu-link key="add_filter" dark on-click("select", "filter.add") icon="+">Add Filter</menu-link>
<menu-gap /> <menu-gap />
</menu> </menu>

37
marko/page/logs/components/page/component.js

@ -3,6 +3,7 @@ const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader")
module.exports = class { module.exports = class {
onCreate(input) { onCreate(input) {
this.state = { this.state = {
filter: input.filter,
logs: input.logs, logs: input.logs,
modal: null, modal: null,
} }
@ -16,8 +17,40 @@ module.exports = class {
this.state.modal = null this.state.modal = null
} }
onMount() {
logHeaderApi.list({limit: 0}).then(logs => {
addFilter(type, filter) {
if (type === "search") {
this.state.filter = Object.assign({}, this.state.filter, {search: filter})
} else {
this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).concat(filter)})
}
console.log("FILTER:", this.state.filter)
this.refresh()
}
removeFilter(type, filter) {
console.log(type, filter, this.state.filter)
if (type === "search") {
this.state.filter = Object.assign({}, this.state.filter, {search: null})
this.refresh()
} else if ((this.state.filter[type] || []).length === 1) {
this.state.filter = Object.assign({}, this.state.filter, {[type]: null})
this.refresh()
} else {
const index = (this.state.filter[type] || []).indexOf(filter)
if (index !== -1) {
this.state.filter = Object.assign({}, this.state.filter, {[type]: this.state.filter[type].splice(0, index).concat(this.state.filter[type].splice(index + 1))},)
this.refresh()
}
}
}
refresh() {
console.log("REFRESH", this.state.filter)
logHeaderApi.list(this.state.filter).then(logs => {
this.state.logs = logs this.state.logs = logs
}) })
} }

5
marko/page/logs/components/page/index.marko

@ -1,6 +1,7 @@
<background src="/assets/images/bg.png" opacity=0.25 /> <background src="/assets/images/bg.png" opacity=0.25 />
<logs-menu key="menu" on-select("open") selected=(input.selected || {}) user=input.user />
<logs-menu key="menu" on-select("open") on-removefilter("removeFilter") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
<main> <main>
<logs-list logs=state.logs /> <logs-list logs=state.logs />
</main> </main>
<add-log-modal enabled=(state.modal === "add") on-close("close") />
<add-log-modal enabled=(state.modal === "log.add") on-close("close") />
<add-filter-modal enabled=(state.modal === "filter.add") characters=input.characters filter=state.filter on-add("addFilter") on-close("close") />

2
marko/page/logs/list.marko

@ -1,6 +1,6 @@
<include("../layout", {title: "Logs", site: "logs"})> <include("../layout", {title: "Logs", site: "logs"})>
<@body> <@body>
<!-- Page needed to get component.js functionality --> <!-- Page needed to get component.js functionality -->
<page logs=input.logs user=input.user selected=input.selected />
<page logs=input.logs filter=input.filter characters=input.characters user=input.user selected=input.selected />
</@body> </@body>
</include> </include>

22
routes/logs/index.js

@ -1,14 +1,32 @@
const express = require("express") const express = require("express")
const router = express.Router() const router = express.Router()
const {logHeaderApi} = require("../../rpdata/api/LogHeader")
const {logHeaderApi, eventNames} = require("../../rpdata/api/LogHeader")
const {charactersApi} = require("../../rpdata/api/Character")
const listTemplate = require("../../marko/page/logs/list.marko") const listTemplate = require("../../marko/page/logs/list.marko")
router.get("/", async(req, res) => { router.get("/", async(req, res) => {
const filter = {limit: 0}
if (req.query.characters) {
filter.characters = req.query.characters.split(",")
}
if (req.query.channels) {
filter.channels = req.query.channels.split(",")
}
if (req.query.events) {
filter.events = req.query.events.split(",")
}
if (req.query.search) {
filter.search = req.query.search
}
try { try {
res.markoAsync(listTemplate, { res.markoAsync(listTemplate, {
logs: logHeaderApi.list({limit: 0}),
filter: filter,
logs: logHeaderApi.list(filter),
characters: charactersApi.listHeaders(),
selected: {index: true}, selected: {index: true},
}) })
} catch(err) { } catch(err) {

49
rpdata/api/Character.js

@ -27,21 +27,34 @@ class Character {
} }
} }
class CharacterHeader {
/**
* @param {string} id
* @param {string} author
* @param {string} name
*/
constructor(id, author, name) {
this.id = id
this.author = author
this.name = name
}
/**
* Create a character object from data.
*/
static fromData(data) {
return new CharacterHeader(data.id, data.author, data.name)
}
}
class ChracterAPI { class ChracterAPI {
/** /**
* Call `characters(filter)` query * Call `characters(filter)` query
* *
* @param {{ids:string[], nicks:string[], names:string[], author:string, search:string, logged:boolean}} filter * @param {{ids:string[], nicks:string[], names:string[], author:string, search:string, logged:boolean}} filter
* @returns {Promise<Story[]>}
* @returns {Promise<Character[]>}
*/ */
list(filter = {}) { list(filter = {}) {
if (filter.earliestFictionalDate != null && typeof(filter.earliestFictionalDate) !== "string") {
filter.earliestFictionalDate = filter.earliestFictionalDate.toISOString()
}
if (filter.latestFictionalDate != null && typeof(filter.latestFictionalDate) !== "string") {
filter.latestFictionalDate = filter.latestFictionalDate.toISOString()
}
return query(` return query(`
query ListCharacters($filter: CharactersFilter) { query ListCharacters($filter: CharactersFilter) {
characters(filter: $filter) { characters(filter: $filter) {
@ -58,6 +71,26 @@ class ChracterAPI {
}) })
} }
/**
* Call `characters(filter)` query
*
* @param {{ids:string[], nicks:string[], names:string[], author:string, search:string, logged:boolean}} filter
* @returns {Promise<CharacterHeader[]>}
*/
listHeaders(filter = {}) {
return query(`
query ListCharacters($filter: CharactersFilter) {
characters(filter: $filter) {
id
name
author
}
}
`, {filter}).then(({characters}) => {
return characters.map(d => CharacterHeader.fromData(d))
})
}
/** /**
* Call `addCharacter(input)` mutation, returns addable fields. * Call `addCharacter(input)` mutation, returns addable fields.
* *

39
rpdata/api/LogHeader.js

@ -1,5 +1,7 @@
const {query} = require("../client") const {query} = require("../client")
let eventNames = []
class LogHeader { class LogHeader {
/** /**
* Construct a log header. You should probably use the logHeaderApi instead of doing * Construct a log header. You should probably use the logHeaderApi instead of doing
@ -50,7 +52,38 @@ class LogHeaderCharacter {
*/ */
const logHeaderApi = { const logHeaderApi = {
/** /**
* Call `stories(filter)` query
* Call `logs` query, returns event name
*
* @returns {Promise<string[]>}
*/
eventNames() {
return query(`
query LogHeadersEventNames() {
headers: logs(filter:{limit:0}) {
eventName
}
}
`, {}).then(({headers}) => {
const eventNames = []
const seen = {}
for (const header of headers) {
if (!header.eventName) {
continue
}
if (!seen[header.eventName]) {
eventNames.push(header.eventName)
seen[header.eventName] = true
}
}
return eventNames
})
},
/**
* Call `logs(filter)` query
* *
* @param {{search:string, channels:string|string[], events:string|string[], open:boolean, characters:string|string[], limit:number}} filter * @param {{search:string, channels:string|string[], events:string|string[], open:boolean, characters:string|string[], limit:number}} filter
* @returns {Promise<LogHeader[]>} * @returns {Promise<LogHeader[]>}
@ -76,9 +109,9 @@ const logHeaderApi = {
} }
} }
`, {filter}).then(({headers}) => { `, {filter}).then(({headers}) => {
return headers.map(h => new LogHeader(h.id, h.shortId, h.date, h.channelName, h.title, h.description, h.eventName, h.open, h.characters))
return headers.map(h => new LogHeader(h.id, h.shortId, h.date, h.channelName, h.title, h.description, h.eventName, h.open, h.characters))
}) })
}, },
} }
module.exports = {LogHeader, LogHeaderCharacter, logHeaderApi}
module.exports = {LogHeader, LogHeaderCharacter, logHeaderApi, eventNames}
Loading…
Cancel
Save