Feat/update query doc sidepanel (#8214)

* create new components for query side panel

* add reusable icon component that uses svg for icons

* integrate with new osquery_fleet_schema.json data

* update UI to work with osquery_fleet_schema.json

* add remark-gfm to safely support direct urls in markdown

* move fleet ace into markdown component so we can render code with ace editor

* add testing for new query sidebar

* remove incomplete tests for query sidepanel
This commit is contained in:
Gabriel Hernandez 2022-10-14 17:45:57 +01:00 committed by GitHub
parent c16ab5f823
commit a950e9d095
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1648 additions and 516 deletions

View file

@ -0,0 +1 @@
- add new query sidebar with updated and improved docs

View file

@ -21,7 +21,7 @@ describe("Labels flow", () => {
it("creates a custom label", () => {
cy.getAttached(".label-filter-select__control").click();
cy.findByRole("button", { name: /add label/i }).click();
cy.getAttached(".ace_content").type(
cy.getAttached(".label-form__text-editor-wrapper .ace_content").type(
"{selectall}{backspace}SELECT * FROM users;"
);
cy.findByLabelText(/name/i).click().type("Show all MAC users");
@ -62,7 +62,7 @@ describe("Labels flow", () => {
it("creates labels with special characters", () => {
cy.getAttached(".label-filter-select__control").click();
cy.findByRole("button", { name: /add label/i }).click();
cy.getAttached(".ace_content").type(
cy.getAttached(".label-form__text-editor-wrapper .ace_content").type(
"{selectall}{backspace}SELECT * FROM users;"
);
cy.findByLabelText(/name/i)

View file

@ -147,7 +147,7 @@ describe("Policies flow (empty)", () => {
cy.findByText(/add a policy/i).click();
});
cy.findByText(/create your own policy/i).click();
cy.getAttached(".ace_scroller")
cy.getAttached(".policy-page__form .ace_scroller")
.click({ force: true })
.type(
"{selectall}SELECT 1 FROM users WHERE username = 'backup' LIMIT 1;"
@ -217,7 +217,7 @@ describe("Policies flow (empty)", () => {
});
// Query with unknown table name displays error message
cy.getAttached(".ace_scroller")
cy.getAttached(".policy-page__form .ace_scroller")
.first()
.click({ force: true })
.type("{selectall}SELECT 1 FROM foo WHERE start_time > 1;");
@ -230,7 +230,7 @@ describe("Policies flow (empty)", () => {
});
// Query with syntax error displays error message
cy.getAttached(".ace_scroller")
cy.getAttached(".policy-page__form .ace_scroller")
.first()
.click({ force: true })
.type("{selectall}SELEC 1 FRO osquery_info WHER start_time > 1;");
@ -243,7 +243,7 @@ describe("Policies flow (empty)", () => {
});
// Query with no tables treated as compatible with all platforms
cy.getAttached(".ace_scroller")
cy.getAttached(".policy-page__form .ace_scroller")
.first()
.click({ force: true })
.type("{selectall}SELECT * WHERE 1 = 1;");
@ -254,7 +254,7 @@ describe("Policies flow (empty)", () => {
});
// Tables defined in common table expression not factored into compatibility check
cy.getAttached(".ace_scroller")
cy.getAttached(".policy-page__form .ace_scroller")
.first()
.click({ force: true })
.type("{selectall} ")
@ -270,7 +270,7 @@ describe("Policies flow (empty)", () => {
// Query with only macOS tables treated as compatible only with macOS
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.getAttached(".ace_scroller")
cy.getAttached(" .policy-page__form .ace_scroller")
.first()
.click({ force: true })
.type("{selectall} ")
@ -285,7 +285,7 @@ describe("Policies flow (empty)", () => {
});
// Query with macadmins extension table is not treated as incompatible
cy.getAttached(".ace_scroller")
cy.getAttached(".policy-page__form .ace_scroller")
.first()
.click({ force: true })
.type("{selectall}SELECT 1 FROM mdm WHERE enrolled='true';");
@ -477,7 +477,7 @@ describe("Policies flow (seeded)", () => {
cy.getAttached("tbody").within(() => {
cy.getAttached(".name__cell .button--text-link").first().click();
});
cy.getAttached(".ace_scroller")
cy.getAttached(".policy-page__form .ace_scroller")
.click({ force: true })
.type(
"{selectall}SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;"

View file

@ -13,7 +13,7 @@ const manageQueriesPage = {
allowsCreateNewQuery: () => {
cy.getAttached(".button--brand"); // ensures cta button loads
cy.findByRole("button", { name: /new query/i }).click();
cy.getAttached(".ace_scroller")
cy.getAttached(".query-page__form .ace_scroller")
.click({ force: true })
.type("{selectall}SELECT * FROM windows_crashes;");
cy.findByRole("button", { name: /save/i }).click();
@ -52,7 +52,7 @@ const manageQueriesPage = {
cy.getAttached(".name__cell .button--text-link")
.first()
.click({ force: true });
cy.getAttached(".ace_text-input")
cy.getAttached(".query-page__form .ace_text-input")
.click({ force: true })
.clear({ force: true })
.type("SELECT 1 FROM cypress;", {
@ -72,7 +72,7 @@ const manageQueriesPage = {
cy.findByText(/get authorized/i).click();
});
cy.findByRole("button", { name: /run query/i }).should("exist");
cy.getAttached(".ace_scroller")
cy.getAttached(".query-page__form .ace_scroller")
.click()
.type("{selectall}SELECT datetime, username FROM windows_crashes;");
cy.findByRole("button", { name: /save as new/i }).should("be.enabled");

View file

@ -23,6 +23,8 @@ export interface IFleetAceProps {
wrapperClassName?: string;
hint?: string;
labelActionComponent?: React.ReactNode;
style?: React.CSSProperties;
onBlur?: (editor?: IAceEditor) => void;
onLoad?: (editor: IAceEditor) => void;
onChange?: (value: string) => void;
handleSubmit?: () => void;
@ -42,6 +44,8 @@ const FleetAce = ({
wrapEnabled = false,
wrapperClassName,
hint,
style,
onBlur,
onLoad,
onChange,
handleSubmit = noop,
@ -54,9 +58,17 @@ const FleetAce = ({
const fixHotkeys = (editor: IAceEditor) => {
editor.commands.removeCommand("gotoline");
editor.commands.removeCommand("find");
};
const onLoadHandler = (editor: IAceEditor) => {
fixHotkeys(editor);
onLoad && onLoad(editor);
};
const onBlurHandler = (event: any, editor?: IAceEditor): void => {
onBlur && onBlur(editor);
};
const handleDelete = (deleteCommand: string) => {
const currentText = editorRef.current?.editor.getValue();
const selectedText = editorRef.current?.editor.getSelectedText();
@ -114,7 +126,8 @@ const FleetAce = ({
maxLines={20}
name={name}
onChange={onChange}
onLoad={fixHotkeys}
onBlur={onBlurHandler}
onLoad={onLoadHandler}
readOnly={readOnly}
setOptions={{ enableLinking: true }}
showGutter={showGutter}
@ -123,6 +136,7 @@ const FleetAce = ({
value={value}
width="100%"
wrapEnabled={wrapEnabled}
style={style}
commands={[
{
name: "commandName",

View file

@ -0,0 +1,87 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import classnames from "classnames";
import { IAceEditor } from "react-ace/lib/types";
import { noop } from "lodash";
import FleetAce from "components/FleetAce";
import ExternalLinkIcon from "../../../assets/images/icon-external-link-12x12@2x.png";
interface ICustomLinkProps {
text: React.ReactNode;
href: string;
newTab?: boolean;
}
const CustomLink = ({ text, href, newTab = false }: ICustomLinkProps) => {
const target = newTab ? "__blank" : "";
return (
<a href={href} target={target} rel="noopener noreferrer">
{text}
<img src={ExternalLinkIcon} alt="Open external link" />
</a>
);
};
interface IFleetMarkdownProps {
markdown: string;
className?: string;
}
const baseClass = "fleet-markdown";
/** This will give us sensible defaults for how we render markdown across the fleet application.
* NOTE: can be extended later to take custom components, but dont need that at the moment.
*/
const FleetMarkdown = ({ markdown, className }: IFleetMarkdownProps) => {
const classNames = classnames(baseClass, className);
return (
<ReactMarkdown
className={classNames}
// enables some more markdown features such as direct urls and strikethroughts.
// more info here: https://github.com/remarkjs/remark-gfm
remarkPlugins={[remarkGfm]}
components={{
a: ({ href = "", children }) => {
return <CustomLink text={children} href={href} newTab />;
},
// Overrides code display to use FleetAce with Readonly overrides.
code: ({ children }) => {
const onEditorBlur = (editor?: IAceEditor) => {
editor && editor.clearSelection();
};
const onEditorLoad = (editor: IAceEditor) => {
editor.setOptions({
indentedSoftWrap: false, // removes automatic indentation when wrapping
});
// removes focus UI styling
editor.renderer.visualizeFocus = noop;
};
return (
<FleetAce
wrapperClassName={`${baseClass}__ace-display`}
value={String(children).replace(/\n/, "")}
showGutter={false}
onBlur={onEditorBlur}
onLoad={onEditorLoad}
style={{ border: "none" }}
wrapEnabled
readOnly
/>
);
},
}}
>
{markdown}
</ReactMarkdown>
);
};
export default FleetMarkdown;

View file

@ -0,0 +1,21 @@
.fleet-markdown {
font-size: $x-small;
ul {
// We need 20px here to keep the list items in line with the left side of
// the container.
padding-left: 20px;
}
pre {
padding: 12px;
border: 1px solid $ui-blue-gray;
background-color: $ui-light-grey; // copy fleet ace background color
.ace_cursor {
// We have the !important here as there doesnt seen a way to programatically
// hide only the cursor in the editor.
display: none !important
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./FleetMarkdown";

View file

View file

@ -1,3 +1,4 @@
import classnames from "classnames";
import React from "react";
interface ITooltipWrapperProps {
@ -5,6 +6,7 @@ interface ITooltipWrapperProps {
tipContent: string;
position?: "top" | "bottom";
isDelayed?: boolean;
className?: string;
}
const baseClass = "component__tooltip-wrapper";
@ -14,13 +16,15 @@ const TooltipWrapper = ({
tipContent,
position = "bottom",
isDelayed,
className,
}: ITooltipWrapperProps): JSX.Element => {
const classname = classnames(baseClass, className);
const tipClass = isDelayed
? `${baseClass}__tip-text delayed-tip`
: `${baseClass}__tip-text`;
return (
<div className={baseClass} data-position={position}>
<div className={classname} data-position={position}>
<div className={`${baseClass}__element`}>
{children}
<div className={`${baseClass}__underline`} data-text={children} />

View file

@ -3,11 +3,15 @@ import Apple from "./Apple";
import Windows from "./Windows";
import Linux from "./Linux";
// a mapping of the usable names of icons to the icon source.
export const ICON_MAP = {
"calendar-check": CalendarCheck,
darwin: Apple,
macOS: Apple,
windows: Windows,
Windows,
linux: Linux,
Linux,
};
export type IconNames = keyof typeof ICON_MAP;

View file

@ -1,24 +1,23 @@
import React from "react";
import classnames from "classnames";
import { IOsqueryTable } from "interfaces/osquery_table";
import { IOsQueryTable } from "interfaces/osquery_table";
import { osqueryTableNames } from "utilities/osquery_tables";
import { PLATFORM_DISPLAY_NAMES } from "utilities/constants";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
// @ts-ignore
import FleetIcon from "components/icons/FleetIcon";
import TooltipWrapper from "components/TooltipWrapper";
import FleetMarkdown from "components/FleetMarkdown";
import Icon from "components/Icon";
import QueryTableColumns from "./QueryTableColumns";
import QueryTablePlatforms from "./QueryTablePlatforms";
// @ts-ignore
import AppleIcon from "../../../../assets/images/icon-apple-dark-20x20@2x.png";
import LinuxIcon from "../../../../assets/images/icon-linux-dark-20x20@2x.png";
import WindowsIcon from "../../../../assets/images/icon-windows-dark-20x20@2x.png";
import CloseIcon from "../../../../assets/images/icon-close-black-50-8x8@2x.png";
import QueryTableExample from "./QueryTableExample";
import QueryTableNotes from "./QueryTableNotes";
interface IQuerySidePanel {
selectedOsqueryTable: IOsqueryTable;
selectedOsqueryTable: IOsQueryTable;
onOsqueryTableSelect: (tableName: string) => void;
onClose?: () => void;
}
@ -30,64 +29,35 @@ const QuerySidePanel = ({
onOsqueryTableSelect,
onClose,
}: IQuerySidePanel): JSX.Element => {
const displayTypeForDataType = (dataType: string) => {
switch (dataType) {
case "TEXT_TYPE":
return "text";
case "BIGINT_TYPE":
return "big int";
case "INTEGER_TYPE":
return "integer";
default:
return dataType;
}
};
const {
name,
description,
platforms,
columns,
examples,
notes,
evented,
} = selectedOsqueryTable;
const onSelectTable = (value: string) => {
onOsqueryTableSelect(value);
};
const renderColumns = () => {
const columns = selectedOsqueryTable?.columns;
const columnBaseClass = "query-column-list";
return columns?.map((column) => (
<li key={column.name} className={`${columnBaseClass}__item`}>
<span className={`${columnBaseClass}__name`}>
<TooltipWrapper tipContent={column.description}>
{column.name}
</TooltipWrapper>
</span>
<div className={`${columnBaseClass}__description`}>
<span className={`${columnBaseClass}__type`}>
{displayTypeForDataType(column.type)}
</span>
</div>
</li>
));
};
const renderTableSelect = () => {
const tableNames = osqueryTableNames?.map((name: string) => {
return { label: name, value: name };
const tableNames = osqueryTableNames?.map((tableName: string) => {
return { label: tableName, value: tableName };
});
if (!tableNames) {
return null;
}
return (
<Dropdown
options={tableNames}
value={selectedOsqueryTable?.name}
value={name}
onChange={onSelectTable}
placeholder="Choose Table..."
/>
);
};
const { description, platforms } = selectedOsqueryTable || {};
const iconClasses = classnames([`${baseClass}__icon`], "icon");
return (
<>
<div
@ -99,60 +69,27 @@ const QuerySidePanel = ({
<img alt="Close sidebar" src={CloseIcon} />
</div>
<div className={`${baseClass}__choose-table`}>
<h2 className={`${baseClass}__header`}>Tables</h2>
<h2 className={`${baseClass}__header`}>
Tables
<span className={`${baseClass}__table-count`}>
{osqueryTableNames.length}
</span>
</h2>
{renderTableSelect()}
<p className={`${baseClass}__description`}>{description}</p>
</div>
<div className={`${baseClass}__os-availability`}>
<h2 className={`${baseClass}__header`}>Compatible with:</h2>
<ul className={`${baseClass}__platforms`}>
{platforms?.map((platform) => {
if (platform === "all") {
return (
<li key={platform}>
<FleetIcon name="hosts" />{" "}
{PLATFORM_DISPLAY_NAMES[platform] || platform}
</li>
);
}
platform = platform.toLowerCase();
let icon = (
<img
src={AppleIcon}
alt={`${platform} icon`}
className={iconClasses}
/>
);
if (platform === "linux") {
icon = (
<img
src={LinuxIcon}
alt={`${platform} icon`}
className={iconClasses}
/>
);
} else if (platform === "windows") {
icon = (
<img
src={WindowsIcon}
alt={`${platform} icon`}
className={iconClasses}
/>
);
}
return (
<li key={platform}>
{icon} {PLATFORM_DISPLAY_NAMES[platform] || platform}
</li>
);
})}
</ul>
</div>
<div className={`${baseClass}__columns`}>
<h2 className={`${baseClass}__header`}>Columns</h2>
<ul className={`${baseClass}__column-list`}>{renderColumns()}</ul>
{evented && (
<div className={`${baseClass}__evented-table-tag`}>
<Icon name="calendar-check" className={`${baseClass}__event-icon`} />
<span>EVENTED TABLE</span>
</div>
)}
<div className={`${baseClass}__description`}>
<FleetMarkdown markdown={description} />
</div>
<QueryTablePlatforms platforms={platforms} />
<QueryTableColumns columns={columns} />
{examples && <QueryTableExample example={examples} />}
{notes && <QueryTableNotes notes={notes} />}
</>
);
};

View file

@ -0,0 +1,103 @@
import React from "react";
import classnames from "classnames";
import { ColumnType, IQueryTableColumn } from "interfaces/osquery_table";
import { PLATFORM_DISPLAY_NAMES } from "utilities/constants";
import TooltipWrapper from "components/TooltipWrapper";
interface IColumnListItemProps {
column: IQueryTableColumn;
}
const baseClass = "column-list-item";
const FOOTNOTES = {
required: "Required in WHERE clause.",
requires_user_context: "Defaults to root.",
platform: "Only available on",
};
/**
* This function is to create the html string for the tooltip. We do this as the
* current tooltip only supports strings. we can change this when it support ReactNodes
* in the future.
*/
const createTooltipHtml = (column: IQueryTableColumn) => {
const toolTipHtml = [];
const descriptionHtml = `<span class="${baseClass}__column-description">${column.description}</span>`;
toolTipHtml.push(descriptionHtml);
if (column.required) {
toolTipHtml.push(
`<span class="${baseClass}__footnote">${FOOTNOTES.required}</span>`
);
}
if (column.requires_user_context) {
toolTipHtml.push(
`<span class="${baseClass}__footnote">${FOOTNOTES.requires_user_context}</span>`
);
}
if (column.platforms?.length === 1) {
const platform = column.platforms[0];
toolTipHtml.push(
`<span class="${baseClass}__footnote">${FOOTNOTES.platform} ${platform}</span>`
);
}
if (column.platforms?.length === 2) {
const platform1 = PLATFORM_DISPLAY_NAMES[column.platforms[0]];
const platform2 = PLATFORM_DISPLAY_NAMES[column.platforms[1]];
toolTipHtml.push(
`<span class="${baseClass}__footnote">${FOOTNOTES.platform} ${platform1} and ${platform2}.</span>`
);
}
const tooltip = toolTipHtml.join("");
return tooltip;
};
const hasFootnotes = (column: IQueryTableColumn) => {
return (
column.required ||
column.requires_user_context ||
(column.platforms !== undefined && column.platforms.length !== 0)
);
};
const createTypeDisplayText = (type: ColumnType) => {
return type.replace("_", " ").toUpperCase();
};
const createListItemClassnames = (column: IQueryTableColumn) => {
return classnames(`${baseClass}__name`, {
[`${baseClass}__has-footnotes`]: hasFootnotes(column),
});
};
const ColumnListItem = ({ column }: IColumnListItemProps) => {
const columnNameClasses = createListItemClassnames(column);
return (
<li key={column.name} className={baseClass}>
<div className={`${baseClass}__name-wrapper`}>
<span className={columnNameClasses}>
<TooltipWrapper
tipContent={createTooltipHtml(column)}
className={`${baseClass}__tooltip`}
>
{column.name}
</TooltipWrapper>
</span>
{column.required && <span className={`${baseClass}__asterisk`}>*</span>}
</div>
<span className={`${baseClass}__type`}>
{createTypeDisplayText(column.type)}
</span>
</li>
);
};
export default ColumnListItem;

View file

@ -0,0 +1,51 @@
.column-list-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
&__name-wrapper {
display: flex;
align-items: center;
}
&__name {
font-size: $x-small;
}
// this is to override the tooltip text style from TooltipWrapper. When we
// change the component to support ReactNode we wont need this.
&__tooltip {
.component__tooltip-wrapper__tip-text {
font-style: normal;
}
}
&__asterisk {
// specific height to align it vertically with the column name
height: 19px;
}
&__has-footnotes {
font-style: italic;
margin-right: $pad-xsmall;
}
&__type {
font-size: $xx-small;
}
&__footnote {
font-weight: $bold;
margin: $pad-small 0;
display: block;
&:last-child {
margin-bottom: 0;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./ColumnListItem";

View file

@ -0,0 +1,47 @@
import React from "react";
import { IQueryTableColumn } from "interfaces/osquery_table";
import ColumnListItem from "./ColumnListItem";
const sortAlphabetically = (
columnA: IQueryTableColumn,
columnB: IQueryTableColumn
) => {
return columnA.name.localeCompare(columnB.name);
};
/**
* Orders the columns by required columns first sorted alphabetically,
* then the rest of the columns sorted alphabetically.
*/
const orderColumns = (columns: IQueryTableColumn[]) => {
const requiredColumns = columns.filter((column) => column.required);
const nonRequiredColumns = columns.filter((column) => !column.required);
const sortedRequiredColumns = requiredColumns.sort(sortAlphabetically);
const sortedNonRequiredColumns = nonRequiredColumns.sort(sortAlphabetically);
return [...sortedRequiredColumns, ...sortedNonRequiredColumns];
};
interface IQueryTableColumnsProps {
columns: IQueryTableColumn[];
}
const baseClass = "query-table-columns";
const QueryTableColumns = ({ columns }: IQueryTableColumnsProps) => {
const columnListItems = orderColumns(columns).map((column) => {
return <ColumnListItem key={column.name} column={column} />;
});
return (
<div className={baseClass}>
<h3>Columns</h3>
<ul className={`${baseClass}__column-list`}>{columnListItems}</ul>
</div>
);
};
export default QueryTableColumns;

View file

@ -0,0 +1,13 @@
.query-table-columns {
margin-bottom: $pad-xlarge;
h3 {
font-size: $small;
font-weight: $bold;
}
&__column-list {
list-style: none;
padding-left: 0;
}
}

View file

@ -0,0 +1 @@
export { default } from "./QueryTableColumns";

View file

@ -0,0 +1,20 @@
import React from "react";
import FleetMarkdown from "components/FleetMarkdown";
interface IQueryTableExampleProps {
example: string;
}
const baseClass = "query-table-example";
const QueryTableExample = ({ example }: IQueryTableExampleProps) => {
return (
<div className={baseClass}>
<h3>Example</h3>
<FleetMarkdown markdown={example} />
</div>
);
};
export default QueryTableExample;

View file

@ -0,0 +1,7 @@
.query-table-example {
h3 {
font-size: $small;
font-weight: $bold;
}
}

View file

@ -0,0 +1 @@
export { default } from "./QueryTableExample";

View file

@ -0,0 +1,23 @@
import React from "react";
import FleetMarkdown from "components/FleetMarkdown";
interface IQueryTableNotesProps {
notes: string;
}
const baseClass = "query-table-notes";
const QueryTableNotes = ({ notes }: IQueryTableNotesProps) => {
return (
<div className={baseClass}>
<h3>Notes</h3>
<FleetMarkdown
markdown={notes}
className={`${baseClass}__notes-markdown`}
/>
</div>
);
};
export default QueryTableNotes;

View file

@ -0,0 +1,18 @@
.query-table-notes {
h3 {
font-size: $small;
font-weight: $bold;
}
// overriding default FleetMarkdown styles here
&__notes-markdown {
li {
margin-bottom: $pad-medium;
&:last-child {
margin-bottom: 0;
}
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./QueryTableNotes";

View file

@ -0,0 +1,51 @@
import React from "react";
import { IOsqueryPlatform } from "interfaces/platform";
import { PLATFORM_DISPLAY_NAMES } from "utilities/constants";
import Icon from "components/Icon";
interface IPLatformListItemProps {
platform: IOsqueryPlatform;
}
const baseClassListItem = "platform-list-item";
const PlatformListItem = ({ platform }: IPLatformListItemProps) => {
return (
<li key={platform} className={baseClassListItem}>
<Icon name={platform} />
<span>{PLATFORM_DISPLAY_NAMES[platform]}</span>
</li>
);
};
// TODO: remove when freebsd is removed
type IPlatformsWithFreebsd = IOsqueryPlatform | "freebsd";
interface IQueryTablePlatformsProps {
platforms: IPlatformsWithFreebsd[];
}
const baseClass = "query-table-platforms";
const QueryTablePlatforms = ({ platforms }: IQueryTablePlatformsProps) => {
const platformListItems = platforms
.filter((platform) => platform !== "freebsd")
.map((platform) => {
return (
<PlatformListItem
key={platform}
platform={platform as IOsqueryPlatform} // TODO: remove when freebsd is removed
/>
);
});
return (
<div className={baseClass}>
<h3>Compatible with</h3>
<ul className={`${baseClass}__platform-list`}>{platformListItems}</ul>
</div>
);
};
export default QueryTablePlatforms;

View file

@ -0,0 +1,28 @@
.query-table-platforms {
h3 {
font-size: $small;
font-weight: $bold;
}
&__platform-list {
list-style: none;
padding-left: 0;
}
.platform-list-item {
display: flex;
align-items: center;
padding-bottom: $pad-medium;
font-size: $x-small;
&:last-child {
padding-bottom: 0;
}
.icon {
margin-right: $pad-small;
width: 20px;
height: 20px;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./QueryTablePlatforms";

View file

@ -26,121 +26,47 @@
}
}
&__header {
margin: 0 0 $pad-small;
font-size: $x-small;
font-weight: $bold;
color: $core-fleet-black;
}
&__choose-table {
margin: 0 0 $pad-xlarge;
margin: 0 0 $pad-large;
.form-field {
margin-bottom: $pad-medium;
}
}
&__description {
font-size: $x-small;
font-style: italic;
color: $core-fleet-black;
margin: 0;
}
&__platforms {
font-size: $x-small;
color: $core-fleet-black;
list-style: none;
margin: 0 0 $pad-xlarge;
padding: 0;
.fleeticon {
font-size: 18px;
margin-right: $pad-medium;
}
.icon {
margin-right: $pad-medium;
width: 20px;
height: 20px;
}
li {
height: 20px;
display: flex;
align-items: center;
padding-bottom: $pad-medium;
&:last-child {
padding-bottom: 0;
}
}
}
&__columns,
&__suggested-queries {
margin: 0 0 $pad-large;
}
&__column-list {
margin: 0;
padding: 0;
list-style: none;
}
&__column-wrapper {
display: flex;
margin: 0 0 15px;
padding-top: $pad-small;
border-top: 1px solid $ui-fleet-blue-15;
}
&__suggestion {
flex-grow: 1;
font-size: $x-small;
line-height: 1.71;
letter-spacing: 0.5px;
text-align: left;
color: $core-fleet-black;
}
&__load-suggestion {
align-self: center;
padding: 1px 5px;
margin: 0 0 0 10px;
}
}
.query-column-list {
&__item {
&__header {
margin: 0 0 $pad-medium;
font-size: $small;
font-weight: $bold;
display: flex;
align-items: center;
justify-content: space-between;
color: $core-fleet-black;
font-size: $x-small;
padding: $pad-small 0;
&:first-of-type {
border: 0;
}
}
&__name {
border-radius: $border-radius;
margin-right: $pad-small;
&__table-count {
line-height: normal;
margin-left: $pad-small;
background-color: $ui-fleet-blue-15;
padding: $pad-xsmall $pad-small;
border-radius: 8px;
font-size: $x-small;
}
&__evented-table-tag {
display: inline-flex;
align-items: center;
background-color: $ui-fleet-blue-15;
padding: $pad-xsmall $pad-small;
border-radius: 6px;
font-size: $xxx-small;
font-weight: $bold;
color: $core-fleet-black;
}
&__event-icon {
margin-right: $pad-small;
}
&__description {
flex-grow: 1;
text-align: right;
}
&__type {
font-size: $x-small;
color: $core-fleet-black;
overflow-wrap: break-word;
}
}

View file

@ -2,7 +2,7 @@ import React, { createContext, useReducer, ReactNode } from "react";
import { find } from "lodash";
import { osqueryTables } from "utilities/osquery_tables";
import { IOsqueryTable, DEFAULT_OSQUERY_TABLE } from "interfaces/osquery_table";
import { IOsQueryTable, DEFAULT_OSQUERY_TABLE } from "interfaces/osquery_table";
import { IPlatformString } from "interfaces/platform";
enum ACTIONS {
@ -55,10 +55,14 @@ type InitialStateType = {
setLastEditedQueryPlatform: (value: IPlatformString | null) => void;
policyTeamId: number;
setPolicyTeamId: (id: number) => void;
selectedOsqueryTable: IOsqueryTable;
selectedOsqueryTable: IOsQueryTable;
setSelectedOsqueryTable: (tableName: string) => void;
};
const initTable =
osqueryTables.find((table) => table.name === "users") ||
DEFAULT_OSQUERY_TABLE;
const initialState = {
lastEditedQueryId: null,
lastEditedQueryName: "",
@ -74,8 +78,7 @@ const initialState = {
setLastEditedQueryPlatform: () => null,
policyTeamId: 0,
setPolicyTeamId: () => null,
selectedOsqueryTable:
find(osqueryTables, { name: "users" }) || DEFAULT_OSQUERY_TABLE,
selectedOsqueryTable: initTable,
setSelectedOsqueryTable: () => null,
};
@ -89,7 +92,9 @@ const reducer = (state: InitialStateType, action: IAction) => {
case ACTIONS.SET_SELECTED_OSQUERY_TABLE:
return {
...state,
selectedOsqueryTable: find(osqueryTables, { name: action.tableName }),
selectedOsqueryTable:
find(osqueryTables, { name: action.tableName }) ||
DEFAULT_OSQUERY_TABLE,
};
case ACTIONS.SET_LAST_EDITED_QUERY_INFO:
return {

View file

@ -3,14 +3,14 @@ import { find } from "lodash";
import { osqueryTables } from "utilities/osquery_tables";
import { DEFAULT_QUERY } from "utilities/constants";
import { DEFAULT_OSQUERY_TABLE, IOsqueryTable } from "interfaces/osquery_table";
import { DEFAULT_OSQUERY_TABLE, IOsQueryTable } from "interfaces/osquery_table";
type Props = {
children: ReactNode;
};
type InitialStateType = {
selectedOsqueryTable: IOsqueryTable;
selectedOsqueryTable: IOsQueryTable;
lastEditedQueryId: number | null;
lastEditedQueryName: string;
lastEditedQueryDescription: string;
@ -45,7 +45,7 @@ const actions = {
SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO",
} as const;
const reducer = (state: any, action: any) => {
const reducer = (state: InitialStateType, action: any) => {
switch (action.type) {
case actions.SET_SELECTED_OSQUERY_TABLE:
return {

View file

@ -8,6 +8,7 @@ should be discussed within the team and documented before merged.
## Table of contents
- [Typing](#typing)
- [Utilities](#utilities)
- [Components](#components)
- [React Hooks](#react-hooks)
- [React Context](#react-context)
@ -60,6 +61,23 @@ const functionWithTableName = (tableName: string): boolean => {
};
```
## Utilities
### Named exports
We export individual utility functions and avoid exporting default objects when exporting utilities.
```ts
// good
export const replaceNewLines = () => {...}
// bad
export default {
replaceNewLines
}
```
## Components
### React Functional Components

View file

@ -1,4 +1,5 @@
import PropTypes from "prop-types";
import { IOsqueryPlatform } from "./platform";
export default PropTypes.shape({
columns: PropTypes.arrayOf(
@ -13,27 +14,37 @@ export default PropTypes.shape({
platform: PropTypes.string,
});
interface ITableColumn {
description: string;
export type ColumnType =
| "integer"
| "bigint"
| "double"
| "text"
| "unsigned_bigint";
export interface IQueryTableColumn {
name: string;
type: string;
description: string;
type: ColumnType;
hidden: boolean;
required: boolean;
index: boolean;
platforms?: IOsqueryPlatform[];
requires_user_context?: boolean;
}
export interface IOsqueryTable {
columns: ITableColumn[];
description: string;
export interface IOsQueryTable {
name: string;
platform?: string;
description: string;
url: string;
platforms: string[];
platforms: IOsqueryPlatform[];
evented: boolean;
cacheable: boolean;
columns: IQueryTableColumn[];
examples?: string;
notes?: string;
}
export const DEFAULT_OSQUERY_TABLE: IOsqueryTable = {
export const DEFAULT_OSQUERY_TABLE: IOsQueryTable = {
name: "users",
description:
"Local user accounts (including domain accounts that have logged on locally (Windows)).",

View file

@ -1,5 +1,5 @@
import URL_PREFIX from "router/url_prefix";
import { IOsqueryPlatform, IPlatformString } from "interfaces/platform";
import { IOsqueryPlatform } from "interfaces/platform";
const { origin } = global.window.location;
export const BASE_URL = `${origin}${URL_PREFIX}/api`;

View file

@ -1,19 +1,16 @@
import { flatMap, sortBy } from "lodash";
// @ts-ignore
import osqueryTablesJSON from "../osquery_tables.json";
import { flatMap } from "lodash";
export const normalizeTables = (
tablesJSON: Record<string, unknown> | string
) => {
// osquery JSON needs less parsing than it used to
const parsedTables =
typeof tablesJSON === "object" ? tablesJSON : JSON.parse(tablesJSON);
return sortBy(parsedTables, (table) => {
return table.name;
});
};
import { IOsQueryTable } from "interfaces/osquery_table";
import osqueryFleetTablesJSON from "../../schema/osquery_fleet_schema.json";
// Typecasting explicity here as we are adding more rigid types such as
// IOsqueryPlatform for platform names, instead of just any strings.
const queryTable = osqueryFleetTablesJSON as IOsQueryTable[];
export const osqueryTables = queryTable.sort((a, b) => {
return a.name >= b.name ? 1 : -1;
});
export const osqueryTables = normalizeTables(osqueryTablesJSON);
export const osqueryTableNames = flatMap(osqueryTables, (table) => {
return table.name;
});

View file

@ -52,6 +52,7 @@
"react-dom": "16.14.0",
"react-entity-getter": "0.0.8",
"react-error-boundary": "3.1.4",
"react-markdown": "^8.0.3",
"react-query": "3.34.16",
"react-router": "3.2.6",
"react-router-transition": "1.2.1",
@ -60,6 +61,7 @@
"react-table": "7.7.0",
"react-tabs": "3.2.3",
"react-tooltip": "4.2.21",
"remark-gfm": "^3.0.1",
"select": "1.1.2",
"sockjs-client": "1.6.1",
"sqlite-parser": "1.0.1",
@ -172,7 +174,7 @@
"@typescript-eslint/parser": "4.33.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "9.0.0",
"babel-jest": "23.6.0",
"babel-jest": "^29.2.0",
"babel-loader": "8.2.3",
"css-loader": "1.0.1",
"cypress": "9.5.1",

View file

@ -5,7 +5,8 @@
"target": "ES2019",
"sourceMap": true,
"jsx": "react",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"include": [
"./frontend/**/*"

1269
yarn.lock

File diff suppressed because it is too large Load diff