refactor and document logs

pull/857/head
gabrielburnworth 2018-05-17 13:12:33 -07:00
parent e138e9f28d
commit 490befc4b9
17 changed files with 169 additions and 188 deletions

View File

@ -7,7 +7,8 @@ import {
TaggedWebAppConfig,
TaggedSensor,
TaggedFirmwareConfig,
TaggedPinBinding
TaggedPinBinding,
TaggedLog
} from "../../resources/tagged_resources";
import { ExecutableType } from "../../farm_designer/interfaces";
import { fakeResource } from "../fake_resource";
@ -54,6 +55,22 @@ export function fakeFarmEvent(exe_type: ExecutableType,
});
}
export function fakeLog(): TaggedLog {
return fakeResource("Log", {
id: idCounter++,
message: "Farmbot is up and Running!",
type: "info",
x: 1,
y: 2,
z: 3,
verbosity: 1,
major_version: 5,
minor_version: 1,
channels: ["toast"],
created_at: 1501703421
});
}
export function fakeImage(): TaggedImage {
return fakeResource("Image", {
id: idCounter++,

View File

@ -1,11 +0,0 @@
import { Log } from "../interfaces";
export let log: Log = {
id: 1234567,
message: "Farmbot is up and Running!",
type: "info",
channels: [
"toast"
],
created_at: 1501703421
};

View File

@ -5,12 +5,13 @@ import * as _ from "lodash";
import { init, error } from "farmbot-toastr";
import { NavBar } from "./nav";
import { Everything, Log } from "./interfaces";
import { Everything } from "./interfaces";
import { LoadingPlant } from "./loading_plant";
import { BotState, Xyz } from "./devices/interfaces";
import { ResourceName, TaggedUser } from "./resources/tagged_resources";
import {
selectAllLogs,
ResourceName, TaggedUser, TaggedLog
} from "./resources/tagged_resources";
import {
maybeFetchUser,
maybeGetTimeOffset,
getFirmwareConfig
@ -24,6 +25,7 @@ import { BooleanSetting } from "./session_keys";
import { getPathArray } from "./history";
import { FirmwareConfig } from "./config_storage/firmware_configs";
import { getWebAppConfigValue } from "./config_storage/actions";
import { takeSortedLogs } from "./logs/state_to_props";
/** Remove 300ms delay on touch devices - https://github.com/ftlabs/fastclick */
const fastClick = require("fastclick");
@ -35,7 +37,7 @@ init();
export interface AppProps {
dispatch: Function;
loaded: ResourceName[];
logs: Log[];
logs: TaggedLog[];
user: TaggedUser | undefined;
bot: BotState;
consistent: boolean;
@ -51,12 +53,7 @@ function mapStateToProps(props: Everything): AppProps {
dispatch: props.dispatch,
user: maybeFetchUser(props.resources.index),
bot: props.bot,
logs: _(selectAllLogs(props.resources.index))
.map(x => x.body)
.sortBy("created_at")
.reverse()
.take(250)
.value(),
logs: takeSortedLogs(250, props.resources.index),
loaded: props.resources.loaded,
consistent: !!(props.bot || {}).consistent,
axisInversion: {

View File

@ -1,7 +1,7 @@
import { fetchNewDevice, getDevice } from "../device";
import { dispatchNetworkUp, dispatchNetworkDown } from "./index";
import { Log } from "../interfaces";
import { ALLOWED_CHANNEL_NAMES, Farmbot, BotStateTree } from "farmbot";
import { Farmbot, BotStateTree } from "farmbot";
import { noop, throttle } from "lodash";
import { success, error, info, warning } from "farmbot-toastr";
import { HardwareState } from "../devices/interfaces";
@ -25,6 +25,7 @@ import { getWebAppConfigValue } from "../config_storage/actions";
import { BooleanSetting } from "../session_keys";
import { versionOK } from "../util";
import { onLogs } from "./log_handlers";
import { ChannelName } from "../sequences/interfaces";
export const TITLE = "New message from bot";
/** TODO: This ought to be stored in Redux. It is here because of historical
@ -42,9 +43,9 @@ export let incomingStatus = (statusMessage: HardwareState) =>
/** Determine if an incoming log has a certain channel. If it is, execute the
* supplied callback. */
export function actOnChannelName(
log: Log, channelName: ALLOWED_CHANNEL_NAMES, cb: (log: Log) => void) {
log: Log, channelName: ChannelName, cb: (log: Log) => void) {
const CHANNELS: keyof Log = "channels";
const chanList: string[] = log[CHANNELS] || ["ERROR FETCHING CHANNELS"];
const chanList: ChannelName[] = log[CHANNELS] || ["ERROR FETCHING CHANNELS"];
return log && (chanList.includes(channelName) ? cb(log) : noop());
}
@ -96,7 +97,7 @@ export function readStatus() {
export const onOffline = () => {
dispatchNetworkDown("user.mqtt");
error(t(Content.MQTT_DISCONNECTED),t("Error"));
error(t(Content.MQTT_DISCONNECTED), t("Error"));
};
export const changeLastClientConnected = (bot: Farmbot) => () => {

View File

@ -5,6 +5,7 @@ import { Color as FarmBotJsColor, ALLOWED_MESSAGE_TYPES, PlantStage } from "farm
import { DraggableState } from "./draggable/interfaces";
import { PeripheralState } from "./controls/peripherals/interfaces";
import { RestResources } from "./resources/interfaces";
import { ChannelName } from "./sequences/interfaces";
/** Regimens and sequences may have a "color" which determines how it looks
in the UI. Only certain colors are valid. */
@ -47,7 +48,7 @@ export interface Log {
verbosity?: number;
major_version?: number;
minor_version?: number;
channels: string[];
channels: ChannelName[];
created_at: number;
}

View File

@ -28,37 +28,20 @@ import * as React from "react";
import { mount } from "enzyme";
import { Logs } from "../index";
import { ToolTips } from "../../constants";
import { TaggedLog, SpecialStatus } from "../../resources/tagged_resources";
import { Log } from "../../interfaces";
import { generateUuid } from "../../resources/util";
import { TaggedLog } from "../../resources/tagged_resources";
import { bot } from "../../__test_support__/fake_state/bot";
import { Dictionary } from "farmbot";
import { NumericSetting } from "../../session_keys";
import { fakeLog } from "../../__test_support__/fake_state/resources";
describe("<Logs />", () => {
function fakeLogs(): TaggedLog[] {
const logs: Log[] = [{
id: 1,
created_at: -1,
message: "Fake log message 1",
type: "info",
channels: []
},
{
id: 2,
created_at: -1,
message: "Fake log message 2",
type: "success",
channels: []
}];
return logs.map((body: Log): TaggedLog => {
return {
kind: "Log",
uuid: generateUuid(body.id, "Log"),
specialStatus: SpecialStatus.SAVED,
body
};
});
const log1 = fakeLog();
log1.body.message = "Fake log message 1";
const log2 = fakeLog();
log2.body.message = "Fake log message 2";
log2.body.type = "success";
return [log1, log2];
}
const fakeProps = () => {
@ -94,6 +77,8 @@ describe("<Logs />", () => {
it("shows position", () => {
const p = fakeProps();
p.logs[0].body.x = 100;
p.logs[0].body.y = undefined;
p.logs[0].body.z = undefined;
p.logs[1].body.x = 0;
p.logs[1].body.y = 1;
p.logs[1].body.z = 2;

View File

@ -1,29 +1,13 @@
import { mapStateToProps } from "../state_to_props";
import { fakeState } from "../../__test_support__/fake_state";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { TaggedLog, SpecialStatus } from "../../resources/tagged_resources";
import { Log } from "../../interfaces";
import { generateUuid } from "../../resources/util";
import { TaggedLog } from "../../resources/tagged_resources";
import { times } from "lodash";
import { fakeFbosConfig } from "../../__test_support__/fake_state/resources";
import { fakeFbosConfig, fakeLog } from "../../__test_support__/fake_state/resources";
describe("mapStateToProps()", () => {
function fakeLogs(count: number): TaggedLog[] {
const log: Log = {
id: 1,
created_at: -1,
message: "Fake log message",
type: "info",
channels: []
};
return times(count, () => log).map((body: Log): TaggedLog => {
return {
kind: "Log",
uuid: generateUuid(body.id, "Log"),
specialStatus: SpecialStatus.SAVED,
body
};
});
return times(count, fakeLog);
}
it("returns limited number of logs", () => {

View File

@ -1,13 +1,15 @@
import * as React from "react";
import { LogsFilterMenuProps, LogsState } from "../interfaces";
import { LogsFilterMenuProps } from "../interfaces";
import * as _ from "lodash";
import { Slider } from "@blueprintjs/core";
import { t } from "i18next";
import { Filters } from "../interfaces";
export const LogsFilterMenu = (props: LogsFilterMenuProps) => {
const btnColor = (x: keyof LogsState) => props.state[x] != 0
/** Filter level 0: logs hidden. */
const btnColor = (x: keyof Filters) => props.state[x] != 0
? "green" : "red";
/** Set the filter level to the same value for all log message types. */
const setAll = (level: number) => () => {
["success", "busy", "warn", "error", "info", "fun", "debug"]
.map((x: keyof Filters) => props.setFilterLevel(x)(level));
@ -26,7 +28,7 @@ export const LogsFilterMenu = (props: LogsFilterMenuProps) => {
</fieldset>
{Object.keys(props.state)
.filter(x => { if (!(x == "autoscroll")) { return x; } })
.map((logType: keyof LogsState) => {
.map((logType: keyof Filters) => {
return <fieldset key={logType}>
<label>
<div className={`saucer ${logType}`} />

View File

@ -1,34 +1,36 @@
import * as React from "react";
import { t } from "i18next";
import { TaggedLog } from "../../resources/tagged_resources";
import { LogsState, LogsTableProps } from "../interfaces";
import { LogsState, LogsTableProps, Filters } from "../interfaces";
import { formatLogTime } from "../index";
import * as _ from "lodash";
import { Classes } from "@blueprintjs/core";
import { isNumber, startCase } from "lodash";
interface LogsRowProps {
tlog: TaggedLog;
state: LogsState;
timeOffset: number;
}
/** A log is displayed in a single row of the logs table. */
const LogsRow = ({ tlog, timeOffset }: LogsRowProps) => {
const log = tlog.body;
const { x, y, z, verbosity, type } = log;
const time = formatLogTime(log.created_at, timeOffset);
return <tr key={tlog.uuid}>
const { uuid } = tlog;
const { x, y, z, verbosity, type, created_at, message } = tlog.body;
const time = formatLogTime(created_at, timeOffset);
return <tr key={uuid}>
<td>
<div className={`saucer ${type}`}>
<p>
{verbosity}
</p>
</div>
{_.startCase(type)}
{startCase(type)}
</td>
<td>
{log.message || "Loading"}
{message || "Loading"}
</td>
<td>
{
(_.isNumber(x) && _.isNumber(y) && _.isNumber(z))
(isNumber(x) && isNumber(y) && isNumber(z))
? `${x}, ${y}, ${z}`
: "Unknown"
}
@ -38,12 +40,14 @@ const LogsRow = ({ tlog, timeOffset }: LogsRowProps) => {
</td>
</tr>;
};
const LOG_TABLE_CLASS = [
Classes.HTML_TABLE,
Classes.HTML_TABLE_STRIPED,
"logs-table"
].join(" ");
/** All log messages with select data in table form for display in the app. */
export const LogsTable = (props: LogsTableProps) => {
return <table className={LOG_TABLE_CLASS}>
<thead>
@ -55,26 +59,34 @@ export const LogsTable = (props: LogsTableProps) => {
</tr>
</thead>
<tbody>
{filterByVerbosity(props.state, props.logs)
{filterByVerbosity(getFilterLevel(props.state), props.logs)
.map((log: TaggedLog) => {
return <LogsRow
key={log.uuid}
tlog={log}
state={props.state}
timeOffset={props.timeOffset} />;
})}
</tbody>
</table>;
};
const filterByVerbosity = (state: LogsState, logs: TaggedLog[]) => {
return logs
.filter((log: TaggedLog) => {
const { type, verbosity } = log.body;
const filterLevel = state[type as keyof LogsState];
const displayLog = verbosity
? verbosity <= filterLevel
: filterLevel != 0;
return displayLog;
});
};
/** Get current verbosity filter level for a message type from LogsState. */
const getFilterLevel = (state: LogsState) =>
(type: keyof Filters): number => {
const filterLevel = state[type as keyof Filters];
return isNumber(filterLevel) ? filterLevel : 1;
};
/** Filter TaggedLogs by verbosity level using a fetch filter level function. */
export const filterByVerbosity =
(getLevelFor: (type: keyof Filters) => number, logs: TaggedLog[]) => {
return logs
.filter((log: TaggedLog) => {
const { type, verbosity } = log.body;
const filterLevel = getLevelFor(type);
const displayLog = verbosity
? verbosity <= filterLevel
: filterLevel != 0;
return displayLog;
});
};

View File

@ -4,9 +4,7 @@ import { Help } from "../../ui/index";
import { ToolTips } from "../../constants";
import { ToggleButton } from "../../controls/toggle_button";
import { updateConfig } from "../../devices/actions";
import {
LogSettingProps, LogsSettingsMenuProps, LogsState
} from "../interfaces";
import { LogSettingProps, LogsSettingsMenuProps, Filters } from "../interfaces";
import { Session, safeNumericSetting } from "../../session";
import { ConfigurationName } from "farmbot";
@ -54,7 +52,8 @@ const FIRMWARE_LOG_SETTINGS = (): LogSettingRecord[] => [
const LogSetting = (props: LogSettingProps) => {
const { label, setting, toolTip, setFilterLevel, sourceFbosConfig } = props;
const updateMinFilterLevel = (name: keyof LogsState, level: number) => {
/** Update the current filter level to a minimum needed for log display. */
const updateMinFilterLevel = (name: keyof Filters, level: number) => {
const currentLevel =
Session.deprecatedGetNum(safeNumericSetting(name + "_log")) || 0;
if (currentLevel < level) { setFilterLevel(name)(level); }

View File

@ -5,7 +5,7 @@ import { Col, Row, Page, ToolTip } from "../ui/index";
import { mapStateToProps } from "./state_to_props";
import { t } from "i18next";
import { Popover, Position } from "@blueprintjs/core";
import { LogsState, LogsProps } from "./interfaces";
import { LogsState, LogsProps, Filters } from "./interfaces";
import { ToolTips } from "../constants";
import { LogsSettingsMenu } from "./components/settings_menu";
import { LogsFilterMenu } from "./components/filter_menu";
@ -15,11 +15,14 @@ import { isUndefined } from "lodash";
import { NumericSetting } from "../session_keys";
import { NumberConfigKey } from "../config_storage/web_app_configs";
/** Format log date and time for display in the app. */
export const formatLogTime = (created_at: number, timeoffset: number) =>
moment.unix(created_at).utcOffset(timeoffset).format("MMM D, h:mma");
@connect(mapStateToProps)
export class Logs extends React.Component<LogsProps, Partial<LogsState>> {
/** Initialize log type verbosity level to the configured or default value. */
initialize = (name: NumberConfigKey, defaultValue: number): number => {
const currentValue = Session.deprecatedGetNum(safeNumericSetting(name));
if (isUndefined(currentValue)) {
@ -41,33 +44,31 @@ export class Logs extends React.Component<LogsProps, Partial<LogsState>> {
debug: this.initialize(NumericSetting.debug_log, 1),
};
toggle = (name: keyof LogsState) => {
switch (this.state[name]) {
case 0:
return () => {
this.setState({ [name]: 1 });
Session.deprecatedSetNum(safeNumericSetting(name + "_log"), 1);
};
default:
return () => {
this.setState({ [name]: 0 });
Session.deprecatedSetNum(safeNumericSetting(name + "_log"), 0);
};
}
/** Toggle display of a log type. Verbosity level 0 hides all, 3 shows all.*/
toggle = (name: keyof Filters) => {
// If log type is off, set it to verbosity level 1, otherwise turn it off
const newSetting = this.state[name] === 0 ? 1 : 0;
return () => {
this.setState({ [name]: newSetting });
Session.deprecatedSetNum(safeNumericSetting(name + "_log"), newSetting);
};
}
setFilterLevel = (name: keyof LogsState) => {
/** Set log type filter level. i.e., level 2 shows verbosity 2 and lower.*/
setFilterLevel = (name: keyof Filters) => {
return (value: number) => {
this.setState({ [name]: value });
Session.deprecatedSetNum(safeNumericSetting(name + "_log"), value);
};
};
/** Determine if log type filters are active. */
get filterActive() {
const filterKeys = Object.keys(this.state)
.filter(x => !(x === "autoscroll"));
const filterValues = filterKeys
.map((key: keyof LogsState) => this.state[key]);
.map((key: keyof Filters) => this.state[key]);
// Filters active if every log type level is not equal to 3 (max verbosity)
return !filterValues.every(x => x == 3);
}

View File

@ -1,6 +1,6 @@
import { TaggedLog } from "../resources/tagged_resources";
import { BotState, SourceFbosConfig } from "../devices/interfaces";
import { ConfigurationName } from "farmbot";
import { ConfigurationName, ALLOWED_MESSAGE_TYPES } from "farmbot";
export interface LogsProps {
logs: TaggedLog[];
@ -10,15 +10,7 @@ export interface LogsProps {
sourceFbosConfig: SourceFbosConfig;
}
export interface Filters {
success: number;
busy: number;
warn: number;
error: number;
info: number;
fun: number;
debug: number;
}
export type Filters = Record<ALLOWED_MESSAGE_TYPES, number>;
export interface LogsState extends Filters {
autoscroll: boolean;

View File

@ -7,6 +7,18 @@ import {
} from "../devices/components/source_config_value";
import { getFbosConfig } from "../resources/selectors_by_kind";
import { validFbosConfig } from "../util";
import { ResourceIndex } from "../resources/interfaces";
import { TaggedLog } from "../resources/tagged_resources";
/** Take the specified number of logs after sorting by time created. */
export function takeSortedLogs(
numberOfLogs: number, ri: ResourceIndex): TaggedLog[] {
return _(selectAllLogs(ri))
.sortBy("body.created_at")
.reverse()
.take(numberOfLogs)
.value();
}
export function mapStateToProps(props: Everything): LogsProps {
const { hardware } = props.bot;
@ -14,11 +26,7 @@ export function mapStateToProps(props: Everything): LogsProps {
return {
dispatch: props.dispatch,
sourceFbosConfig: sourceFbosConfigValue(fbosConfig, hardware.configuration),
logs: _(selectAllLogs(props.resources.index))
.sortBy("body.created_at")
.reverse()
.take(250)
.value(),
logs: takeSortedLogs(250, props.resources.index),
bot: props.bot,
timeOffset: maybeGetTimeOffset(props.resources.index)
};

View File

@ -3,7 +3,6 @@ import { shallow } from "enzyme";
import { NavBar } from "../index";
import { bot } from "../../__test_support__/fake_state/bot";
import { log } from "../../__test_support__/log";
import { taggedUser } from "../../__test_support__/user";
describe("NavBar", () => {
@ -13,7 +12,7 @@ describe("NavBar", () => {
<NavBar
timeOffset={0}
consistent={true}
logs={[log]}
logs={[]}
bot={bot}
user={taggedUser}
dispatch={jest.fn()} />
@ -25,7 +24,7 @@ describe("NavBar", () => {
const wrapper = shallow(<NavBar
timeOffset={0}
consistent={true}
logs={[log]}
logs={[]}
bot={bot}
user={taggedUser}
dispatch={jest.fn()} />);

View File

@ -22,21 +22,14 @@ jest.mock("../../session", () => {
import * as React from "react";
import { mount } from "enzyme";
import { TickerList } from "../ticker_list";
import { Log } from "../../interfaces";
import { Dictionary } from "farmbot";
import { fakeLog } from "../../__test_support__/fake_state/resources";
describe("<TickerList />", () => {
const log: Log = {
id: 1234567,
message: "Farmbot is up and Running!",
type: "info",
channels: [
"toast"
],
created_at: 1501703421
};
const log = fakeLog();
log.body.message = "Farmbot is up and Running!";
log.body.created_at = 1501703421;
it("shows log message and datetime", () => {
const wrapper = mount(
@ -85,7 +78,7 @@ describe("<TickerList />", () => {
it("all logs filtered out", () => {
["success", "busy", "warn", "error", "info", "fun", "debug"]
.map(logType => mockStorj[logType + "_log"] = 0);
log.verbosity = 1;
log.body.verbosity = 1;
const wrapper = mount(<TickerList
logs={[log]} tickerListOpen={false} toggle={jest.fn()} timeOffset={0} />);
const labels = wrapper.find("label");

View File

@ -1,6 +1,5 @@
import { BotState } from "../devices/interfaces";
import { Log } from "../interfaces";
import { TaggedUser } from "../resources/tagged_resources";
import { TaggedUser, TaggedLog } from "../resources/tagged_resources";
export interface NavButtonProps {
user: TaggedUser | undefined;
@ -12,7 +11,7 @@ export interface NavButtonProps {
export interface NavBarProps {
consistent: boolean;
logs: Log[];
logs: TaggedLog[];
bot: BotState;
user: TaggedUser | undefined;
dispatch: Function;
@ -34,7 +33,7 @@ export interface MobileMenuProps {
export interface TickerListProps {
toggle: (property: keyof NavBarState) => ToggleEventHandler;
logs: Log[]
logs: TaggedLog[]
tickerListOpen: boolean;
timeOffset: number;
}

View File

@ -1,61 +1,63 @@
import * as React from "react";
import { Collapse } from "@blueprintjs/core";
import { Markdown } from "../ui/index";
import { Log } from "../interfaces";
import { TickerListProps } from "./interfaces";
import { Link } from "react-router";
import { t } from "i18next";
import { formatLogTime } from "../logs/index";
import { Session, safeNumericSetting } from "../session";
import { isNumber } from "lodash";
import { ErrorBoundary } from "../error_boundary";
import { ALLOWED_MESSAGE_TYPES } from "farmbot";
import { filterByVerbosity } from "../logs/components/logs_table";
import { TaggedLog, SpecialStatus } from "../resources/tagged_resources";
import { isNumber } from "lodash";
const logFilter = (log: Log): Log | undefined => {
const { type, verbosity } = log;
const filterLevel = Session.deprecatedGetNum(safeNumericSetting(type + "_log"));
const filterLevelCompare = isNumber(filterLevel) ? filterLevel : 1;
const displayLog = verbosity
? verbosity <= filterLevelCompare
: filterLevel != 0;
const whitelisted = !log.message.toLowerCase().includes("filtered");
if (displayLog && whitelisted) {
return log;
}
return;
/** Get current verbosity filter level for a message type from WebAppConfig. */
const getFilterLevel = (type: ALLOWED_MESSAGE_TYPES): number => {
const filterLevel =
Session.deprecatedGetNum(safeNumericSetting(type + "_log"));
return isNumber(filterLevel) ? filterLevel : 1;
};
const getfirstTickerLog = (logs: Log[]): Log => {
if (logs.length == 0) {
return {
message: t("No logs yet."),
/** Generate a fallback TaggedLog to display in the first line of the ticker. */
const generateFallbackLog = (uuid: string, message: string): TaggedLog => {
return {
kind: "Log",
uuid,
specialStatus: SpecialStatus.SAVED,
body: {
message,
type: "debug",
verbosity: -1,
channels: [], created_at: NaN
};
}
};
};
/** Choose the log to display in the first line of the ticker. */
const getfirstTickerLog = (logs: TaggedLog[]): TaggedLog => {
if (logs.length == 0) {
return generateFallbackLog("no_logs_yet", t("No logs yet."));
} else {
const filteredLogs = logs.filter(log => logFilter(log));
const filteredLogs = filterByVerbosity(getFilterLevel, logs);
if (filteredLogs.length > 0) {
return filteredLogs[0];
} else {
return {
message: t("No logs to display. Visit Logs page to view filters."),
type: "debug",
verbosity: -1,
channels: [], created_at: NaN
};
return generateFallbackLog("no_logs_to_display",
t("No logs to display. Visit Logs page to view filters."));
}
}
};
const Ticker = (log: Log, index: number, timeOffset: number) => {
const time = formatLogTime(log.created_at, timeOffset);
const { type } = log;
// TODO: Should utilize log's `uuid` instead of index.
return <div key={index} className="status-ticker-wrapper">
/** Format a single log for display in the ticker. */
const Ticker = (log: TaggedLog, timeOffset: number) => {
const { message, type, created_at } = log.body;
const time = formatLogTime(created_at, timeOffset);
return <div key={log.uuid} className="status-ticker-wrapper">
<div className={`saucer ${type}`} />
<label className="status-ticker-message">
<Markdown>
{log.message.replace(/\s+/g, " ") || "Loading"}
{message.replace(/\s+/g, " ") || "Loading"}
</Markdown>
</label>
<label className="status-ticker-created-at">
@ -64,18 +66,18 @@ const Ticker = (log: Log, index: number, timeOffset: number) => {
</div>;
};
/** The logs ticker, with closed/open views, and a link to the Logs page. */
export let TickerList = (props: TickerListProps) => {
return <ErrorBoundary>
<div className="ticker-list" onClick={props.toggle("tickerListOpen")} >
<div className="first-ticker">
{Ticker(getfirstTickerLog(props.logs), -1, props.timeOffset)}
{Ticker(getfirstTickerLog(props.logs), props.timeOffset)}
</div>
<Collapse isOpen={props.tickerListOpen}>
{props
.logs
{filterByVerbosity(getFilterLevel, props.logs)
// Don't use first log again since it's already displayed in first row
.filter((_, index) => index !== 0)
.filter((log) => logFilter(log))
.map((log: Log, index: number) => Ticker(log, index, props.timeOffset))}
.map((log: TaggedLog) => Ticker(log, props.timeOffset))}
</Collapse>
<Collapse isOpen={props.tickerListOpen}>
<Link to={"/app/logs"}>