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
This commit is contained in:
gillespi314 2021-06-03 19:34:40 -05:00 committed by GitHub
parent 2e679a9fcf
commit 71f53e6f4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 450 additions and 81 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

View file

@ -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;
},
},
});

View file

@ -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;
},
},
});

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}
}

View file

@ -1,9 +1,9 @@
<div id="query-detail" v-cloak>
<div class="d-flex justify-content-center">
<div class="d-none d-md-flex justify-content-center">
<div class="col-6 my-5">
<h2 class="mb-3">{{query.name}}</h2>
<h6 class="font-weight-light pb-3">{{query.description}}</h6>
<h6 class="pb-3">{{query.description}}</h6>
<div v-if="!!query.tip">
<div class="container query-tip d-flex align-items-center border-left border-primary p-4 my-5">
<img alt="lightbulb" class="lightbulb" src="/images/lightbulb-blue-24x24@2x.png"/><p class="d-flex m-0">{{query.tip}}</p>
@ -11,22 +11,19 @@
</div>
<h3 class="py-3">Query</h3>
<code class="pb-3">{{query.query}}</code>
<div v-if="query.purpose === 'Detection' && query.remediation">
<div class="remediation" v-if="query.purpose === 'Detection' && query.remediation">
<h3 class="pt-5 pb-3">Remediation</h3>
<ul class="px-4">
<li>{{query.remediation}}</li>
</ul>
<p>{{query.remediation}}</p>
</div>
</div>
<div class="col-3 mx-5 my-5 d-none d-md-block">
<!-- TODO: refactor as page script to type-check and normalize"-->
<div class="query-sidebar border-bottom mb-3">
<h5>Platforms</h5>
<p>
<span v-if="query.platforms.includes('macOS')"><i class="fa fa-apple fa-lg mr-3" alt="Mac"></i></span>
<span v-if="query.platforms.includes('Windows')"><i class="fa fa-windows fa-lg mr-3" alt="Windows"></i></span>
<span v-if="query.platforms.includes('Linux')"><i class="fa fa-linux fa-lg mr" alt="Linux"></i></span>
<span v-if="query.platforms.includes('macOS')"><img class="d-inline mr-3 logo" src="/images/os-macos-black-16x16@2x.png" alt="macOS"/></span>
<span v-if="query.platforms.includes('Windows')"><img class="d-inline mr-3 logo" src="/images/os-windows-black-16x16@2x.png" alt="Windows"/></span>
<span v-if="query.platforms.includes('Linux')"><img class="d-inline mr-3 logo" src="/images/os-linux-black-16x16@2x.png" alt="Linux"/></span>
</p>
</div>
@ -35,15 +32,68 @@
<p>{{query.purpose}}</p>
</div>
<div class="query-sidebar border-bottom mb-3" v-if="query.contributors && query.contributors.length">
<div class="query-sidebar" v-if="query.contributors && query.contributors.length">
<h5>Contributors</h5>
<!-- TODO: display github avatars"-->
<p>{{query.contributors}}</p>
<div class="d-flex mb-3">
<div v-for="contributor in contributors">
<div class="d-flex m-1 avatar-frame" @click="clickAvatar(contributor)">
<img alt="GitHub profile image" :alt="contributor.name" :src="contributor.avatar_url"/>
</div>
</div>
</div>
</div>
<h5>Contribute to this page</h5>
<a target="_blank" :href="'https://github.com/fleetdm/fleet/edit/master/'+queryLibraryYmlRepoPath">View source</a>
</div>
</div>
<div class="d-block d-md-none px-4 py-5 justify-content-center">
<h2 class="mb-3">{{query.name}}</h2>
<h6 class="pb-3">{{query.description}}</h6>
</div>
<div class="d-block d-md-none">
<div class="col d-sm-flex">
<div class="col pr-sm-4 platforms-purpose">
<div class="d-flex align-items-center justify-content-between platforms">
<h5 class="m-0">Platforms</h5>
<p class="m-0 platform">
<span v-if="query.platforms.includes('macOS')"><img class="d-inline ml-3 logo" src="/images/os-macos-black-16x16@2x.png" alt="macOS"/></span>
<span v-if="query.platforms.includes('Windows')"><img class="d-inline ml-3-3 logo" src="/images/os-windows-black-16x16@2x.png" alt="Windows"/></span>
<span v-if="query.platforms.includes('Linux')"><img class="d-inline ml-3 logo" src="/images/os-linux-black-16x16@2x.png" alt="Linux"/></span>
</p>
</div>
<div class="my-3 divider"></div>
<div class="d-flex align-items-center justify-content-between purpose">
<h5 class="m-0">Purpose</h5>
<p class="m-0">{{query.purpose}}</p>
</div>
</div>
<div class="d-sm-none my-3 divider"></div>
<div class="col pl-sm-4 contributors-contribute">
<div class="d-flex align-items-center justify-content-between contributors">
<h5 class="m-0">Contributors</h5>
<div class="d-flex align-items-center">
<div class="d-flex" v-for="contributor in contributors">
<div class="d-flex m-1 avatar-frame" @click="clickAvatar(contributor)">
<img alt="GitHub profile image" :alt="contributor.name" :src="contributor.avatar_url"/>
</div>
</div>
</div>
</div>
<div class="my-3 divider"></div>
<div class="d-flex align-items-center justify-content-between contribute">
<h5 class="m-0">Contribute to this page</h5>
<a class="text-right m-0" target="_blank" :href="'https://github.com/fleetdm/fleet/edit/master/'+queryLibraryYmlRepoPath">View source</a>
</div>
</div>
</div>
</div>
<div class="d-block d-md-none py-5 justify-content-center" style="padding-left: 30px; padding-right: 30px;">
<h3 class="py-3">Query</h3>
<code class="pb-3">{{query.query}}</code>
<div class="remediation" v-if="query.purpose === 'Detection'">
<h3 class="pt-5 pb-3">Remediation</h3>
<p>{{!query.remediation ? "N/A" : query.remediation}}</p>
</div>
</div>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>

View file

@ -1,24 +1,49 @@
<div id="query-library" v-cloak>
<div class="d-flex justify-content-center">
<div class="col-8 my-5">
<div class="col-6-grow justify-content-center my-5 library">
<h2 class="mb-3">Standard query library</h2>
<h6 class="font-weight-light pb-3">Fleet's standard query library includes a growing collection of useful queries for organizations deploying Fleet and osquery.</h6>
<div class="filter-and-search-bar container">
<div class="row justify-content-between">
<div class="col-8 d-flex px-0">
<p class="pb-3 description">Fleet's standard query library includes a growing collection of useful queries for organizations deploying Fleet and osquery.</p>
<div class="p-0 m-0">
<div class="d-sm-none">
<div class="d-flex search-mobile">
<input class="mobile" v-model="inputTextValue" placeholder="Search queries" @keydown.self="delayInput(setSearchString, 500, 'defaultTimer')()"/>
</div>
<div class="d-flex select-mobile">
<div class="select-mobile-border">
<select class="select-purpose mobile font-weight-bold" v-model="selectedPurpose">
<option value="all" selected>All queries</option>
<option value="information">Informational queries</option>
<option value="detection">Detection queries</option>
</select>
</div>
</div>
<div class="d-flex select-mobile">
<div class="select-mobile-border">
<select class="select-purpose mobile font-weight-bold" v-model="selectedPlatform">
<option value="all" selected>All platforms</option>
<option value="macOS">macOS</option>
<option value="Windows">Windows</option>
<option value="Linux">Linux</option>
</select>
</div>
</div>
</div>
<div class="filter-and-search-bar d-none d-sm-flex row justify-content-between">
<div class="d-flex col col-xs-7 px-0">
<div class="filter-purpose">
<span>Show
<select class="mr-1" v-model="selectedPurpose">
<select class="mr-1 select-purpose" v-model="selectedPurpose">
<option value="all" selected>all queries</option>
<option value="information">informational</option>
<option value="information">information</option>
<option value="detection">detection</option>
</select>
</span>
</div>
<div class="filter-platform">
<span> compatible with
<select class="mr-1" v-model="selectedPlatform">
<select class="mr-1 select-platform" v-model="selectedPlatform">
<option value="all" selected>all platforms</option>
<option value="macOS">macOS</option>
<option value="Windows">Windows</option>
@ -27,9 +52,9 @@
</span>
</div>
</div>
<div class="col-3 px-0 d-none d-lg-block justify-content-end">
<div class="search ">
<input v-model="inputTextValue" placeholder="Search" @keydown.self="delayInput(setSearchString, 1000, 'defaultTimer')()"/>
<div class="col col-xs-5 px-0 justify-content-end">
<div class="search d-flex justify-content-end">
<input v-model="inputTextValue" placeholder="Search queries" @keydown.self="delayInput(setSearchString, 500, 'defaultTimer')()"/>
</div>
</div>
</div>
@ -38,33 +63,41 @@
<div class="category__informational">
<div v-for="query of queriesList">
<div class="card results" @click="clickCard(query.slug)">
<div class="card-body">
<div class="card-body query-card">
<div class="row justify-content-between align-items-center">
<div class="col-10">
<div class="col-sm-9 col-md-10">
<h5 class="card-title m-0">{{query.name}}</h5>
<h6 class="font-italic mb-1 p-0">{{query.description}}</h6>
<p class="font-italic mb-1 p-0 description">{{query.description}}</p>
<div class="contributors" v-if="query.contributors && query.contributors.length">
<p class="mb-0">contributed by {{query.contributors}}</p>
<div class="d-flex mb-2 mb-sm-1 align-items-center">
<div v-for="contributor in query.contributors.split(',')">
<div class="d-flex m-1 avatar-frame" @click="clickAvatar(contributor)">
<img alt="GitHub profile image" :alt="contributor" :src="getAvatarUrl(contributorsDictionary[contributor])"/>
</div>
</div>
<p class="mb-0 ml-1">contributed by {{getContributorsString(query.contributors.split(','), contributorsDictionary)}}</p>
</div>
</div>
</div>
<div class="col-2">
<div class="text-right m-0">
<span v-if="query.platforms.includes('macOS')"><i class="fa fa-apple fa-md ml-1" alt="Mac"></i></span>
<span v-if="query.platforms.includes('Windows')"><i class="fa fa-windows fa-md ml-1" alt="Windows"></i></span>
<span v-if="query.platforms.includes('Linux')"><i class="fa fa-linux fa-md ml-1" alt="Linux"></i></span>
<div class="col-sm-3 col-md-2">
<div class="text-sm-right m-0">
<span v-if="query.platforms.includes('macOS')"><img class="d-inline mr-1 mr-sm-0 ml-sm-1 ml-md-2 logo" src="/images/os-macos-black-16x16@2x.png" alt="macOS"/></span>
<span v-if="query.platforms.includes('Windows')"><img class="d-inline mr-1 mr-sm-0 ml-sm-1 ml-md-2 logo" src="/images/os-windows-black-16x16@2x.png" alt="Windows"/></span>
<span v-if="query.platforms.includes('Linux')"><img class="d-inline mr-1 mr-ms-0 ml-sm-1 ml-md-2 logo" src="/images/os-linux-black-16x16@2x.png" alt="Linux"/></span>
</div>
</div>
</div>
</div>
<div class="divider"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-center">
<div class="card call-to-action col-8">
<div class="card-body flex-fill">
<div class="d-flex justify-content-center p-3">
<div class="card call-to-action col-6-grow my-5 library">
<div class="card-body">
<h3 class="mb-3">Contributors</h3>
<p><strong>Want to add your own query?</strong> Please submit a pull request <a href="https://github.com/fleetdm/fleet/tree/master/handbook/queries" >over on GitHub</a>.</p>
</div>