Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a custom logs panel #46

Merged
merged 4 commits into from
Jan 16, 2024
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: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@types/react": "17.0.14",
"@types/react-test-renderer": "16.9.2",
"@types/react-virtualized-auto-sizer": "1.0.0",
"@types/react-window": "^1.8.8",
"@types/semver": "7.3.7",
"@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1",
Expand Down
26 changes: 12 additions & 14 deletions src/datasource/components/FieldValueFrequency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,15 @@ function getValueCountsForField(unmappedFieldValuesArray: any): ValueFrequency[]

const InnerTitle = (field: Field) => {
return (
<div>
<span
style={{
fontFamily: 'monospace',
}}
>
<h5>
<b>{field.name}</b>
</h5>
</span>
</div>
<span
style={{
fontFamily: 'monospace',
}}
>
<h5>
<b>{field.name}</b>
</h5>
</span>
);
};

Expand All @@ -72,7 +70,7 @@ const InnerContent = (
) => {
let mostCommonValues = getValueCountsForField(field.unmappedFieldValuesArray)
return (
<div>
<>
<span>
<b>
Top {mostCommonValues.length} {mostCommonValues.length > 1 ? 'values' : 'value'}
Expand Down Expand Up @@ -117,7 +115,7 @@ const InnerContent = (
</VerticalGroup>
);
})}
</div>
</>
);
};

Expand All @@ -135,7 +133,7 @@ const InnerFooter = (field: Field) => {
const FieldValueFrequency = ({ field, children, onMinusClick, onPlusClick }: Props) => {
// This doesn't make sense for this field
if (field.name === '_source') {
return <div></div>;
return <></>;
}

return (
Expand Down
325 changes: 325 additions & 0 deletions src/datasource/components/Logs/LogsCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
import React from 'react'
import { Log } from 'datasource/types'
import { LogColumnType, LogColumn } from 'datasource/components/Logs/types'
import getLogTableContext from 'datasource/components/Logs/context'
import { Button, useTheme2 } from '@grafana/ui'
import { dateTimeParse, getTimeZone } from '@grafana/data'
import { DARK_THEME_HIGHLIGHTED_BACKGROUND, DARK_THEME_OUTLINE, LIGHT_THEME_HIGHLIGHTED_BACKGROUND, LIGHT_THEME_OUTLINE } from './styles'


interface LogKeyValProps {
field: string,
val: any
}


const LogKeyVal = ({ field, val }: LogKeyValProps) => {
return (<div style={{display:'inline-block', paddingRight: '10px'}}>
<div
style={{
backgroundColor: useTheme2().isDark ? DARK_THEME_HIGHLIGHTED_BACKGROUND : LIGHT_THEME_HIGHLIGHTED_BACKGROUND,
display: 'inline',
borderRadius: '6px',
marginRight: '3px',
padding: '2px 4px'
}}>
{field + ":"}

</div>
<div style={{display: 'inline-block'}}>
{val}
</div>
</div>);
}

interface ExpandedLogKeyValProps {
field: string,
val: any
}


const ExpandedLogKeyVal = ({ field, val }: ExpandedLogKeyValProps) => {
return (
<tr>
<td style={{}}>
{field + ":"}

</td>
<td>
{val}
</td>
</tr>);
}

interface ExpandedDocumentProps {
log: Log,
index: number,
datasourceUid: string,
datasourceName: string,
datasourceField: string,
}


const ExpandedDocument = ({ log, index, datasourceUid, datasourceName, datasourceField }: ExpandedDocumentProps) => {
const { setSize, windowWidth } = getLogTableContext();
const root = React.useRef<HTMLDivElement>();
React.useEffect(() => {
if (root.current) {
setSize(index, root.current.getBoundingClientRect().height);
}
}, [windowWidth]);

const link = {
datasource: datasourceUid,
queries: [{
query: log.get(datasourceField),
refId: "A",
}],
range:{
from: "now-15m", // TODO: This shouldn't be hardcoded
to: "now", // TODO: This also shouldn't be hardcoded
},
};

const orgId = 1; // TODO: This shouldn't be hardcoded
const protocol = window.location.protocol.toString();
const hostname = window.location.hostname.toString();
const port = window.location.port.toString();

/*
* The format for the Grafana Explore UI (in a urlencoded form):
*'https://<grafana URL>/explore?left={"datasource":"<datasource UID>","queries":[{"query":"<trace ID>","refId":"A"}],"range":{"from":"now-15m"," to":"now"}}&orgId=<org ID>'
*/
const formattedLink = `${protocol}//${hostname}${port ? ":" + port : ""}/explore?left=${encodeURIComponent(JSON.stringify(link))}&orgId=${orgId}`
kyle-sammons marked this conversation as resolved.
Show resolved Hide resolved

// TODO: We should add an icon here as well
return (
<div ref={root}>
<span style={
{
fontWeight: 'bold',
paddingTop: '25px',
paddingBottom: '25px'
}
}>
Expanded Document
</span>
<table>
<tr>
<th>
Field
</th>
<th>
Value
</th>

</tr>
{
Array.from(log.keys()).map((key) => (
<ExpandedLogKeyVal
field={key}
val={log.get(key)}
key={key}
/>
))
}
</table>
{
log.has(datasourceField) && datasourceName && datasourceUid ?
(
<a href={formattedLink}>
<Button
size={'sm'}
variant={'primary'}
>
View in {datasourceName}
</Button>
</a>
)
: ''
}

</div>
);
}


const DocumentCell = (log: Log, style: any, rowIndex: number, expanded: boolean, datasourceUid: string, datasourceName: string, datasourceField: string) => (
<div
style={{
display: 'inline-block',
lineHeight: '2em',
fontFamily: 'monospace',
fontSize: '12px',
overflow: 'hidden',
paddingTop: '10px',
...style
}}
>
<div style={{maxHeight: '115px', overflow: 'hidden'}}>
{
Array.from(log.keys()).map((key) => (
<LogKeyVal
field={key}
val={log.get(key)}
key={key}
/>
))
}
</div>
{
expanded ?
(<ExpandedDocument
log={log}
index={rowIndex}
datasourceUid={datasourceUid}
datasourceName={datasourceName}
datasourceField={datasourceField}
/>)

: ''
}
</div>
)

const TimestampCell = (timestamp: number, style: any, rowIndex: number, expandedRows: boolean[], onClick: ((index: number) => void)) => {
const getFoldIcon = () => {
if (expandedRows[rowIndex]) {
return 'angle-down';
}
return 'angle-right';
};

return (
<div style={
{
alignContent: 'center',
textAlign: 'center',
fontFamily: 'monospace',
fontSize: '12px',
paddingTop: '10px',
display: 'flex',
...style
}}>
<div
style={{
paddingLeft: '10px',
paddingRight: '15px'
}}
>
<Button
size={'sm'}
variant={'secondary'}
icon={getFoldIcon()}
onClick={() => onClick(rowIndex)}
/>
</div>
<>
{dateTimeParse(timestamp, {timeZone: getTimeZone()}).format("YYYY-MM-DD @ HH:mm:ss:SSS Z").toString()}
</>
</div>
);
}

const FieldCell = () => {
return (<></>);
}

const HeaderCell = (column: LogColumn, style) => {
// TODO: Handle field types
// const log = data.logs[rowIndex];
// const _timeField = data.timeField;

// TODO: Implement sorting?
return (
<div
style={{
paddingLeft: column.logColumnType === LogColumnType.TIME ? '10px' : '0px',
...style
}}
>
{column.logColumnType === LogColumnType.TIME ? 'Time' : 'Document'}
</div>
)
}

// Either expands or collapses the row, depending on the state its currently in
const invertRow = (expandedRows: boolean[], rowIndex: number): boolean[] => {
expandedRows[rowIndex] = !expandedRows[rowIndex]
return expandedRows
}

// If the row isn't being expanded, then shrink it back to its original size.
// NOTE: Because of how we handle it down below, "shrink it to its original size"
// effectively means set it to 0.
const shrinkRows = (expandedRows: boolean[], rowIndex: number, setSize: (index: number, value: number) => void): boolean => {
if (!expandedRows[rowIndex]) {
setSize(rowIndex, 0);
return true;
}
return false;
}



const LogCell = ({ columnIndex, rowIndex, style, data }) => {
const log = data.logs[rowIndex];
const timestamp = data.timestamps[rowIndex];
const column = data.columns[columnIndex];
const setExpandedRowsAndReRender = data.setExpandedRowsAndReRender;
const expandedRows = data.expandedRows;
const datasourceUid: string = data.datasourceUid;
const datasourceName: string = data.datasourceName;
const datasourceField: string = data.datasourceField;
const { setSize } = getLogTableContext();
const darkModeEnabled = useTheme2().isDark ;


// TODO: Ignoring for now as these will be used in a future pass
// const _timeField = data.timeField;
// const _setColumns = data.setColumns

const handleOnClick = (rowIndex: number): any => {
const newExpandedRows = invertRow(expandedRows, rowIndex);
shrinkRows(newExpandedRows, rowIndex, setSize);
setExpandedRowsAndReRender([...newExpandedRows], rowIndex);
}

const outline = darkModeEnabled ? DARK_THEME_OUTLINE : LIGHT_THEME_OUTLINE;

// Handle drawing the borders for the entire row
// Only draw a borderon the left if we're on the left-most cell
if (columnIndex === 0) {
style['borderLeft'] = outline;
}

// Only draw a border on the top if we're on the top-most cell
if (rowIndex === 0) {
style['borderTop'] = outline;
}

// Only draw a border on the right if we're on the right-most cell
if (columnIndex === data.columns.length - 1) {
style['borderRight'] = outline;
}

style['borderBottom'] = outline;



// Header row
if (rowIndex === 0) {
return HeaderCell(column, style)

}

if (column.logColumnType === LogColumnType.TIME) {
return TimestampCell(timestamp, style, rowIndex, expandedRows, handleOnClick);
} else if (column.logColumnType === LogColumnType.DOCUMENT) {
return DocumentCell(log, style, rowIndex, expandedRows[rowIndex], datasourceUid, datasourceName, datasourceField);
} else {
return FieldCell();
}
};

export default LogCell
Loading
Loading