Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#49](https://github.com/kobsio/kobs/pull/49): Add new chart type `table` for Prometheus plugin, which allows a user to render the results of multiple Prometheus queries in ab table.
- [#51](https://github.com/kobsio/kobs/pull/51): Add new command-line flag to forbid access for resources.
- [#52](https://github.com/kobsio/kobs/pull/52): Add option to enter a single trace id in the Jaeger plugin.
- [#56](https://github.com/kobsio/kobs/pull/56): Add actions for Elasticsearch plugin to include/exclude and toggle values in the logs view.

### Fixed

Expand Down
3 changes: 3 additions & 0 deletions app/src/plugins/elasticsearch/ElasticsearchLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface IElasticsearchLogsProps extends IElasticsearchOptions {
setDocument?: (document: React.ReactNode) => void;
setScrollID: (scrollID: string) => void;
selectField?: (field: string) => void;
showActions: boolean;
}

// ElasticsearchLogs is a wrapper component for the Elasticsearch results view (ElasticsearchLogsGrid), it is used to
Expand All @@ -54,6 +55,7 @@ const ElasticsearchLogs: React.FunctionComponent<IElasticsearchLogsProps> = ({
setDocument,
setScrollID,
selectField,
showActions,
}: IElasticsearchLogsProps) => {
const [data, setData] = useState<IDataState>({
buckets: [],
Expand Down Expand Up @@ -186,6 +188,7 @@ const ElasticsearchLogs: React.FunctionComponent<IElasticsearchLogsProps> = ({
setDocument={setDocument}
setScrollID={setScrollID}
selectField={selectField}
showActions={showActions}
/>
);
};
Expand Down
84 changes: 84 additions & 0 deletions app/src/plugins/elasticsearch/ElasticsearchLogsActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Button, ButtonVariant } from '@patternfly/react-core';
import { ColumnsIcon, MinusIcon, PlusIcon } from '@patternfly/react-icons';
import { useHistory, useLocation } from 'react-router-dom';
import React from 'react';

import { IKeyValue, getOptionsFromSearch } from 'plugins/elasticsearch/helpers';

interface IElasticsearchLogsActionsProps {
field: IKeyValue;
}

const ElasticsearchLogsActions: React.FunctionComponent<IElasticsearchLogsActionsProps> = ({
field,
}: IElasticsearchLogsActionsProps) => {
const history = useHistory();
const location = useLocation();

const adjustQuery = (addition: string): void => {
const opts = getOptionsFromSearch(location.search);
const fields = opts.fields ? opts.fields.map((field) => `&field=${field}`) : [];
const query = `${opts.query} ${addition}`;

history.push({
pathname: location.pathname,
search: `?query=${query}${fields && fields.length > 0 ? fields.join('') : ''}&timeEnd=${opts.timeEnd}&timeStart=${
opts.timeStart
}`,
});
};

const adjustFields = (field: string): void => {
const opts = getOptionsFromSearch(location.search);
let tmpFields = opts.fields ? opts.fields : [];

if (tmpFields.includes(field)) {
tmpFields = tmpFields.filter((f) => f !== field);
} else {
tmpFields.push(field);
}

const fields = tmpFields ? tmpFields.map((field) => `&field=${field}`) : [];

history.push({
pathname: location.pathname,
search: `?query=${opts.query}${fields && fields.length > 0 ? fields.join('') : ''}&timeEnd=${
opts.timeEnd
}&timeStart=${opts.timeStart}`,
});
};

return (
<React.Fragment>
<Button
style={{ fontSize: '10px', padding: '0px 6px' }}
variant={ButtonVariant.plain}
isSmall={true}
aria-label="Include"
onClick={(): void => adjustQuery(`AND ${field.key}: ${field.value}`)}
>
<PlusIcon />
</Button>
<Button
style={{ fontSize: '10px', padding: '0px 6px' }}
variant={ButtonVariant.plain}
isSmall={true}
aria-label="Exclude"
onClick={(): void => adjustQuery(`AND NOT ${field.key}: ${field.value}`)}
>
<MinusIcon />
</Button>
<Button
style={{ fontSize: '10px', padding: '0px 6px' }}
variant={ButtonVariant.plain}
isSmall={true}
aria-label="Toggle"
onClick={(): void => adjustFields(field.key)}
>
<ColumnsIcon />
</Button>
</React.Fragment>
);
};

export default ElasticsearchLogsActions;
33 changes: 30 additions & 3 deletions app/src/plugins/elasticsearch/ElasticsearchLogsDocument.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
import {
Card,
DrawerActions,
DrawerCloseButton,
DrawerHead,
DrawerPanelBody,
DrawerPanelContent,
Tab,
TabTitleText,
Tabs,
} from '@patternfly/react-core';
import React from 'react';
import React, { useState } from 'react';

import { IDocument, formatTimeWrapper } from 'plugins/elasticsearch/helpers';
import Editor from 'components/Editor';
import ElasticsearchLogsDocumentOverview from 'plugins/elasticsearch/ElasticsearchLogsDocumentOverview';
import Title from 'components/Title';

export interface IElasticsearchLogsDocumentProps {
document: IDocument;
showActions: boolean;
close: () => void;
}

// Document renders a single document in a drawer panel. We show the whole JSON representation for this document in a
// code view. The highlighting of this JSON document is handled by highlight.js.
const ElasticsearchLogsDocument: React.FunctionComponent<IElasticsearchLogsDocumentProps> = ({
document,
showActions,
close,
}: IElasticsearchLogsDocumentProps) => {
const [activeTab, setActiveTab] = useState<string>('overview');

return (
<DrawerPanelContent minSize="50%">
<DrawerHead>
Expand All @@ -36,8 +45,26 @@ const ElasticsearchLogsDocument: React.FunctionComponent<IElasticsearchLogsDocum
</DrawerHead>

<DrawerPanelBody>
<Editor value={JSON.stringify(document, null, 2)} mode="json" readOnly={true} />
<p>&nbsp;</p>
<Tabs
activeKey={activeTab}
onSelect={(event, tabIndex): void => setActiveTab(tabIndex.toString())}
className="pf-u-mt-md"
isFilled={true}
mountOnEnter={true}
>
<Tab eventKey="overview" title={<TabTitleText>Overview</TabTitleText>}>
<div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}>
<ElasticsearchLogsDocumentOverview document={document} showActions={showActions} />
</div>
</Tab>
<Tab eventKey="json" title={<TabTitleText>JSON</TabTitleText>}>
<div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}>
<Card>
<Editor value={JSON.stringify(document, null, 2)} mode="json" readOnly={true} />
</Card>
</div>
</Tab>
</Tabs>
</DrawerPanelBody>
</DrawerPanelContent>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Card, CardBody, DescriptionList } from '@patternfly/react-core';
import React from 'react';

import { IDocument, IKeyValue } from 'plugins/elasticsearch/helpers';
import ElasticsearchLogsDocumentOverviewItem from 'plugins/elasticsearch/ElasticsearchLogsDocumentOverviewItem';

// getKeyValues creates an array with all keys and values of the document.
const getKeyValues = (obj: IDocument, prefix = ''): IKeyValue[] => {
return Object.keys(obj).reduce((res: IKeyValue[], el) => {
if (Array.isArray(obj[el])) {
return res;
} else if (typeof obj[el] === 'object' && obj[el] !== null) {
return [...res, ...getKeyValues(obj[el], prefix + el + '.')];
}
return [...res, { key: prefix + el, value: obj[el] }];
}, []);
};

export interface IElasticsearchLogsDocumentOverviewProps {
document: IDocument;
showActions: boolean;
}

// ElasticsearchLogsDocumentOverview renders a list of all keys and their values in a description list. For that we have
// to generate a fields list via the getKeyValues function first.
const ElasticsearchLogsDocumentOverview: React.FunctionComponent<IElasticsearchLogsDocumentOverviewProps> = ({
document,
showActions,
}: IElasticsearchLogsDocumentOverviewProps) => {
const fields = getKeyValues(document['_source']);

return (
<Card>
<CardBody>
<DescriptionList className="pf-u-text-break-word">
{fields.map((field) => (
<ElasticsearchLogsDocumentOverviewItem key={field.key} field={field} showActions={showActions} />
))}
</DescriptionList>
</CardBody>
</Card>
);
};

export default ElasticsearchLogsDocumentOverview;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core';
import React, { useState } from 'react';

import ElasticsearchLogsActions from 'plugins/elasticsearch/ElasticsearchLogsActions';
import { IKeyValue } from 'plugins/elasticsearch/helpers';

interface IElasticsearchLogsDocumentOverviewItemProps {
field: IKeyValue;
showActions: boolean;
}

// Item is the component to render a single field from the generated key/values list. When the user hovers over the
// title/key of the description list, we will show some actions, which can be used to include/exclude the value from the
// search or to add it as column to the table.
const ElasticsearchLogsDocumentOverviewItem: React.FunctionComponent<IElasticsearchLogsDocumentOverviewItemProps> = ({
field,
showActions,
}: IElasticsearchLogsDocumentOverviewItemProps) => {
const [show, setShow] = useState<boolean>(false);

return (
<DescriptionListGroup>
<DescriptionListTerm onMouseEnter={(): void => setShow(true)} onMouseLeave={(): void => setShow(false)}>
{field.key}
{showActions && show ? <ElasticsearchLogsActions field={field} /> : null}
</DescriptionListTerm>
<DescriptionListDescription>{field.value}</DescriptionListDescription>
</DescriptionListGroup>
);
};

export default ElasticsearchLogsDocumentOverviewItem;
26 changes: 12 additions & 14 deletions app/src/plugins/elasticsearch/ElasticsearchLogsDocuments.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { TableComposable, TableVariant, Tbody, Th, Thead, Tr } from '@patternfly/react-table';
import { Card } from '@patternfly/react-core';
import React from 'react';

import { IDocument, formatTimeWrapper, getProperty } from 'plugins/elasticsearch/helpers';
import ElasticsearchLogsDocumentsItem from 'plugins/elasticsearch/ElasticsearchLogsDocumentsItem';
import { IDocument } from 'plugins/elasticsearch/helpers';

export interface IElasticsearchLogsDocumentsProps {
selectedFields: string[];
documents: IDocument[];
select?: (doc: IDocument) => void;
showActions: boolean;
}

// ElasticsearchLogsDocuments renders a list of documents. If the user has selected some fields, we will render the
Expand All @@ -17,6 +19,7 @@ const ElasticsearchLogsDocuments: React.FunctionComponent<IElasticsearchLogsDocu
selectedFields,
documents,
select,
showActions,
}: IElasticsearchLogsDocumentsProps) => {
return (
<Card style={{ maxWidth: '100%', overflowX: 'scroll' }}>
Expand All @@ -33,18 +36,13 @@ const ElasticsearchLogsDocuments: React.FunctionComponent<IElasticsearchLogsDocu
</Thead>
<Tbody>
{documents.map((document, index) => (
<Tr key={index} onClick={select ? (): void => select(document) : undefined}>
<Td dataLabel="Time">{formatTimeWrapper(document['_source']['@timestamp'])}</Td>
{selectedFields.length > 0 ? (
selectedFields.map((selectedField, index) => (
<Td key={index} dataLabel={selectedField}>
{getProperty(document['_source'], selectedField)}
</Td>
))
) : (
<Td dataLabel="_source">{JSON.stringify(document['_source'])}</Td>
)}
</Tr>
<ElasticsearchLogsDocumentsItem
key={index}
document={document}
selectedFields={selectedFields}
select={select}
showActions={showActions}
/>
))}
</Tbody>
</TableComposable>
Expand Down
41 changes: 41 additions & 0 deletions app/src/plugins/elasticsearch/ElasticsearchLogsDocumentsItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { TableText, Td, Tr } from '@patternfly/react-table';
import React from 'react';

import { IDocument, formatTimeWrapper } from 'plugins/elasticsearch/helpers';
import ElasticsearchLogsDocumentsItemField from 'plugins/elasticsearch/ElasticsearchLogsDocumentsItemField';

export interface IElasticsearchLogsDocumentsItemProps {
selectedFields: string[];
document: IDocument;
select?: (doc: IDocument) => void;
showActions: boolean;
}

const ElasticsearchLogsDocumentsItem: React.FunctionComponent<IElasticsearchLogsDocumentsItemProps> = ({
selectedFields,
document,
select,
showActions,
}: IElasticsearchLogsDocumentsItemProps) => {
return (
<Tr>
<Td dataLabel="Time" onClick={select ? (): void => select(document) : undefined}>
<TableText wrapModifier="nowrap"> {formatTimeWrapper(document['_source']['@timestamp'])}</TableText>
</Td>
{selectedFields.length > 0 ? (
selectedFields.map((selectedField, index) => (
<ElasticsearchLogsDocumentsItemField
key={index}
document={document}
selectedField={selectedField}
showActions={showActions}
/>
))
) : (
<Td dataLabel="_source">{JSON.stringify(document['_source'])}</Td>
)}
</Tr>
);
};

export default ElasticsearchLogsDocumentsItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useState } from 'react';
import { Td } from '@patternfly/react-table';

import { IDocument, getProperty } from 'plugins/elasticsearch/helpers';
import ElasticsearchLogsActions from 'plugins/elasticsearch/ElasticsearchLogsActions';

export interface IElasticsearchLogsDocumentsItemFieldProps {
selectedField: string;
document: IDocument;
select?: (doc: IDocument) => void;
showActions: boolean;
}

const ElasticsearchLogsDocumentsItemField: React.FunctionComponent<IElasticsearchLogsDocumentsItemFieldProps> = ({
document,
selectedField,
showActions,
}: IElasticsearchLogsDocumentsItemFieldProps) => {
const [show, setShow] = useState<boolean>(false);
const value = getProperty(document['_source'], selectedField);

return (
<Td dataLabel={selectedField} onMouseEnter={(): void => setShow(true)} onMouseLeave={(): void => setShow(false)}>
{value}
{showActions && show ? <ElasticsearchLogsActions field={{ key: selectedField, value: value as string }} /> : null}
</Td>
);
};

export default ElasticsearchLogsDocumentsItemField;
Loading