mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-08-01 00:35:31 +00:00
Make the protocol color affect the details layout color and write protocol abbreviation vertically
This commit is contained in:
parent
c5969c267e
commit
d20cc1412b
@ -13,7 +13,7 @@ var protocol api.Protocol = api.Protocol{
|
|||||||
Abbreviation: "AMQP",
|
Abbreviation: "AMQP",
|
||||||
BackgroundColor: "#ff6600",
|
BackgroundColor: "#ff6600",
|
||||||
ForegroundColor: "#ffffff",
|
ForegroundColor: "#ffffff",
|
||||||
FontSize: 10,
|
FontSize: 12,
|
||||||
ReferenceLink: "https://www.rabbitmq.com/amqp-0-9-1-reference.html",
|
ReferenceLink: "https://www.rabbitmq.com/amqp-0-9-1-reference.html",
|
||||||
OutboundPorts: []string{"5671", "5672"},
|
OutboundPorts: []string{"5671", "5672"},
|
||||||
InboundPorts: []string{},
|
InboundPorts: []string{},
|
||||||
|
@ -21,7 +21,7 @@ var protocol api.Protocol = api.Protocol{
|
|||||||
Abbreviation: "HTTP",
|
Abbreviation: "HTTP",
|
||||||
BackgroundColor: "#205cf5",
|
BackgroundColor: "#205cf5",
|
||||||
ForegroundColor: "#ffffff",
|
ForegroundColor: "#ffffff",
|
||||||
FontSize: 10,
|
FontSize: 12,
|
||||||
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc2616",
|
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc2616",
|
||||||
OutboundPorts: []string{"80", "8080", "443"},
|
OutboundPorts: []string{"80", "8080", "443"},
|
||||||
InboundPorts: []string{},
|
InboundPorts: []string{},
|
||||||
@ -29,11 +29,11 @@ var protocol api.Protocol = api.Protocol{
|
|||||||
|
|
||||||
var http2Protocol api.Protocol = api.Protocol{
|
var http2Protocol api.Protocol = api.Protocol{
|
||||||
Name: "http",
|
Name: "http",
|
||||||
LongName: "Hypertext Transfer Protocol Version 2 (HTTP/2)",
|
LongName: "Hypertext Transfer Protocol Version 2 (HTTP/2) (gRPC)",
|
||||||
Abbreviation: "HTTP/2",
|
Abbreviation: "HTTP/2",
|
||||||
BackgroundColor: "#244c5a",
|
BackgroundColor: "#244c5a",
|
||||||
ForegroundColor: "#ffffff",
|
ForegroundColor: "#ffffff",
|
||||||
FontSize: 10,
|
FontSize: 12,
|
||||||
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc7540",
|
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc7540",
|
||||||
OutboundPorts: []string{"80", "8080", "443"},
|
OutboundPorts: []string{"80", "8080", "443"},
|
||||||
InboundPorts: []string{},
|
InboundPorts: []string{},
|
||||||
|
@ -13,7 +13,7 @@ var protocol api.Protocol = api.Protocol{
|
|||||||
Abbreviation: "KAFKA",
|
Abbreviation: "KAFKA",
|
||||||
BackgroundColor: "#000000",
|
BackgroundColor: "#000000",
|
||||||
ForegroundColor: "#ffffff",
|
ForegroundColor: "#ffffff",
|
||||||
FontSize: 10,
|
FontSize: 12,
|
||||||
ReferenceLink: "https://kafka.apache.org/protocol",
|
ReferenceLink: "https://kafka.apache.org/protocol",
|
||||||
OutboundPorts: []string{"9092"},
|
OutboundPorts: []string{"9092"},
|
||||||
InboundPorts: []string{},
|
InboundPorts: []string{},
|
||||||
|
@ -89,9 +89,9 @@ export const HarEntry: React.FC<HAREntryProps> = ({entry, setFocusedEntryId, isS
|
|||||||
<div className={styles.directionContainer}>
|
<div className={styles.directionContainer}>
|
||||||
<span className={styles.port} title="Source Port">{entry.source_port}</span>
|
<span className={styles.port} title="Source Port">{entry.source_port}</span>
|
||||||
{entry.isOutgoing ?
|
{entry.isOutgoing ?
|
||||||
<img src={outgoingIcon} alt="Outgoing traffic" title="Outgoing"/>
|
<img src={outgoingIcon} alt="Ingoing traffic" title="Ingoing"/>
|
||||||
:
|
:
|
||||||
<img src={ingoingIcon} alt="Ingoing traffic" title="Ingoing"/>
|
<img src={ingoingIcon} alt="Outgoing traffic" title="Outgoing"/>
|
||||||
}
|
}
|
||||||
<span className={styles.port} title="Destination Port">{entry.destination_port}</span>
|
<span className={styles.port} title="Destination Port">{entry.destination_port}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,7 +77,7 @@ export const HAREntryDetailed: React.FC<HarEntryDetailedProps> = ({classes, harE
|
|||||||
<HarEntryTitle protocol={harEntry.protocol} har={har}/>
|
<HarEntryTitle protocol={harEntry.protocol} har={har}/>
|
||||||
{har && <HarEntrySummary har={har}/>}
|
{har && <HarEntrySummary har={har}/>}
|
||||||
<>
|
<>
|
||||||
{har && <HAREntryViewer representation={harEntry.representation}/>}
|
{har && <HAREntryViewer representation={harEntry.representation} color={harEntry.protocol.background_color}/>}
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
};
|
};
|
||||||
|
@ -29,13 +29,14 @@ const HAREntryViewLine: React.FC<HAREntryViewLineProps> = ({label, value}) => {
|
|||||||
|
|
||||||
|
|
||||||
interface HAREntrySectionCollapsibleTitleProps {
|
interface HAREntrySectionCollapsibleTitleProps {
|
||||||
title: string;
|
title: string,
|
||||||
isExpanded: boolean;
|
color: string,
|
||||||
|
isExpanded: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const HAREntrySectionCollapsibleTitle: React.FC<HAREntrySectionCollapsibleTitleProps> = ({title, isExpanded}) => {
|
const HAREntrySectionCollapsibleTitle: React.FC<HAREntrySectionCollapsibleTitleProps> = ({title, color, isExpanded}) => {
|
||||||
return <div className={styles.title}>
|
return <div className={styles.title}>
|
||||||
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
|
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`} style={{backgroundColor: color}}>
|
||||||
{isExpanded ? '-' : '+'}
|
{isExpanded ? '-' : '+'}
|
||||||
</span>
|
</span>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
@ -43,32 +44,35 @@ const HAREntrySectionCollapsibleTitle: React.FC<HAREntrySectionCollapsibleTitleP
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface HAREntrySectionContainerProps {
|
interface HAREntrySectionContainerProps {
|
||||||
title: string;
|
title: string,
|
||||||
|
color: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HAREntrySectionContainer: React.FC<HAREntrySectionContainerProps> = ({title, children}) => {
|
export const HAREntrySectionContainer: React.FC<HAREntrySectionContainerProps> = ({title, color, children}) => {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
return <CollapsibleContainer
|
return <CollapsibleContainer
|
||||||
className={styles.collapsibleContainer}
|
className={styles.collapsibleContainer}
|
||||||
isExpanded={expanded}
|
isExpanded={expanded}
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
title={<HAREntrySectionCollapsibleTitle title={title} isExpanded={expanded}/>}
|
title={<HAREntrySectionCollapsibleTitle title={title} color={color} isExpanded={expanded}/>}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CollapsibleContainer>
|
</CollapsibleContainer>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HAREntryBodySectionProps {
|
interface HAREntryBodySectionProps {
|
||||||
content: any;
|
content: any,
|
||||||
encoding?: string;
|
color: string,
|
||||||
contentType?: string;
|
encoding?: string,
|
||||||
|
contentType?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HAREntryBodySection: React.FC<HAREntryBodySectionProps> = ({
|
export const HAREntryBodySection: React.FC<HAREntryBodySectionProps> = ({
|
||||||
content,
|
color,
|
||||||
encoding,
|
content,
|
||||||
contentType,
|
encoding,
|
||||||
}) => {
|
contentType,
|
||||||
|
}) => {
|
||||||
const MAXIMUM_BYTES_TO_HIGHLIGHT = 10000; // The maximum of chars to highlight in body, in case the response can be megabytes
|
const MAXIMUM_BYTES_TO_HIGHLIGHT = 10000; // The maximum of chars to highlight in body, in case the response can be megabytes
|
||||||
const supportedLanguages = [['html', 'html'], ['json', 'json'], ['application/grpc', 'json']]; // [[indicator, languageToUse],...]
|
const supportedLanguages = [['html', 'html'], ['json', 'json'], ['application/grpc', 'json']]; // [[indicator, languageToUse],...]
|
||||||
const jsonLikeFormats = ['json'];
|
const jsonLikeFormats = ['json'];
|
||||||
@ -101,7 +105,7 @@ export const HAREntryBodySection: React.FC<HAREntryBodySectionProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{content && content?.length > 0 && <HAREntrySectionContainer title='Body'>
|
{content && content?.length > 0 && <HAREntrySectionContainer title='Body' color={color}>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<HAREntryViewLine label={'Mime type'} value={contentType}/>
|
<HAREntryViewLine label={'Mime type'} value={contentType}/>
|
||||||
@ -127,14 +131,15 @@ export const HAREntryBodySection: React.FC<HAREntryBodySectionProps> = ({
|
|||||||
|
|
||||||
interface HAREntrySectionProps {
|
interface HAREntrySectionProps {
|
||||||
title: string,
|
title: string,
|
||||||
|
color: string,
|
||||||
arrayToIterate: any[],
|
arrayToIterate: any[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HAREntryTableSection: React.FC<HAREntrySectionProps> = ({title, arrayToIterate}) => {
|
export const HAREntryTableSection: React.FC<HAREntrySectionProps> = ({title, color, arrayToIterate}) => {
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{
|
{
|
||||||
arrayToIterate && arrayToIterate.length > 0 ?
|
arrayToIterate && arrayToIterate.length > 0 ?
|
||||||
<HAREntrySectionContainer title={title}>
|
<HAREntrySectionContainer title={title} color={color}>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{arrayToIterate.map(({name, value}, index) => <HAREntryViewLine key={index} label={name}
|
{arrayToIterate.map(({name, value}, index) => <HAREntryViewLine key={index} label={name}
|
||||||
@ -151,6 +156,7 @@ export const HAREntryTableSection: React.FC<HAREntrySectionProps> = ({title, arr
|
|||||||
interface HAREntryPolicySectionProps {
|
interface HAREntryPolicySectionProps {
|
||||||
service: string,
|
service: string,
|
||||||
title: string,
|
title: string,
|
||||||
|
color: string,
|
||||||
response: any,
|
response: any,
|
||||||
latency?: number,
|
latency?: number,
|
||||||
arrayToIterate: any[],
|
arrayToIterate: any[],
|
||||||
@ -195,13 +201,13 @@ export const HAREntryPolicySectionContainer: React.FC<HAREntryPolicySectionConta
|
|||||||
</CollapsibleContainer>
|
</CollapsibleContainer>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HAREntryTablePolicySection: React.FC<HAREntryPolicySectionProps> = ({service, title, response, latency, arrayToIterate}) => {
|
export const HAREntryTablePolicySection: React.FC<HAREntryPolicySectionProps> = ({service, title, color, response, latency, arrayToIterate}) => {
|
||||||
const base64ToJson = response.content.mimeType === "application/json; charset=utf-8" ? JSON.parse(Buffer.from(response.content.text, "base64").toString()) : {};
|
const base64ToJson = response.content.mimeType === "application/json; charset=utf-8" ? JSON.parse(Buffer.from(response.content.text, "base64").toString()) : {};
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{
|
{
|
||||||
arrayToIterate && arrayToIterate.length > 0 ?
|
arrayToIterate && arrayToIterate.length > 0 ?
|
||||||
<>
|
<>
|
||||||
<HAREntrySectionContainer title={title}>
|
<HAREntrySectionContainer title={title} color={color}>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{arrayToIterate.map(({rule, matched}, index) => {
|
{arrayToIterate.map(({rule, matched}, index) => {
|
||||||
|
@ -3,19 +3,19 @@ import styles from './HAREntryViewer.module.sass';
|
|||||||
import Tabs from "../Tabs";
|
import Tabs from "../Tabs";
|
||||||
import {HAREntryTableSection, HAREntryBodySection, HAREntryTablePolicySection} from "./HAREntrySections";
|
import {HAREntryTableSection, HAREntryBodySection, HAREntryTablePolicySection} from "./HAREntrySections";
|
||||||
|
|
||||||
const SectionsRepresentation: React.FC<any> = ({data}) => {
|
const SectionsRepresentation: React.FC<any> = ({data, color}) => {
|
||||||
const sections = []
|
const sections = []
|
||||||
|
|
||||||
for (const [i, row] of data.entries()) {
|
for (const [i, row] of data.entries()) {
|
||||||
switch (row.type) {
|
switch (row.type) {
|
||||||
case "table":
|
case "table":
|
||||||
sections.push(
|
sections.push(
|
||||||
<HAREntryTableSection title={row.title} arrayToIterate={JSON.parse(row.data)}/>
|
<HAREntryTableSection title={row.title} color={color} arrayToIterate={JSON.parse(row.data)}/>
|
||||||
)
|
)
|
||||||
break;
|
break;
|
||||||
case "body":
|
case "body":
|
||||||
sections.push(
|
sections.push(
|
||||||
<HAREntryBodySection content={row.data} encoding={row.encoding} contentType={row.mime_type}/>
|
<HAREntryBodySection color={color} content={row.data} encoding={row.encoding} contentType={row.mime_type}/>
|
||||||
)
|
)
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -26,8 +26,7 @@ const SectionsRepresentation: React.FC<any> = ({data}) => {
|
|||||||
return <>{sections}</>;
|
return <>{sections}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutoRepresentation: React.FC<any> = ({representation, isResponseMocked}) => {
|
const AutoRepresentation: React.FC<any> = ({representation, color, isResponseMocked}) => {
|
||||||
console.log(representation)
|
|
||||||
const {request, response} = JSON.parse(representation);
|
const {request, response} = JSON.parse(representation);
|
||||||
|
|
||||||
const rulesMatched = []
|
const rulesMatched = []
|
||||||
@ -47,18 +46,18 @@ const AutoRepresentation: React.FC<any> = ({representation, isResponseMocked}) =
|
|||||||
return <div className={styles.harEntry}>
|
return <div className={styles.harEntry}>
|
||||||
{<div className={styles.body}>
|
{<div className={styles.body}>
|
||||||
<div className={styles.bodyHeader}>
|
<div className={styles.bodyHeader}>
|
||||||
<Tabs tabs={TABS} currentTab={currentTab} onChange={setCurrentTab} leftAligned/>
|
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
|
||||||
{request?.url && <a className={styles.endpointURL} href={request.payload.url} target='_blank' rel="noreferrer">{request.payload.url}</a>}
|
{request?.url && <a className={styles.endpointURL} href={request.payload.url} target='_blank' rel="noreferrer">{request.payload.url}</a>}
|
||||||
</div>
|
</div>
|
||||||
{currentTab === TABS[0].tab && <React.Fragment>
|
{currentTab === TABS[0].tab && <React.Fragment>
|
||||||
<SectionsRepresentation data={request}/>
|
<SectionsRepresentation data={request} color={color}/>
|
||||||
</React.Fragment>}
|
</React.Fragment>}
|
||||||
{currentTab === TABS[1].tab && <React.Fragment>
|
{currentTab === TABS[1].tab && <React.Fragment>
|
||||||
<SectionsRepresentation data={response}/>
|
<SectionsRepresentation data={response} color={color}/>
|
||||||
</React.Fragment>}
|
</React.Fragment>}
|
||||||
{currentTab === TABS[2].tab && <React.Fragment>
|
{currentTab === TABS[2].tab && <React.Fragment>
|
||||||
{// FIXME: Fix here
|
{// FIXME: Fix here
|
||||||
<HAREntryTablePolicySection service={representation.log.entries[0].service} title={'Rule'} latency={0} response={response} arrayToIterate={rulesMatched ? rulesMatched : []}/>}
|
<HAREntryTablePolicySection service={representation.log.entries[0].service} title={'Rule'} color={color} latency={0} response={response} arrayToIterate={rulesMatched ? rulesMatched : []}/>}
|
||||||
</React.Fragment>}
|
</React.Fragment>}
|
||||||
</div>}
|
</div>}
|
||||||
</div>;
|
</div>;
|
||||||
@ -66,12 +65,13 @@ const AutoRepresentation: React.FC<any> = ({representation, isResponseMocked}) =
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
representation: any;
|
representation: any;
|
||||||
|
color: string,
|
||||||
isResponseMocked?: boolean;
|
isResponseMocked?: boolean;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HAREntryViewer: React.FC<Props> = ({representation, isResponseMocked, showTitle=true}) => {
|
const HAREntryViewer: React.FC<Props> = ({representation, color, isResponseMocked, showTitle=true}) => {
|
||||||
return <AutoRepresentation representation={representation} isResponseMocked={isResponseMocked} showTitle={showTitle}/>
|
return <AutoRepresentation representation={representation} color={color} isResponseMocked={isResponseMocked} showTitle={showTitle}/>
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HAREntryViewer;
|
export default HAREntryViewer;
|
||||||
|
@ -26,7 +26,7 @@ const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal}) => {
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: protocol.background_color,
|
backgroundColor: protocol.background_color,
|
||||||
color: protocol.foreground_color,
|
color: protocol.foreground_color,
|
||||||
fontSize: protocol.font_size * 1.3,
|
fontSize: protocol.font_size * 1.1,
|
||||||
}}
|
}}
|
||||||
title={protocol.abbreviation}
|
title={protocol.abbreviation}
|
||||||
>
|
>
|
||||||
|
@ -16,6 +16,7 @@ interface Props {
|
|||||||
classes?: any,
|
classes?: any,
|
||||||
tabs: Tab[],
|
tabs: Tab[],
|
||||||
currentTab: string,
|
currentTab: string,
|
||||||
|
color: string,
|
||||||
onChange: (string) => void,
|
onChange: (string) => void,
|
||||||
leftAligned?: boolean,
|
leftAligned?: boolean,
|
||||||
dark?: boolean,
|
dark?: boolean,
|
||||||
@ -28,7 +29,7 @@ const useTabsStyles = makeStyles((theme) => ({
|
|||||||
paddingTop: 15
|
paddingTop: 15
|
||||||
},
|
},
|
||||||
|
|
||||||
tab: {
|
tab: {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
color: variables.blueColor,
|
color: variables.blueColor,
|
||||||
@ -71,7 +72,7 @@ const useTabsStyles = makeStyles((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
const Tabs: React.FC<Props> = ({classes={}, tabs, currentTab, onChange, leftAligned, dark}) => {
|
const Tabs: React.FC<Props> = ({classes={}, tabs, currentTab, color, onChange, leftAligned, dark}) => {
|
||||||
const _classes = {...useTabsStyles(), ...classes};
|
const _classes = {...useTabsStyles(), ...classes};
|
||||||
return <div className={`${_classes.root} ${leftAligned ? _classes.tabsAlignLeft : ''}`}>
|
return <div className={`${_classes.root} ${leftAligned ? _classes.tabsAlignLeft : ''}`}>
|
||||||
{tabs.filter((tab) => !tab.hidden).map(({tab, disabled, disabledMessage, highlight, badge}, index) => {
|
{tabs.filter((tab) => !tab.hidden).map(({tab, disabled, disabledMessage, highlight, badge}, index) => {
|
||||||
@ -80,6 +81,7 @@ const Tabs: React.FC<Props> = ({classes={}, tabs, currentTab, onChange, leftAlig
|
|||||||
key={tab}
|
key={tab}
|
||||||
className={`${_classes.tab} ${active ? _classes.active : ''} ${disabled ? _classes.disabled : ''} ${highlight ? _classes.highlight : ''} ${dark ? 'dark' : ''}`}
|
className={`${_classes.tab} ${active ? _classes.active : ''} ${disabled ? _classes.disabled : ''} ${highlight ? _classes.highlight : ''} ${dark ? 'dark' : ''}`}
|
||||||
onClick={() => !disabled && onChange(tab)}
|
onClick={() => !disabled && onChange(tab)}
|
||||||
|
style={{color: color}}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab}
|
||||||
|
|
||||||
@ -94,4 +96,4 @@ const Tabs: React.FC<Props> = ({classes={}, tabs, currentTab, onChange, leftAlig
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Tabs;
|
export default Tabs;
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
text-align: center
|
text-align: center
|
||||||
font-size: 10px
|
font-size: 10px
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
// letter-spacing: 2px;
|
|
||||||
background-color: #000
|
background-color: #000
|
||||||
color: #fff
|
color: #fff
|
||||||
margin-left: -8px;
|
margin-left: -8px;
|
||||||
@ -13,12 +12,13 @@
|
|||||||
|
|
||||||
.vertical
|
.vertical
|
||||||
line-height: 22px
|
line-height: 22px
|
||||||
|
letter-spacing: 0.5px;
|
||||||
width: 22px
|
width: 22px
|
||||||
height: 48px
|
height: 48px
|
||||||
border-radius: 4px 0 0 4px
|
border-radius: 0px 4px 4px 0
|
||||||
writing-mode: vertical-lr;
|
writing-mode: vertical-lr;
|
||||||
// transform: rotate(-180deg);
|
transform: rotate(-180deg);
|
||||||
text-orientation: upright;
|
text-orientation: mixed;
|
||||||
|
|
||||||
.horizontal
|
.horizontal
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
|
Loading…
Reference in New Issue
Block a user