From 71f53e6f4ea577263fc2d7614745d70b51d7dee0 Mon Sep 17 00:00:00 2001 From: gillespi314 <73313222+gillespi314@users.noreply.github.com> Date: Thu, 3 Jun 2021 19:34:40 -0500 Subject: [PATCH] Query library improvements (#945) * Add contributor avatars to query-detail page * Add check for contributors; style elements * Add GitHub avatars, style css, reorder page script * Add os logos, adjust styles * Add mobile styles, refactor scripts, prettier * Update img paths, fix linting errors --- website/assets/images/chevron-down-9x6@2x.png | Bin 0 -> 332 bytes .../assets/images/icon-search-16x16@2x.png | Bin 0 -> 814 bytes website/assets/js/pages/query-detail.page.js | 32 +++- website/assets/js/pages/query-library.page.js | 148 ++++++++++++++---- website/assets/styles/pages/query-detail.less | 68 +++++++- .../assets/styles/pages/query-library.less | 126 +++++++++++++-- website/views/pages/query-detail.ejs | 78 +++++++-- website/views/pages/query-library.ejs | 79 +++++++--- 8 files changed, 450 insertions(+), 81 deletions(-) create mode 100644 website/assets/images/chevron-down-9x6@2x.png create mode 100644 website/assets/images/icon-search-16x16@2x.png diff --git a/website/assets/images/chevron-down-9x6@2x.png b/website/assets/images/chevron-down-9x6@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6750af9178ff55e7a3b99b36ded967fbc8c20366 GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&J!3HGrh2HJ~Qk(@Ik;M!QVyYm_=ozH)0Vv2= z9OUlAuAXTzQ){5@o)@^+hQV$Aa@JpnDQfC4qARH+z!3x>Xb ze(uXA^ZmYZnCtJvK8Y>HR&rYXigVc4dl%L>esRCxte3c^QsJ9ckC;}=v?NK&hpT=__yqgu9XHZXBI_lZu{%$(RNZm^-9ujt9+){`iI^Ho-yC_ ZhW&45Ls6VtvOmzX44$rjF6*2Ung9mFd?^3` literal 0 HcmV?d00001 diff --git a/website/assets/images/icon-search-16x16@2x.png b/website/assets/images/icon-search-16x16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fb6d1bb3d0abbcd110be8187ce91ee538307aef1 GIT binary patch literal 814 zcmV+}1JV46P)eo}K~#7Fy;n_c z(?Asd#tths;RKiyklr9YLD?fUWRKJs8Y;vh&Z<(A;zVMfq=-E|LDL)L2ACUU0}?cz z;n_|@IzRR}hJ1_Int3ztoA<`=ePOUb5Kiikx%-MacrJh@1M2jwXR~ZU?-97Tj-7b^ z-fu@J0aoZ3wG#%>`RlN7!|5I3zAX_Wf#Jtl3yh6y9ZOhpj{e!(<57{JmcV8BxyK;_ zRL7|6ANzR*4t94QS=e+2;Z>Mt@E`&gmsiO}o9GS2lF~6=fls&ddl60=fZL$?9$9(1 zwUiFul#UI7^KjzT;NH-yCmRWf@lWs|@Y_q|U=Yp%+OnsaTCNZu=q~0a1&`qNbj~Rq z{daz=Q{+5B*KY*^aa%K0r^6W{i_Pi3>71X@rYH$hBx;DC_|ier2)NvBs@sTY7v~AO z={ga~SQL6mNgfip2R~(i9|Q!Pv5jC6{3$yP}?yfRb7ex$R&^<>FLDw8HAXj6xf_p(E(}4LmLvz zX3Mo=qRPgO7x!I}<2_JQ8~$ skVj%~1WY6x>=DVG$-WLY&qmn%50rT~gpu4Y;s5{u07*qoM6N<$f?EM>AOHXW literal 0 HcmV?d00001 diff --git a/website/assets/js/pages/query-detail.page.js b/website/assets/js/pages/query-detail.page.js index e44781c79a..dc4010d7d1 100644 --- a/website/assets/js/pages/query-detail.page.js +++ b/website/assets/js/pages/query-detail.page.js @@ -3,23 +3,45 @@ parasails.registerPage('query-detail', { // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ data: { - //… + contributors: [], }, // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ - beforeMount: function() { + beforeMount: function () { //… }, - mounted: async function() { - //… + mounted: async function () { + if (this.query && this.query.contributors) { + this.contributors = await Promise.all( + this.query.contributors + .split(',') + .map(async (contributor) => this.getGitHubUserData(contributor)) + ); + } }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ methods: { + getGitHubUserData: async function (userName) { + const url = + 'https://api.github.com/users/' + encodeURIComponent(userName); - } + return await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/vnd.github.v3+json', + }, + }) + .then((response) => response.json()) + .catch((error) => console.log(error)); + }, + + clickAvatar: function (contributor) { + window.location = contributor.html_url; + }, + }, }); diff --git a/website/assets/js/pages/query-library.page.js b/website/assets/js/pages/query-library.page.js index 046e961d8f..12a9303fe4 100644 --- a/website/assets/js/pages/query-library.page.js +++ b/website/assets/js/pages/query-library.page.js @@ -3,6 +3,7 @@ parasails.registerPage('query-library', { // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ data: { + contributorsDictionary: {}, inputTextValue: '', inputTimers: {}, searchString: '', // The user input string to be searched against the query library @@ -12,57 +13,71 @@ parasails.registerPage('query-library', { computed: { filteredQueries: function () { - return _.filter(this.queries, (query) => this.isIncluded(query.platforms, this.selectedPlatform) && this.isIncluded(query.purpose, this.selectedPurpose)); + return this.queries.filter( + (query) => + this._isIncluded(query.platforms, this.selectedPlatform) && + this._isIncluded(query.purpose, this.selectedPurpose) + ); }, searchResults: function () { - return this.search(this.filteredQueries, this.searchString); + return this._search(this.filteredQueries, this.searchString); }, queriesList: function () { return this.searchResults; - } - + }, }, // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ - beforeMount: function() { + beforeMount: function () { //… }, - mounted: async function() { - //… + mounted: async function () { + const uniqueContributors = this._getUniqueContributors(this.queries); + this.contributorsDictionary = Object.assign( + {}, + await this._threadGitHubAPICalls(uniqueContributors) + ); }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ methods: { + clickCard: function (querySlug) { + window.location = '/queries/' + querySlug; // we can trust the query slug is url-safe + }, - isIncluded: function (queryProperty, selectedOption) { - if (selectedOption.startsWith('all') || selectedOption === '') { - return true; + clickAvatar: function (contributor) { + window.location = contributor.html_url; + }, + + getAvatarUrl: function (contributorData) { + return contributorData ? contributorData.avatar_url : ''; + }, + + getContributorsString: function (list, dictionary) { + const displayName = (contributorData) => { + if (contributorData) { + return !contributorData.name + ? contributorData.login + : contributorData.name; + } + }; + let contributorString = displayName(dictionary[list[0]]); + if (list.length > 2) { + contributorString += ` and ${list.length - 1} others`; } - if (_.isArray(queryProperty)) { - queryProperty = queryProperty.join(', '); + if (list.length === 2) { + contributorString += ` and ${displayName(dictionary[list[1]])}`; } - return _.isString(queryProperty) && queryProperty.toLowerCase().includes(selectedOption.toLowerCase()); + return contributorString; }, - search: function (library, searchString) { - const searchTerms = _.isString(searchString) ? searchString.toLowerCase().split(' ') : []; - return library.filter((item) => { - const description = _.isString(item.description) ? item.description.toLowerCase() : ''; - return _.some(searchTerms, (term) => description.includes(term)); - }); - }, - - setSearchString: function () { - this.searchString = this.inputTextValue; - }, - - delayInput: function(callback, ms, label) { + delayInput: function (callback, ms, label) { let inputTimers = this.inputTimers; return function () { label = label || 'defaultTimer'; @@ -71,10 +86,85 @@ parasails.registerPage('query-library', { }; }, - clickCard: function (querySlug) { - window.location = '/queries/' + querySlug;// we can trust the query slug is url-safe + setSearchString: function () { + this.searchString = this.inputTextValue; }, - } + _search: function (library, searchString) { + const searchTerms = _.isString(searchString) + ? searchString.toLowerCase().split(' ') + : []; + return library.filter((item) => { + const description = _.isString(item.description) + ? item.description.toLowerCase() + : ''; + return searchTerms.some((term) => description.includes(term)); + }); + }, + _isIncluded: function (data, selectedOption) { + if (selectedOption.startsWith('all') || selectedOption === '') { + return true; + } + if (_.isArray(data)) { + data = data.join(', '); + } + return ( + _.isString(data) && data.toLowerCase().includes(selectedOption.toLowerCase()) + ); + }, + + _threadGitHubAPICalls: async function (contributorsList) { + // create threads object with a thread for each contributor each thread is a promise that will resolve + // when the async call to the GitHub API resolves for that contributor + const threads = contributorsList.reduce((threads, contributor) => { + threads[contributor] = this._getGitHubUserData(contributor); + return threads; + }, {}); + + // each thread resolves with a key-value pair where the key is the contributor's GitHub handle and the value + // is the deserialized JSON response returned by the GitHub API for that contributor + const resolvedThreads = await Promise.all( + Object.keys(threads).map((key) => + Promise.resolve(threads[key]).then((result) => ({ [key]: result })) + ) + ).then((resultsArray) => { + const resolvedThreads = resultsArray.reduce( + (resolvedThreads, result) => { + Object.assign(resolvedThreads, result); + return resolvedThreads; + }, + {} + ); + return resolvedThreads; + }); + return resolvedThreads; + }, + + _getUniqueContributors: function (queries) { + return queries.reduce((uniqueContributors, query) => { + if (query.contributors) { + uniqueContributors = _.union( + uniqueContributors, + query.contributors.split(',') + ); + } + return uniqueContributors; + }, []); + }, + + _getGitHubUserData: async function (gitHubHandle) { + const url = + 'https://api.github.com/users/' + encodeURIComponent(gitHubHandle); + const userData = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/vnd.github.v3+json', + }, + }) + .then((response) => response.json()) + .catch(() => {}); + return userData; + }, + }, }); diff --git a/website/assets/styles/pages/query-detail.less b/website/assets/styles/pages/query-detail.less index 2a7caac610..0f9dc087b7 100644 --- a/website/assets/styles/pages/query-detail.less +++ b/website/assets/styles/pages/query-detail.less @@ -6,8 +6,15 @@ } h6 { + font-family: 'Nunito'; font-size: 16px; - line-height: 24px; + line-height: 25px; + } + + p { + &.platform, &.purpose { + margin-bottom: 8px; + } } a { @@ -15,6 +22,13 @@ color: @core-vibrant-blue; } + img { + &.logo { + height: 24px; + width: 24px; + } + } + .query-tip { background-color: @ui-off-white; @@ -32,4 +46,56 @@ } + .avatar-frame { + width: 24px; + height: 24px; + position: relative; + overflow: hidden; + border-radius: 50%; + &:hover { + cursor: pointer; + } + + img { + display: inline; + margin: 0 auto; + height: 100%; + width: auto; + } + } + + .divider { + border-bottom: 1px solid; + border-color: #E2E4EA; + } + + .platforms, .purpose, .contributors, .contribute { + min-height: 36px; + p, a { + font-family: 'Nunito'; + font-size: 16px; + line-height: 24px; + } + } + + .remediation { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + p { + font-family: 'Nunito'; + font-size: 16px; + line-height: 24px; + } + } + + @media (max-width: 575px) { + + h2 { + font-size: 28px; + line-height: 36px; + } + + } + } diff --git a/website/assets/styles/pages/query-library.less b/website/assets/styles/pages/query-library.less index b503c60ef3..ba8d336d74 100644 --- a/website/assets/styles/pages/query-library.less +++ b/website/assets/styles/pages/query-library.less @@ -22,18 +22,85 @@ color: @core-vibrant-blue; } - select { - color: @core-vibrant-blue; - border: none; + img { + &.logo { + height: 16px; + width: 16px; + } } input { - max-width: 176px; + height: 54px; + width: 250px; + border: 1px solid #C5C7D1; + border-radius: 8px; + padding: 15px; + &.mobile { + width: 100%; + margin-right: 0; + margin-left: 0; + } + } + + select { + color: @core-vibrant-blue; + border: 0px; + &:focus { + border: 0px; + } + &.select-purpose { + width: 102px; + } + &.select-platform { + width: 118px; + } + &.mobile { + height: 50px; + width: 100%; + margin-right: 0; + margin-left: 0; + border-radius: 8px; + padding: 12px; + } + } + + .library { + max-width: 860px; + } + + .description { + padding: 0px 30px 0px 30px; + p { + font-size: 16px; + line-height: 25px; + } + } + + .select-mobile-border { + height: 54px; + width: 100%; + margin-right: 0; + margin-left: 0; + border: 1px solid #C5C7D1; + border-radius: 8px; + padding-right: 15px; +} + + .select-mobile { + padding-left: 30px; + padding-right: 30px; + padding-bottom: 12px; + } + + .search-mobile { + padding-left: 30px; + padding-right: 30px; + padding-bottom: 12px; } .filter-and-search-bar { - margin-left: 30px; - margin-right: 30px; + padding-left: 45px; + padding-right: 45px; } .contributors, .platforms { @@ -42,7 +109,6 @@ font-size: 13px; line-height: 20px; } - } .row { @@ -50,12 +116,17 @@ align-items: center; } + .divider { + margin-left: 30px; + margin-right: 30px; + border-bottom: 1px solid; + border-color: #E2E4EA; + } + .card.results { box-shadow: none; border: none; border-radius: 8px; - border-bottom: 1px solid; - border-color: #E2E4EA; &:hover { background-color: #F1F0FF; @@ -69,11 +140,48 @@ border-color: @ui-off-white; box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); margin-bottom: 90px; + width: 100%; } .card-body { padding: 30px; + padding-top: 24px; + padding-bottom: 24px; + } + + .avatar-frame { + width: 21px; + height: 21px; + position: relative; + overflow: hidden; + border-radius: 50%; + &:hover { + cursor: pointer; + } + + img { + display: inline; + margin: 0 auto; + height: 100%; + width: auto; + } + } + + @media (max-width: 575px) { + + h2 { + font-size: 28px; + line-height: 36px; + } + + .contributors, .platforms { + + p { + font-size: 13px; + line-height: 20px; + } + } } } diff --git a/website/views/pages/query-detail.ejs b/website/views/pages/query-detail.ejs index c5bf2e9e80..6f4c3a2f76 100644 --- a/website/views/pages/query-detail.ejs +++ b/website/views/pages/query-detail.ejs @@ -1,9 +1,9 @@
-
+

{{query.name}}

-
{{query.description}}
+
{{query.description}}
lightbulb

{{query.tip}}

@@ -11,22 +11,19 @@

Query

{{query.query}} -
+

Remediation

-
    -
  • {{query.remediation}}
  • -
+

{{query.remediation}}

-
Platforms

- - - + + +

@@ -35,15 +32,68 @@

{{query.purpose}}

-
+
Contributors
- -

{{query.contributors}}

+
+
+
+ GitHub profile image +
+
+
Contribute to this page
View source
- +
+

{{query.name}}

+
{{query.description}}
+
+
+
+
+
+
Platforms
+

+ + + +

+
+
+
+
Purpose
+

{{query.purpose}}

+
+
+
+
+
+
Contributors
+
+
+
+ GitHub profile image +
+
+
+
+
+
+
Contribute to this page
+ View source +
+
+
+
+
+

Query

+ {{query.query}} +
+

Remediation

+

{{!query.remediation ? "N/A" : query.remediation}}

+
+
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %> diff --git a/website/views/pages/query-library.ejs b/website/views/pages/query-library.ejs index 9a96b8651e..16b758ec8c 100644 --- a/website/views/pages/query-library.ejs +++ b/website/views/pages/query-library.ejs @@ -1,24 +1,49 @@
-
+

Standard query library

-
Fleet's standard query library includes a growing collection of useful queries for organizations deploying Fleet and osquery.
-