Skip to content

Commit

Permalink
Feat/ux improvements (#66)
Browse files Browse the repository at this point in the history
* feat: added copying from all fields in modal

feat: added copying from all fields in modal

* feat: Filter / Export visibility improvements

* feat: added disabling/enabling whole category of filters

* feat: fixed Simple mode

feat: fixed Simple mode

* feat: improved naming scheme and descriptions for settings

* chore: new version

* fix: conditions for export button

* feat: added filter tooltips, callid filtering and checking if any filters exist before showing menu

* feat: added tooltip to "simple mode" switch

* fix: added missing line

* fix: removed part of tooltip that got added by mistake

* fix: updated tooltip to be consistent with other ones

* feat: redo eth/ip/tcp/udp packet encoders in .ts

* fix: checkbox styling in filter

* fix: aligned checkboxes inside "collapse" with checkbox in "collapse" title

* feat: extraction of exporter functions from SimplePanel.tsx to separate files and proper typing for them

* feat: extraction of formatting, sorting and filtering functions from simple to separate files with proper types

* feat: revised filter tooltip visuals

* fix: ipv6 with middle segments missing

* fix: incorrect payload length + removal of accidentally setting flow label

* chore: cleanup
  • Loading branch information
AlexeyOplachko authored Apr 23, 2024
1 parent ad103f9 commit 678a14e
Show file tree
Hide file tree
Showing 21 changed files with 717 additions and 2,638 deletions.
9 changes: 4 additions & 5 deletions ngx-flow/projects/ngx-flow/src/lib/ngx-flow.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,13 @@ export class NgxFlowComponent implements OnInit {
description: i.aboveArrow || '',
destination_ip: dist.ip,
destination_port: dist.port,
diff: ' ',
diff_absolute: i.subTitle || '',
diff_num: ' ',
details: i.details,
dstAlias: DIST,
id: 0,
info_date: i.belowArrow || '... ',
info_date_absolute: i.subTitle || '... ',
info_date_absolute: i.details || '... ',
ipDirection: SRC + ' > ' + DIST,
line: i.line,
messageData: null,
method: ' ',
method_text: i.title || i.messageID,
Expand All @@ -110,7 +109,7 @@ export class NgxFlowComponent implements OnInit {
srcAlias: SRC,

typeItem: "SIP",
hash: i.hash
hash: i.hash,
}
}

Expand Down
4 changes: 2 additions & 2 deletions ngx-flow/projects/ngx-flow/src/models/flow.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface FlowItem {
destination: string;

title?: string;
subTitle?: string;
details?: string;

// hidden in Simplified mode
aboveArrow?: string;
Expand All @@ -27,7 +27,7 @@ export interface ArrowStyling {
}
export interface TextColors {
title?: string;
subTitle?: string;
details?: string;

// hidden in Simplified mode
aboveArrow?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@
</div>
<div class="call-text-date">
{{
isAbsolute
? item.diff_absolute
: item.diff
? item.diff
: "+0.00ms"
item.details
}}
</div>
<div class="call-text-date">
{{
item.line
}}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion ngx-flow/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class AppComponent {
{
messageID: 'some unique Id as string 1',
title: 'Title',
subTitle: 'subTitle',
details: 'details',
aboveArrow: 'aboveArrow',
belowArrow: 'belowArrow',
source: 'B',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "qxip-flow-panel",
"version": "10.1.0",
"version": "10.1.1",
"description": "Plugin providing Flow diagram for Grafana",
"scripts": {
"build:component": "cd ./ngx-flow && npm run build:component",
Expand Down
33 changes: 33 additions & 0 deletions src/components/CopyText/CopyText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { FC, useRef, useState } from 'react';
import { IconButton } from '@grafana/ui';


export const CopyText: FC<{ text: string }> = ({ text }) => {
const [copied, setCopied] = useState(false);
const textAreaRef: any = useRef(null);
const copy = () => {
if (navigator && navigator.clipboard) {
navigator.clipboard.writeText(text);
setCopied(true);
} else {
if (textAreaRef && textAreaRef.current) {
textAreaRef.current.focus();
textAreaRef.current.select();
document.execCommand('copy');
}
setCopied(false);
}
};
return (
<>
<textarea
ref={textAreaRef} value={text} style={{ pointerEvents: 'none', opacity: 0, position: 'fixed', left: 0, top: 0, border: 0, padding: 0 }} />

<IconButton
name="copy"
tooltip={copied ? 'Copied!' : 'Copy'}
onClick={copy}
/>
</>
)
}
127 changes: 108 additions & 19 deletions src/components/FilterPanel/FilterPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { css, cx } from "@emotion/css";
import { PanelData } from "@grafana/data";
import { Button, Checkbox, Collapse, HorizontalGroup, InlineSwitch, Toggletip, useTheme2 } from "@grafana/ui";
import { Button, Checkbox, Collapse, HorizontalGroup, InlineSwitch, Toggletip, useTheme2, Tooltip, Icon } from "@grafana/ui";
import React, { useEffect, useState } from "react";

export interface FilterProps {
data: PanelData
onFilter: Function
onSimplify: Function
options: any
}
export interface Filters {
ip: {
Expand All @@ -23,19 +24,33 @@ export interface Filters {
},
type: {
[key: string]: boolean
},
callid: {
[key: string]: boolean
}
}
export interface Filter {
name: string
value: boolean
}
export const FilterPanel = ({ data, onFilter, onSimplify }: FilterProps) => {
const getStyles = () => {
return {
collapseChildrenWrapper: css`
padding-left: 18px;
width: 100%;
height: 100%;
`

};
};
export const FilterPanel = ({ data, onFilter, onSimplify, options }: FilterProps) => {
const [ipsArray, setIpsArray] = useState<string[]>([]);
// const [portsArray, setPortsArray] = useState<string[]>([]);
// const [ipPortsArray, setIpPortsArray] = useState<string[]>([]);
const [methodsArray, setMethodsArray] = useState<string[]>([]);
const [payloadTypesArray, setPayloadTypesArray] = useState<string[]>([]);
const [filters, setFilters] = useState<Filters>({ ip: {}, port: {}, ipPort: {}, method: {}, type: {} })
const [callidsArray, setCallidsArray] = useState<string[]>([]);
const [filters, setFilters] = useState<Filters>({ ip: {}, port: {}, ipPort: {}, method: {}, type: {}, callid: {} });
const [isSimplify, setIsSimplify] = useState(false);
useEffect(() => {
const [serie] = data.series || [];
Expand All @@ -47,7 +62,8 @@ export const FilterPanel = ({ data, onFilter, onSimplify }: FilterProps) => {
const ipPorts = new Set<string>()
const methods = new Set<string>()
const payloadType = new Set<string>()
labelValues.forEach((label) => {
const callid = new Set<string>()
labelValues?.forEach((label) => {
ips.add(label.dst_ip)
ips.add(label.src_ip)
ports.add(label.dst_port)
Expand All @@ -58,18 +74,23 @@ export const FilterPanel = ({ data, onFilter, onSimplify }: FilterProps) => {
methods.add(label.response)
}
payloadType.add(label.type)
if (label.callid !== undefined) {
callid.add(label.callid)
}
})
setIpsArray(Array.from(ips))
// setPortsArray(Array.from(ports))
// setIpPortsArray(Array.from(ipPorts))
setMethodsArray(Array.from(methods))
setPayloadTypesArray(Array.from(payloadType))
setCallidsArray(Array.from(callid))
setFilters({
ip: Object.fromEntries([...ips].map(ip => [ip, true])),
port: Object.fromEntries([...ports].map(port => [port, true])),
ipPort: Object.fromEntries([...ipPorts].map(ipPort => [ipPort, true])),
method: Object.fromEntries([...methods].map(method => [method, true])),
type: Object.fromEntries([...payloadType].map(type => [type, true])),
callid: Object.fromEntries([...callid].map(callid => [callid, true])),
})
}, [data])
useEffect(() => {
Expand All @@ -86,9 +107,20 @@ export const FilterPanel = ({ data, onFilter, onSimplify }: FilterProps) => {
`
)}
>
<InlineSwitch showLabel={true} defaultChecked={false} value={isSimplify} onChange={() => { setIsSimplify(!isSimplify); }} label="Simple format" />
<Tooltip
content={`Changes display to simple format, which is more compact and doesn't have "${options?.aboveArrow}" ${options?.belowArrow && options?.aboveArrow ? 'and' : ''} "${options?.belowArrow}" labels`} placement="top">
<span style={{ display: 'flex', flexDirection: 'column' }}>
<InlineSwitch
showLabel={true}
defaultChecked={false}
value={isSimplify}
onChange={() => { setIsSimplify(!isSimplify); }}
label="Simple format" />
</span>
</Tooltip>
<hr />
<MyCollapse label="Payload type">
{payloadTypesArray.length > 0 &&
<MyCollapse label="Payload type" filterState={filters} filterProperty={'type'} setFilters={setFilters} filterLabel={"type"}>
<HorizontalGroup spacing="md" >
{payloadTypesArray.map((payloadType) => (
<Checkbox value={filters?.type?.[payloadType]} key={payloadType} defaultChecked={true} label={payloadType} onChange={(v) => {
Expand All @@ -97,23 +129,38 @@ export const FilterPanel = ({ data, onFilter, onSimplify }: FilterProps) => {
))}
</HorizontalGroup>
</MyCollapse>
<MyCollapse label="Method">
<HorizontalGroup spacing="md" wrap={true}>
}
{methodsArray.length > 0 &&
<MyCollapse label="Method" filterState={filters} filterProperty={'method'} setFilters={setFilters} filterLabel={"response"}>
<HorizontalGroup spacing="md" wrap={true}>
{methodsArray.map((method) => (
<Checkbox value={filters?.method?.[method]} key={method} defaultChecked={true} label={method} onChange={(v) => {
setFilters({ ...filters, method: { ...filters.method, [method]: (v.target as HTMLInputElement).checked } })
}} />
))}
</HorizontalGroup>
</MyCollapse>

<MyCollapse label="IP">
<HorizontalGroup spacing="md" wrap={true}>
}
{ipsArray.length > 0 &&
<MyCollapse label="IP" filterState={filters} filterProperty={'ip'} setFilters={setFilters} filterLabel={`src_ip" or "dst_ip`}>
<HorizontalGroup spacing="md" wrap={true}>
{ipsArray.map((ip) => (
<Checkbox value={filters?.ip?.[ip]} key={ip} defaultChecked={true} label={ip} onChange={(v) => setFilters({ ...filters, ip: { ...filters.ip, [ip]: (v.target as HTMLInputElement).checked } })} />
))}
</HorizontalGroup>
</MyCollapse>
}
{callidsArray.length > 0 &&
<MyCollapse label="Call ID" filterState={filters} filterProperty={'callid'} setFilters={setFilters} filterLabel={`callid`}>
<HorizontalGroup spacing="md" wrap={true}>
{callidsArray.map((callid) => (
<Checkbox value={filters?.callid?.[callid]} key={callid} defaultChecked={true} label={callid} onChange={(v) => setFilters({ ...filters, callid: { ...filters.callid, [callid]: (v.target as HTMLInputElement).checked } })} />

))}

</HorizontalGroup>
</MyCollapse>
}
</span>
);
const themeName: string = useTheme2().name;
Expand All @@ -125,23 +172,65 @@ export const FilterPanel = ({ data, onFilter, onSimplify }: FilterProps) => {
onClose={() => { onFilter(filters); onSimplify(isSimplify) }}
>
<Button className={cx(css`
position: absolute;
top: 15px;
right: 40px;
border: 1px solid ${themeName === 'Dark' ? 'hsla(240, 18.6%, 83.1%, 0.12)' : 'hsla(210, 12.2%, 16.1%, 0.12)'};
border-radius: 2px;
background-color: ${themeName === 'Dark' ? 'hsla(0, 0%, 0%, 0.5)' : 'hsla(0, 0%, 100%, 0.5)'};
z-index: 2;
background-color: ${themeName === 'Dark' ? 'hsla(0, 0%, 0%, 0.3)' : 'hsla(0, 0%, 100%, 0.3)'};
`)} id="filter" icon="filter" fill="text" variant="secondary" />
</Toggletip>

)
}
const MyCollapse = ({ label, children }: any) => {
interface MyCollapseProps {
label: string;
children: React.ReactNode;
filterState: any;
filterProperty: string;
setFilters: React.Dispatch<React.SetStateAction<any>>;
filterLabel: string;
}
const MyCollapse = ({ label, children, filterState, filterProperty, setFilters, filterLabel }: MyCollapseProps) => {
const [isOpen, setIsOpen] = useState(false)
const [indeterminate, setIndeterminate] = useState(false)
const [checked, setChecked] = useState(false)
useEffect(() => {
let count = 0
const keys = Object.values(filterState[filterProperty])
keys.forEach(item => {
if (item) {
count += 1
}
})
console.log(count, keys.length, filterState[filterProperty])
setIndeterminate(count > 0 && count < keys.length)
setChecked(count === keys.length)
}, [filterState, filterProperty])
const setFilterState = () => {

setFilters(() => ({
...filterState,
[filterProperty]: Object.fromEntries(
Object.entries(filterState[filterProperty]).map(([key]) => [key, !checked])
),
}));

}
return (
<Collapse collapsible={true} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} label={label}>
{children}

<span style={{ position: 'relative', width: '100%' }}>
<span style={{ position: 'absolute', left: '28px', right: 0, top: '10px', display: 'flex', alignItems: 'center' }}>
<Checkbox indeterminate={indeterminate} checked={checked} onChange={setFilterState} />
<span style={{ zIndex: 1, marginLeft: '8px', pointerEvents: 'none' }}>{label}</span>

<Tooltip content={`Filters by "${filterLabel}" label`}>
<Icon name="question-circle" style={{ zIndex: 1, position: 'absolute', right: '10px' }} size="xl"></Icon>
</Tooltip>
</span>
<Collapse collapsible={true} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} label={''}>
<div className={getStyles().collapseChildrenWrapper}>

{children}
</div>
</Collapse>
</span>
)
}
Loading

0 comments on commit 678a14e

Please sign in to comment.