From c1ed73a8b4def99462b189beba0053f3f84ec094 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 7 Nov 2024 15:49:16 -0600 Subject: [PATCH] Website: Update links in query library and syntax highlighting on policy pages. (#23591) Closes: #23516 Closes: #23517 Changes: - Updated the headings on the /queries page to be clickable links - Updated the query detail page to highlight osquery tables/columns in queries - Updated code blocks on the query details page to prevent the copy button from covering the content in the code block. --- website/api/controllers/view-query-detail.js | 25 +++++++++ website/assets/js/pages/query-detail.page.js | 54 +++++++++++++++++-- website/assets/styles/pages/query-detail.less | 37 +++++++++++-- .../assets/styles/pages/query-library.less | 17 +++--- website/views/pages/query-library.ejs | 6 +-- 5 files changed, 121 insertions(+), 18 deletions(-) diff --git a/website/api/controllers/view-query-detail.js b/website/api/controllers/view-query-detail.js index c495092888..b02ba24ee9 100644 --- a/website/api/controllers/view-query-detail.js +++ b/website/api/controllers/view-query-detail.js @@ -34,6 +34,29 @@ module.exports = { throw 'notFound'; } + // Find the related osquery table documentation for tables used in this query, and grab the keywordsForSyntaxHighlighting from each table used. + let allTablesInformation = _.filter(sails.config.builtStaticContent.markdownPages, (pageInfo)=>{ + return _.startsWith(pageInfo.url, '/tables/'); + }); + // Get all the osquery table names, we'll use this list to determine which tables are used. + let allTableNames = _.pluck(allTablesInformation, 'title'); + // Create an array of words in the query. + let queryWords = _.words(query.query, /[^ ]+/g); + let columnNamesForSyntaxHighlighting = []; + let tableNamesForSyntaxHighlighting = []; + // Get all of the words that appear in both arrays + let intersectionBetweenQueryWordsAndTableNames = _.intersection(queryWords, allTableNames); + // For each matched osquery table, add the keywordsForSyntaxHighlighting and the names of the tables used into two arrays. + for(let tableName of intersectionBetweenQueryWordsAndTableNames) { + let tableMentionedInThisQuery = _.find(sails.config.builtStaticContent.markdownPages, {title: tableName}); + tableNamesForSyntaxHighlighting.push(tableMentionedInThisQuery.title); + let keyWordsForThisTable = tableMentionedInThisQuery.keywordsForSyntaxHighlighting; + columnNamesForSyntaxHighlighting = columnNamesForSyntaxHighlighting.concat(keyWordsForThisTable); + } + // Remove the table names from the array of column names to highlight. + columnNamesForSyntaxHighlighting = _.difference(columnNamesForSyntaxHighlighting, tableNamesForSyntaxHighlighting); + + // Setting the meta title and description of this page using the query object, and falling back to a generic title or description if query.name or query.description are missing. let pageTitleForMeta = query.name ? query.name + ' | Query details' : 'Query details'; let pageDescriptionForMeta = query.description ? query.description : 'View more information about a query in Fleet\'s standard query library'; @@ -43,6 +66,8 @@ module.exports = { queryLibraryYmlRepoPath: sails.config.builtStaticContent.queryLibraryYmlRepoPath, pageTitleForMeta, pageDescriptionForMeta, + columnNamesForSyntaxHighlighting, + tableNamesForSyntaxHighlighting, algoliaPublicKey: sails.config.custom.algoliaPublicKey, }; diff --git a/website/assets/js/pages/query-detail.page.js b/website/assets/js/pages/query-detail.page.js index d03b89b4f2..8622000bbc 100644 --- a/website/assets/js/pages/query-detail.page.js +++ b/website/assets/js/pages/query-detail.page.js @@ -27,10 +27,58 @@ parasails.registerPage('query-detail', { }, }); } - - $('pre code').each((i, block) => { - window.hljs.highlightElement(block); + let columnNamesForThisQuery = []; + let tableNamesForThisQuery = []; + if(this.columnNamesForSyntaxHighlighting){ + columnNamesForThisQuery = this.columnNamesForSyntaxHighlighting; + } + if(this.tableNamesForSyntaxHighlighting){ + tableNamesForThisQuery = this.tableNamesForSyntaxHighlighting; + } + // Sorting the arrays of keywords by length to match larger keywords first. + columnNamesForThisQuery = columnNamesForThisQuery.sort((a,b)=>{ + return a.length < b.length ? 1 : -1; }); + tableNamesForThisQuery = tableNamesForThisQuery.sort((a,b)=>{ + return a.length < b.length ? 1 : -1; + }); + (()=>{ + $('pre code').each((i, block) => { + let tableNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight + for(let tableName of tableNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the + for(let match of block.innerHTML.match(tableName+' ')||[]){ + tableNamesToHighlight.push(match); + } + } + // Now iterate through the tableNamesToHighlight, replacing all matches in the elements innerHTML. + let replacementHMTL = block.innerHTML; + for(let keywordInExample of tableNamesToHighlight) { + let regexForThisExample = new RegExp(keywordInExample, 'g'); + replacementHMTL = replacementHMTL.replace(regexForThisExample, ''+_.trim(keywordInExample)+' '); + } + $(block).html(replacementHMTL); + let columnNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight + for(let columnName of columnNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the + for(let match of block.innerHTML.match(columnName)||[]){ + columnNamesToHighlight.push(match); + } + } + + for(let keywordInExample of columnNamesToHighlight) { + let regexForThisExample = new RegExp(keywordInExample, 'g'); + replacementHMTL = replacementHMTL.replace(regexForThisExample, ''+_.trim(keywordInExample)+''); + } + $(block).html(replacementHMTL); + window.hljs.highlightElement(block); + // After we've highlighted our keywords, we'll highlight the rest of the codeblock + // If this example is a single-line, we'll do some basic formatting to make it more human-readable. + if($(block)[0].innerText.match(/\n/gmi)){ + $(block).addClass('has-linebreaks'); + } else { + $(block).addClass('no-linebreaks'); + } + }); + })(); $('[purpose="copy-button"]').on('click', async function() { let code = $(this).siblings('pre').find('code').text(); $(this).addClass('copied'); diff --git a/website/assets/styles/pages/query-detail.less b/website/assets/styles/pages/query-detail.less index ab5a75d544..8026d382f4 100644 --- a/website/assets/styles/pages/query-detail.less +++ b/website/assets/styles/pages/query-detail.less @@ -249,11 +249,11 @@ position: absolute; top: 11px; right: 10px; - // padding: 9px; border-radius: 8px; height: 32px; width: 32px; background: url('/images/icon-copy-16x16@2x.png'); + background-color: #F9FAFC; background-size: 14px 14px; background-position: center; background-repeat: no-repeat; @@ -273,7 +273,7 @@ pre { width: 100%; max-width: 100%; - padding: 16px 24px; + padding: 16px 44px 16px 24px; border: 1px solid #E2E4EA; background: #F9FAFC; border-radius: 4px; @@ -281,6 +281,12 @@ margin-bottom: 24px; code { color: #515774; + &.has-linebreaks { + white-space: pre; + } + &.no-linebreaks { + white-space: normal; + } font-family: 'Source Code Pro'; font-size: 14px; font-weight: 400; @@ -288,8 +294,33 @@ .hljs-keyword { // SQL keywords (SELECT, FROM, WHERE, IN, etc.) color: #AE6DDF; } + [purpose='line-break']:not(:first-of-type)::before { + content: '\a'; + } + .hljs-attr { // For table and column names + .hljs-keyword { + color: #FFF; + } + .hljs-string { // For words wrapped in quotation marks + color: #FFF; + } + color: #FFF; + background-color: #AE6DDF; + border-radius: 3px; + white-space: pre; + vertical-align: baseline; + span { + padding: 0; + } + } + .hljs-number { + color: #f5871f; + } .hljs-string { // For words wrapped in quotation marks - color: #3DB67B; + color: #4fd061; + .hljs-keyword { + color: #4fd061; + } } background-color: @ui-off-white; border: none; diff --git a/website/assets/styles/pages/query-library.less b/website/assets/styles/pages/query-library.less index e43b876c4f..7560755e2b 100644 --- a/website/assets/styles/pages/query-library.less +++ b/website/assets/styles/pages/query-library.less @@ -103,10 +103,6 @@ height: 16px; margin-right: 8px; } - // span { - // padding-left: 15px; - // padding-right: 0px; - // } background: #FFF; &::placeholder { font-size: 16px; @@ -188,11 +184,14 @@ max-width: 663px; } [purpose='policy-name'] { - color: #192147; - font-size: 20px; - font-weight: 800; - line-height: 24px; - margin-bottom: 8px; + a { + text-decoration: none; + color: #192147; + font-size: 20px; + font-weight: 800; + line-height: 24px; + margin-bottom: 8px; + } } [purpose='policy-description'] { margin-top: 16px; diff --git a/website/views/pages/query-library.ejs b/website/views/pages/query-library.ejs index 273b8a0480..038087b207 100644 --- a/website/views/pages/query-library.ejs +++ b/website/views/pages/query-library.ejs @@ -53,7 +53,7 @@
-

<%- policy.name %>

+

<%- policy.name %>

<% if(policy.tags.includes('premium')) {%>
PREMIUM
<% } %> <% if(policy.requiresMdm) {%>
mdm-required
<% } %>
@@ -77,7 +77,7 @@
-

<%- policy.name %>

+

<%- policy.name %>

<% if(policy.tags.includes('premium')) {%>
PREMIUM
<% } %> <% if(policy.requiresMdm) {%>
mdm-required
<% } %>
@@ -101,7 +101,7 @@
-

<%- policy.name %>

+

<%- policy.name %>

<% if(policy.tags.includes('premium')) {%>
PREMIUM
<% } %> <% if(policy.requiresMdm) {%>
mdm-required
<% } %>