diff options
| author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-02-24 16:51:37 -0500 |
|---|---|---|
| committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-02-24 16:51:37 -0500 |
| commit | d7696912e007a2b455a2fd5e1974924d2efe83b3 (patch) | |
| tree | f3bcd5f75e8028ff2386321287899ce491f892ea | |
| parent | 68528f7decadccb4c5ed62f3fff10aeff22dcd43 (diff) | |
| download | SMAPI-d7696912e007a2b455a2fd5e1974924d2efe83b3.tar.gz SMAPI-d7696912e007a2b455a2fd5e1974924d2efe83b3.tar.bz2 SMAPI-d7696912e007a2b455a2fd5e1974924d2efe83b3.zip | |
reimplement log parser with serverside parsing and vue.js frontend
| -rw-r--r-- | docs/release-notes.md | 6 | ||||
| -rw-r--r-- | src/SMAPI.Web/Controllers/LogParserController.cs | 35 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/LogParseException.cs | 15 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 222 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs | 24 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs | 24 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs | 24 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs | 47 | ||||
| -rw-r--r-- | src/SMAPI.Web/ViewModels/LogParserModel.cs | 9 | ||||
| -rw-r--r-- | src/SMAPI.Web/Views/LogParser/Index.cshtml | 219 | ||||
| -rw-r--r-- | src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 325 | ||||
| -rw-r--r-- | src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 321 | ||||
| -rw-r--r-- | src/SMAPI.sln | 2 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs | 2 |
14 files changed, 771 insertions, 504 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index 08f945e4..03b6dd77 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,8 +20,14 @@ * Fixed unhelpful error when a translation file has duplicate keys due to case-insensitivity. * Fixed some JSON field names being case-sensitive. +* For the [log parser][]: + * Significantly reduced download size when viewing files with repeated errors. + * Improved parse error handling. + * Fixed 'log started' field showing incorrect date. + * For SMAPI developers: * Overhauled mod DB format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields. + * Reimplemented log parser with serverside parsing and vue.js on the frontend. ## 2.4 * For players: diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 04a11a82..62547deb 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.LogParsing; +using StardewModdingAPI.Web.Framework.LogParsing.Models; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers @@ -52,25 +54,23 @@ namespace StardewModdingAPI.Web.Controllers [HttpGet] [Route("log")] [Route("log/{id}")] - public ViewResult Index(string id = null) + public async Task<ViewResult> Index(string id = null) { - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id)); + // fresh page + if (string.IsNullOrWhiteSpace(id)) + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null)); + + // log page + PasteInfo paste = await this.GetAsync(id); + ParsedLog log = paste.Success + ? new LogParser().Parse(paste.Content) + : new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error }; + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log)); } /*** ** JSON ***/ - /// <summary>Fetch raw text from Pastebin.</summary> - /// <param name="id">The Pastebin paste ID.</param> - [HttpGet, Produces("application/json")] - [Route("log/fetch/{id}")] - public async Task<PasteInfo> GetAsync(string id) - { - PasteInfo response = await this.Pastebin.GetAsync(id); - response.Content = this.DecompressString(response.Content); - return response; - } - /// <summary>Save raw log data.</summary> /// <param name="content">The log content to save.</param> [HttpPost, Produces("application/json"), AllowLargePosts] @@ -85,6 +85,15 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Private methods *********/ + /// <summary>Fetch raw text from Pastebin.</summary> + /// <param name="id">The Pastebin paste ID.</param> + private async Task<PasteInfo> GetAsync(string id) + { + PasteInfo response = await this.Pastebin.GetAsync(id); + response.Content = this.DecompressString(response.Content); + return response; + } + /// <summary>Compress a string.</summary> /// <param name="text">The text to compress.</param> /// <remarks>Derived from <a href="https://stackoverflow.com/a/17993002/262123"/>.</remarks> diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs b/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs new file mode 100644 index 00000000..5d4c8c08 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs @@ -0,0 +1,15 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.LogParsing +{ + /// <summary>An error while parsing the log file which doesn't require a stack trace to troubleshoot.</summary> + internal class LogParseException : Exception + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="message">The user-friendly error message.</param> + public LogParseException(string message) : base(message) { } + } +} diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs new file mode 100644 index 00000000..1c3b5671 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using StardewModdingAPI.Web.Framework.LogParsing.Models; + +namespace StardewModdingAPI.Web.Framework.LogParsing +{ + /// <summary>Parses SMAPI log files.</summary> + public class LogParser + { + /********* + ** Properties + *********/ + /// <summary>A regex pattern matching the start of a SMAPI message.</summary> + private readonly Regex MessageHeaderPattern = new Regex(@"^\[(?<time>\d\d:\d\d:\d\d) (?<level>[a-z]+) +(?<modName>[^\]]+)\] ", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// <summary>A regex pattern matching SMAPI's initial platform info message.</summary> + private readonly Regex InfoLinePattern = new Regex(@"^SMAPI (?<apiVersion>.+) with Stardew Valley (?<gameVersion>.+) on (?<os>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// <summary>A regex pattern matching SMAPI's mod folder path line.</summary> + private readonly Regex ModPathPattern = new Regex(@"^Mods go here: (?<path>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// <summary>A regex pattern matching SMAPI's log timestamp line.</summary> + private readonly Regex LogStartedAtPattern = new Regex(@"^Log started at (?<timestamp>.+) UTC", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// <summary>A regex pattern matching the start of SMAPI's mod list.</summary> + private readonly Regex ModListStartPattern = new Regex(@"^Loaded \d+ mods:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// <summary>A regex pattern matching an entry in SMAPI's mod list.</summary> + private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>.+) by (?<author>.+) \| (?<description>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + + /********* + ** Public methods + *********/ + /// <summary>Parse SMAPI log text.</summary> + /// <param name="logText">The SMAPI log text.</param> + public ParsedLog Parse(string logText) + { + try + { + // skip if empty + if (string.IsNullOrWhiteSpace(logText)) + { + return new ParsedLog + { + IsValid = false, + Error = "The log is empty." + }; + } + + // init log + ParsedLog log = new ParsedLog + { + IsValid = true, + Messages = this.CollapseRepeats(this.GetMessages(logText)).ToArray(), + }; + + // parse log messages + LogModInfo smapiMod = new LogModInfo { Name = "SMAPI", Author = "Pathoschild", Description = "" }; + IDictionary<string, LogModInfo> mods = new Dictionary<string, LogModInfo>(); + bool inModList = false; + foreach (LogMessage message in log.Messages) + { + // collect stats + if (message.Level == LogLevel.Error) + { + if (message.Mod == "SMAPI") + smapiMod.Errors++; + else if (mods.ContainsKey(message.Mod)) + mods[message.Mod].Errors++; + } + + // collect SMAPI metadata + if (message.Mod == "SMAPI") + { + // update flags + if (inModList && !this.ModListEntryPattern.IsMatch(message.Text)) + inModList = false; + + // mod list + if (!inModList && message.Level == LogLevel.Info && this.ModListStartPattern.IsMatch(message.Text)) + inModList = true; + else if (inModList) + { + Match match = this.ModListEntryPattern.Match(message.Text); + string name = match.Groups["name"].Value; + string version = match.Groups["version"].Value; + string author = match.Groups["author"].Value; + string description = match.Groups["description"].Value; + mods[name] = new LogModInfo { Name = name, Author = author, Version = version, Description = description }; + } + + // platform info line + else if (message.Level == LogLevel.Info && this.InfoLinePattern.IsMatch(message.Text)) + { + Match match = this.InfoLinePattern.Match(message.Text); + log.ApiVersion = match.Groups["apiVersion"].Value; + log.GameVersion = match.Groups["gameVersion"].Value; + log.OperatingSystem = match.Groups["os"].Value; + smapiMod.Version = log.ApiVersion; + } + + // mod path line + else if (message.Level == LogLevel.Debug && this.ModPathPattern.IsMatch(message.Text)) + { + Match match = this.ModPathPattern.Match(message.Text); + log.ModPath = match.Groups["path"].Value; + } + + // log UTC timestamp line + else if (message.Level == LogLevel.Trace && this.LogStartedAtPattern.IsMatch(message.Text)) + { + Match match = this.LogStartedAtPattern.Match(message.Text); + log.Timestamp = DateTime.Parse(match.Groups["timestamp"].Value + "Z"); + } + } + } + + // finalise log + log.Mods = new[] { smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray(); + return log; + } + catch (LogParseException ex) + { + return new ParsedLog + { + IsValid = false, + Error = ex.Message, + RawTextIfError = logText + }; + } + catch (Exception ex) + { + return new ParsedLog + { + IsValid = false, + Error = $"Parsing the log file failed. Technical details:\n{ex}", + RawTextIfError = logText + }; + } + } + + + /********* + ** Private methods + *********/ + /// <summary>Collapse consecutive repeats into the previous message.</summary> + /// <param name="messages">The messages to filter.</param> + private IEnumerable<LogMessage> CollapseRepeats(IEnumerable<LogMessage> messages) + { + LogMessage next = null; + foreach (LogMessage message in messages) + { + // new message + if (next == null) + { + next = message; + continue; + } + + // repeat + if (next.Level == message.Level && next.Mod == message.Mod && next.Text == message.Text) + { + next.Repeated++; + continue; + } + + // non-repeat message + yield return next; + next = message; + } + yield return next; + } + + /// <summary>Split a SMAPI log into individual log messages.</summary> + /// <param name="logText">The SMAPI log text.</param> + /// <exception cref="LogParseException">The log text can't be parsed successfully.</exception> + private IEnumerable<LogMessage> GetMessages(string logText) + { + LogMessage message = new LogMessage(); + using (StringReader reader = new StringReader(logText)) + { + while (true) + { + // read data + string line = reader.ReadLine(); + if (line == null) + break; + Match header = this.MessageHeaderPattern.Match(line); + + // validate + if (message.Text == null && !header.Success) + throw new LogParseException("Found a log message with no SMAPI metadata. Is this a SMAPI log file?"); + + // start or continue message + if (header.Success) + { + if (message.Text != null) + yield return message; + + message = new LogMessage + { + Time = header.Groups["time"].Value, + Level = Enum.Parse<LogLevel>(header.Groups["level"].Value, ignoreCase: true), + Mod = header.Groups["modName"].Value, + Text = line.Substring(header.Length) + }; + } + else + message.Text += "\n" + line; + } + + // end last message + if (message.Text != null) + yield return message; + } + } + } +} diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs new file mode 100644 index 00000000..40d21bf8 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Web.Framework.LogParsing.Models +{ + /// <summary>The log severity levels.</summary> + public enum LogLevel + { + /// <summary>Tracing info intended for developers.</summary> + Trace, + + /// <summary>Troubleshooting info that may be relevant to the player.</summary> + Debug, + + /// <summary>Info relevant to the player. This should be used judiciously.</summary> + Info, + + /// <summary>An issue the player should be aware of. This should be used rarely.</summary> + Warn, + + /// <summary>A message indicating something went wrong.</summary> + Error, + + /// <summary>Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue.</summary> + Alert + } +} diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs new file mode 100644 index 00000000..baeac83c --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Web.Framework.LogParsing.Models +{ + /// <summary>A parsed log message.</summary> + public class LogMessage + { + /********* + ** Accessors + *********/ + /// <summary>The local time when the log was posted.</summary> + public string Time { get; set; } + + /// <summary>The log level.</summary> + public LogLevel Level { get; set; } + + /// <summary>The mod name.</summary> + public string Mod { get; set; } + + /// <summary>The log text.</summary> + public string Text { get; set; } + + /// <summary>The number of times this message was repeated consecutively.</summary> + public int Repeated { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs new file mode 100644 index 00000000..2005e61f --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ModInfo.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Web.Framework.LogParsing.Models +{ + /// <summary>Metadata about a mod or content pack in the log.</summary> + public class LogModInfo + { + /********* + ** Accessors + *********/ + /// <summary>The mod name.</summary> + public string Name { get; set; } + + /// <summary>The mod author.</summary> + public string Author { get; set; } + + /// <summary>The mod version.</summary> + public string Version { get; set; } + + /// <summary>The mod description.</summary> + public string Description { get; set; } + + /// <summary>The number of errors logged by this mod.</summary> + public int Errors { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs new file mode 100644 index 00000000..31ef2fe1 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs @@ -0,0 +1,47 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.LogParsing.Models +{ + /// <summary>Parsed metadata for a log.</summary> + public class ParsedLog + { + /********* + ** Accessors + *********/ + /**** + ** Metadata + ****/ + /// <summary>Whether the log file was successfully parsed.</summary> + public bool IsValid { get; set; } + + /// <summary>An error message indicating why the log file is invalid.</summary> + public string Error { get; set; } + + /// <summary>The raw text if <see cref="IsValid"/> is false.</summary> + public string RawTextIfError { get; set; } + + /**** + ** Log data + ****/ + /// <summary>The SMAPI version.</summary> + public string ApiVersion { get; set; } + + /// <summary>The game version.</summary> + public string GameVersion { get; set; } + + /// <summary>The player's operating system.</summary> + public string OperatingSystem { get; set; } + + /// <summary>The mod folder path.</summary> + public string ModPath { get; set; } + + /// <summary>The ISO 8601 timestamp when the log was started.</summary> + public DateTimeOffset Timestamp { get; set; } + + /// <summary>Metadata about installed mods and content packs.</summary> + public LogModInfo[] Mods { get; set; } = new LogModInfo[0]; + + /// <summary>The log messages.</summary> + public LogMessage[] Messages { get; set; } + } +} diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs index b5b3b14c..8c026536 100644 --- a/src/SMAPI.Web/ViewModels/LogParserModel.cs +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -1,3 +1,5 @@ +using StardewModdingAPI.Web.Framework.LogParsing.Models; + namespace StardewModdingAPI.Web.ViewModels { /// <summary>The view model for the log parser page.</summary> @@ -12,6 +14,9 @@ namespace StardewModdingAPI.Web.ViewModels /// <summary>The paste ID.</summary> public string PasteID { get; set; } + /// <summary>The parsed log info.</summary> + public ParsedLog ParsedLog { get; set; } + /********* ** Public methods @@ -22,10 +27,12 @@ namespace StardewModdingAPI.Web.ViewModels /// <summary>Construct an instance.</summary> /// <param name="sectionUrl">The root URL for the log parser controller.</param> /// <param name="pasteID">The paste ID.</param> - public LogParserModel(string sectionUrl, string pasteID) + /// <param name="parsedLog">The parsed log info.</param> + public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog) { this.SectionUrl = sectionUrl; this.PasteID = pasteID; + this.ParsedLog = parsedLog; } } } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 1659de8f..8d1abbb1 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -1,15 +1,29 @@ @{ ViewData["Title"] = "SMAPI log parser"; } +@using Newtonsoft.Json +@using StardewModdingAPI.Web.Framework.LogParsing.Models @model StardewModdingAPI.Web.ViewModels.LogParserModel @section Head { <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180101" /> + <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script> <script src="~/Content/js/log-parser.js?r=20180101"></script> - <style type="text/css" id="modflags"></style> <script> $(function() { - smapi.logParser('@Model.SectionUrl', '@Model.PasteID'); + smapi.logParser({ + logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)), + showPopup: @Json.Serialize(Model.ParsedLog == null), + showMods: @Json.Serialize(Model.ParsedLog?.Mods?.ToDictionary(p => p.Name, p => true), new JsonSerializerSettings { Formatting = Formatting.None }), + showLevels: { + trace: false, + debug: false, + info: true, + alert: true, + warn: true, + error: true + } + }, '@Model.SectionUrl'); }); </script> } @@ -20,99 +34,122 @@ <p id="blurb">This page lets you upload, view, and share a SMAPI log to help troubleshoot mod issues.</p> <input type="button" id="upload-button" value="Share a new log" /> -@if (Model.PasteID != null) +@if (Model.ParsedLog?.IsValid == true) { <h2>Parsed log</h2> -} -<div id="output" class="trace debug"> - @if (Model.PasteID != null) - { - <div id="log-data" style="display: none;"> - <div class="always"> - <table id="gameinfo"> - <caption>Game info:</caption> - <tr> - <td>SMAPI Version</td> - <td id="api-version"></td> - </tr> - <tr> - <td>Game Version</td> - <td id="game-version"></td> - </tr> - <tr> - <td>Platform</td> - <td id="platform"></td> - </tr> - <tr> - <td>Mods path</td> - <td id="mods-path"></td> - </tr> - <tr> - <td>Log started</td> - <td id="log-started"></td> - </tr> - </table> - <br/> - <table id="modslist"> - <caption>Installed Mods: <span id="modlink-r" class="notice btn">Remove all mod filters</span><span class="notice txt"><i>Click any mod to filter</i></span> <span id="modlink-a" class="notice btn txt">Select all</span></caption> - </table> - <div id="filters"> - Filter messages: <span>TRACE</span> | <span>DEBUG</span> | <span class="active">INFO</span> | <span class="active">ALERT</span> | <span class="active">WARN</span> | <span class="active">ERROR</span> - </div> - </div> - <table id="log"></table> - </div> - } - <div id="error" class="color-red"></div> -</div> -<script class="template" id="template-css" type="text/html"> - #output.modfilter:not(.mod-{0}) .mod-{0} { display:none; } #output.modfilter.mod-{0} #modslist tr { background:#ffeeee; } #output.modfilter.mod-{0} #modslist tr#modlink-{0} { background:#eeffee; } -</script> -<script class="template" id="template-modentry" type="text/html"> - <tr id="modlink-{0}"> - <td>{1}</td> - <td>{2}</td> - <td>{3}</td> - <td class={4}>{5}</td> - </tr> -</script> -<script class="template" id="template-logentry" type="text/html"> - <tr class="{0} mod mod-{1}"> - <td>{2}</td> - <td>{3}</td> - <td data-title="{4}">{4}</td> - <td>{5}</td> - </tr> -</script> -<script class="template" id="template-lognotice" type="text/html"> - <tr class="{0} mod-repeat mod mod-{1}"> - <td colspan="3"></td> - <td><i>repeats [{2}] times.</i></td> - </tr> -</script> -<div id="popup-upload" class="popup"> - <h1>Upload log file</h1> - <div class="frame"> - <ol> - <li><a href="https://stardewvalleywiki.com/Modding:Player_FAQs#SMAPI_log" target="_blank">Find your SMAPI log file</a> (not the console text).</li> - <li>Drag the file onto the textbox below (or paste the text in).</li> - <li>Click <em>Parse</em>.</li> - <li>Share the URL of the new page.</li> - </ol> - <textarea id="input" placeholder="Paste or drag the log here"></textarea> - <div class="buttons"> - <input type="button" id="submit" value="Parse" /> - <input type="button" id="cancel" value="Cancel" /> + <div id="output"> + <table id="metadata"> + <caption>Game info:</caption> + <tr> + <td>SMAPI version:</td> + <td>@Model.ParsedLog.ApiVersion</td> + </tr> + <tr> + <td>Game version:</td> + <td>@Model.ParsedLog.GameVersion</td> + </tr> + <tr> + <td>Platform:</td> + <td>@Model.ParsedLog.OperatingSystem</td> + </tr> + <tr> + <td>Mods path:</td> + <td>@Model.ParsedLog.ModPath</td> + </tr> + <tr> + <td>Log started:</td> + <td>@Model.ParsedLog.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time)</td> + </tr> + </table> + <br /> + <table id="mods"> + <caption> + Installed mods: + <span class="notice txt"><i>click any mod to filter</i></span> + <span class="notice btn txt" v-on:click="showAllMods" v-if="stats.modsHidden > 0">show all</span> + <span class="notice btn txt" v-on:click="hideAllMods" v-if="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span> + </caption> + @foreach (var mod in Model.ParsedLog.Mods) + { + <tr v-on:click="toggleMod('@mod.Name')" class="mod-entry" v-bind:class="{ hidden: !showMods['@mod.Name'] }"> + <td><input type="checkbox" v-bind:checked="showMods['@mod.Name']" v-if="anyModsHidden" /></td> + <td>@mod.Version</td> + <td>@mod.Author</td> + @if (mod.Errors == 0) + { + <td class="color-green">no errors</td> + } + else if (mod.Errors == 1) + { + <td class="color-red">@mod.Errors error</td> + } + else + { + <td class="color-red">@mod.Errors errors</td> + } + </tr> + } + </table> + <div id="filters"> + Filter messages: + <span v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> | + <span v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> | + <span v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> | + <span v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> | + <span v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> | + <span v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span> </div> + + <table id="log"> + @foreach (var message in Model.ParsedLog.Messages) + { + string levelStr = @message.Level.ToString().ToLower(); + + <tr class="@levelStr mod" v-if="showMods['@message.Mod'] && showLevels['@levelStr']"> + <td>@message.Time</td> + <td>@message.Level.ToString().ToUpper()</td> + <td data-title="@message.Mod">@message.Mod</td> + <td>@message.Text</td> + </tr> + if (message.Repeated > 0) + { + <tr class="@levelStr mod mod-repeat" v-if="showMods['@message.Mod'] && showLevels['@levelStr']"> + <td colspan="3"></td> + <td><i>repeats [@message.Repeated] times.</i></td> + </tr> + } + } + </table> </div> -</div> -<div id="popup-raw" class="popup"> - <h1>Raw log file</h1> - <div class="frame"> - <textarea id="dataraw"></textarea> - <div class="buttons"> - <input type="button" id="closeraw" value="Close" /> +} +else if (Model.ParsedLog?.IsValid == false) +{ + <h2>Parsed log</h2> + <div id="error" class="color-red"> + <p><strong>We couldn't parse that file, but you can still share the link.</strong></p> + <p>Error details: @Model.ParsedLog.Error</p> + </div> + + <h3>Raw log</h3> + <pre>@Model.ParsedLog.RawTextIfError</pre> +} + +<div id="upload-area"> + <div id="popup-upload" class="popup"> + <h1>Upload log file</h1> + <div class="frame"> + <ol> + <li><a href="https://stardewvalleywiki.com/Modding:Player_FAQs#SMAPI_log" target="_blank">Find your SMAPI log file</a> (not the console text).</li> + <li>Drag the file onto the textbox below (or paste the text in).</li> + <li>Click <em>Parse</em>.</li> + <li>Share the URL of the new page.</li> + </ol> + <textarea id="input" placeholder="Paste or drag the log here"></textarea> + <div class="buttons"> + <input type="button" id="submit" value="Parse" /> + <input type="button" id="cancel" value="Cancel" /> + </div> </div> </div> + <div id="uploader"></div> </div> -<div id="uploader"></div> diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 9f07f9e8..a3be0c85 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -1,21 +1,23 @@ -.mod-repeat { - font-size: 8pt; +/********* +** Main layout +*********/ +input[type="button"] { + font-size: 20px; + border-radius: 5px; + outline: none; + box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2); + cursor: pointer; |
