Prettify: Add basic indentation hygiene

This commit is contained in:
Himanshu DIxit 2021-02-21 06:23:56 +05:30
parent a1331536ca
commit 999a5094bb
36 changed files with 578 additions and 445 deletions

View File

@ -1,18 +1,16 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties"
],
"env": {
"production": {
"presets": [
"minify"
]
}
}
}
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties"
],
"env": {
"production": {
"presets": ["minify"]
}
}
}

View File

@ -1,14 +1,12 @@
# Docker
# Docker
**Building image**
**Building image**
```docker-compose up``
``docker-compose up`
/ This will also run
or
```docker build . -t tagname```
`docker build . -t tagname`
**Tag to remote url- Introduce versinoing later on**
@ -16,7 +14,7 @@ or
docker tag signoz/frontend:latest 7296823551/signoz:latest
```
**Running locally**
**Running locally**
```
docker-compose up

View File

@ -1,7 +1,7 @@
version: "3.9"
services:
web:
build: .
image: signoz/frontend:latest
ports:
- "3000:3000"
web:
build: .
image: signoz/frontend:latest
ports:
- "3000:3000"

View File

@ -6,7 +6,8 @@
"scripts": {
"dev": "NODE_ENV=development webpack serve",
"start": "node scripts/start.js",
"build": "webpack --config=webpack.config.prod.js"
"build": "webpack --config=webpack.config.prod.js",
"prettify": "prettier --write ."
},
"author": "",
"license": "ISC",

View File

@ -2,7 +2,7 @@ import { Dispatch } from "redux";
import metricsAPI from "../api/metricsAPI";
import { GlobalTime } from "./global";
import { ActionTypes } from "./types";
import {Token} from "../utils/token";
import { Token } from "../utils/token";
import { toUTCEpoch } from "../utils/timeUtils";
export interface servicesListItem {
@ -61,7 +61,8 @@ export interface getFilteredTraceMetricsAction {
export const getServicesList = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = "services?start=" + globalTime.minTime + "&end=" + globalTime.maxTime;
let request_string =
"services?start=" + globalTime.minTime + "&end=" + globalTime.maxTime;
const response = await metricsAPI.get<servicesListItem[]>(request_string);
@ -125,7 +126,7 @@ export const getFilteredTraceMetrics = (
return async (dispatch: Dispatch) => {
let request_string =
"spans/aggregates?start=" +
toUTCEpoch( globalTime.minTime) +
toUTCEpoch(globalTime.minTime) +
"&end=" +
toUTCEpoch(globalTime.maxTime) +
"&" +

View File

@ -10,7 +10,7 @@ import { getUsageDataAction } from "./usage";
import { updateTimeIntervalAction } from "./global";
export enum ActionTypes {
updateTraceFilters= "UPDATE_TRACES_FILTER",
updateTraceFilters = "UPDATE_TRACES_FILTER",
updateInput = "UPDATE_INPUT",
fetchTraces = "FETCH_TRACES",
fetchTraceItem = "FETCH_TRACE_ITEM",

View File

@ -14,9 +14,16 @@ export interface getUsageDataAction {
payload: usageDataItem[];
}
export const getUsageData = (minTime: number, maxTime: number, step: number, service: string) => {
export const getUsageData = (
minTime: number,
maxTime: number,
step: number,
service: string,
) => {
return async (dispatch: Dispatch) => {
let request_string = `usage?start=${toUTCEpoch(minTime)}&end=${toUTCEpoch(maxTime)}&step=${step}&service=${service ? service : ""}`;
let request_string = `usage?start=${toUTCEpoch(minTime)}&end=${toUTCEpoch(
maxTime,
)}&step=${step}&service=${service ? service : ""}`;
//Step can only be multiple of 3600
const response = await metricsAPI.get<usageDataItem[]>(request_string);

View File

@ -1,8 +1,8 @@
import axios from "axios";
import { ENVIRONMENT } from "../constants/env";
import {Token} from "../utils/token";
import { Token } from "../utils/token";
// No auth for the API
export default axios.create({
baseURL: `${ENVIRONMENT.baseURL}/api/prom/api/v1`
baseURL: `${ENVIRONMENT.baseURL}/api/prom/api/v1`,
});

View File

@ -2,5 +2,5 @@ import axios from "axios";
import { ENVIRONMENT } from "../constants/env";
export default axios.create({
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`,
});

View File

@ -1,12 +1,12 @@
import axios from "axios";
import { ENVIRONMENT } from "../constants/env";
import {Token} from "../utils/token";
import { Token } from "../utils/token";
export default axios.create({
// baseURL: 'https://api.telegram.org/bot1518273960:AAHcgVvym9a0Qkl-PKiCI84X1VZaVbkTud0/',
// baseURL: 'http://104.211.113.204:8080/api/v1/',
// baseURL: "/api/v1/",
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`,
});
//https://api.telegram.org/bot1518273960:AAHcgVvym9a0Qkl-PKiCI84X1VZaVbkTud0/sendMessage?chat_id=351813222&text=Hello%20there

View File

@ -1,6 +1,6 @@
import axios from "axios";
import { ENVIRONMENT } from "../constants/env";
import {Token} from "../utils/token";
import { Token } from "../utils/token";
//import { format } from 'path';
export default axios.create({
@ -8,5 +8,5 @@ export default axios.create({
// baseURL: process.env.QUERY_SERVICE_URL,
// console.log('in traces API', process.env.QUERY_SERVICE_URL)
// baseURL: "/api/v1/",
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`
baseURL: `${ENVIRONMENT.baseURL}/api/v1/`,
});

View File

@ -1,8 +1,8 @@
@import "~antd/dist/antd.dark.css";
@import "~antd/dist/antd.compact.css";
.ant-space-item{
margin-right: 0 !important;
.ant-space-item {
margin-right: 0 !important;
}
/* #components-layout-demo-side .logo {
height: 32px;

View File

@ -14,7 +14,7 @@ import {
DeploymentUnitOutlined,
AlignLeftOutlined,
AppstoreOutlined,
SettingOutlined
SettingOutlined,
} from "@ant-design/icons";
import DateTimeSelector from "Src/components/DateTimeSelector";
@ -23,14 +23,28 @@ import styled from "styled-components";
const { Content, Footer, Sider } = Layout;
const ServiceMetrics = React.lazy(() => import("Src/components/metrics/ServiceMetricsDef"));
const ServiceMap = React.lazy(() => import("Src/components/servicemap/ServiceMap"));
const TraceDetail = React.lazy(() => import("Src/components/traces/TraceDetail"));
const TraceGraph = React.lazy(() => import("Src/components/traces/TraceGraphDef"));
const UsageExplorer = React.lazy(() => import("Src/components/usage/UsageExplorerDef"));
const ServicesTable = React.lazy(() => import("Src/components/metrics/ServicesTableDef"));
const Signup = React.lazy(() => import('Src/components/Signup'));
const SettingsPage = React.lazy(() => import("Src/components/settings/settingsPage"));
const ServiceMetrics = React.lazy(
() => import("Src/components/metrics/ServiceMetricsDef"),
);
const ServiceMap = React.lazy(
() => import("Src/components/servicemap/ServiceMap"),
);
const TraceDetail = React.lazy(
() => import("Src/components/traces/TraceDetail"),
);
const TraceGraph = React.lazy(
() => import("Src/components/traces/TraceGraphDef"),
);
const UsageExplorer = React.lazy(
() => import("Src/components/usage/UsageExplorerDef"),
);
const ServicesTable = React.lazy(
() => import("Src/components/metrics/ServicesTableDef"),
);
const Signup = React.lazy(() => import("Src/components/Signup"));
const SettingsPage = React.lazy(
() => import("Src/components/settings/settingsPage"),
);
//PNOTE
//React. lazy currently only supports default exports. If the module you want to import uses named exports, you can create an intermediate module that reexports it as the default. This ensures that tree shaking keeps working and that you don't pull in unused components.
@ -93,7 +107,7 @@ const App = () => {
Metrics
</NavLink>
</Menu.Item>
<Menu.Item key="2" icon={<AlignLeftOutlined/>}>
<Menu.Item key="2" icon={<AlignLeftOutlined />}>
<NavLink to="/traces" style={{ fontSize: 12, textDecoration: "none" }}>
Traces
</NavLink>
@ -115,10 +129,7 @@ const App = () => {
</NavLink>
</Menu.Item>
<Menu.Item key="5" icon={<SettingOutlined />}>
<NavLink
to="/settings"
style={{ fontSize: 12, textDecoration: "none" }}
>
<NavLink to="/settings" style={{ fontSize: 12, textDecoration: "none" }}>
Settings
</NavLink>
</Menu.Item>
@ -148,7 +159,6 @@ const App = () => {
<Route path="/usage-explorer" component={UsageExplorer} />
<Route path="/" component={ServicesTable} />
<Route path="/application" exact component={ServicesTable} />
</Switch>
</Suspense>
</Content>

View File

@ -1,18 +1,13 @@
import React, { Suspense, useState } from "react";
import { Spin } from "antd";
import {
Route,
Switch,
Redirect
} from "react-router-dom";
import { Route, Switch, Redirect } from "react-router-dom";
import Signup from "./Signup";
const App = React.lazy(() => import("Src/components/App"));
const AppWrapper = () => {
console.log("other")
console.log("other");
return (
<Suspense fallback={<Spin size="large"/>}>
<Suspense fallback={<Spin size="large" />}>
<Switch>
<Route path="/application" exact component={App} />
<Route path="/application/:servicename" component={App} />

View File

@ -40,7 +40,7 @@ const _DateTimeSelector = (props: DateTimeSelectorProps) => {
const [refreshButtonClick, setRefreshButtoClick] = useState(0);
const [form_dtselector] = Form.useForm();
const location = useLocation();
const updateTimeOnQueryParamChange = ()=>{
const updateTimeOnQueryParamChange = () => {
const timeDurationInLocalStorage = localStorage.getItem(
LOCAL_STORAGE.METRICS_TIME_IN_DURATION,
);
@ -48,13 +48,18 @@ const _DateTimeSelector = (props: DateTimeSelectorProps) => {
const urlParams = new URLSearchParams(window.location.search);
const intervalInQueryParam = urlParams.get(METRICS_PAGE_QUERY_PARAM.interval);
const startTimeString = urlParams.get(METRICS_PAGE_QUERY_PARAM.startTime);
const endTimeString = urlParams.get(METRICS_PAGE_QUERY_PARAM.endTime);
const endTimeString = urlParams.get(METRICS_PAGE_QUERY_PARAM.endTime);
// first pref: handle both startTime and endTime
if(startTimeString && startTimeString.length>0 && endTimeString && endTimeString.length>0){
if (
startTimeString &&
startTimeString.length > 0 &&
endTimeString &&
endTimeString.length > 0
) {
const startTime = moment(Number(startTimeString));
const endTime = moment(Number(endTimeString));
setCustomTime(startTime,endTime,true)
setCustomTime(startTime, endTime, true);
}
// first pref: handle intervalInQueryParam
else if (intervalInQueryParam) {
@ -66,8 +71,7 @@ const _DateTimeSelector = (props: DateTimeSelectorProps) => {
} else if (timeDurationInLocalStorage) {
setMetricsTimeInterval(timeDurationInLocalStorage);
}
}
};
// On URL Change
useEffect(() => {
@ -79,22 +83,20 @@ const _DateTimeSelector = (props: DateTimeSelectorProps) => {
updateTimeOnQueryParamChange();
}, []);
const setMetricsTimeInterval= (value: string) => {
const setMetricsTimeInterval = (value: string) => {
props.updateTimeInterval(value);
setTimeInterval(value);
setEndTime(null);
setStartTime(null);
window.localStorage.setItem(
LOCAL_STORAGE.METRICS_TIME_IN_DURATION,
value,
);
window.localStorage.setItem(LOCAL_STORAGE.METRICS_TIME_IN_DURATION, value);
};
const setCustomTime= (startTime: moment.Moment, endTime: moment.Moment, triggeredByURLChange = false) => {
props.updateTimeInterval("custom", [
startTime.valueOf(),
endTime.valueOf(),
]);
const setCustomTime = (
startTime: moment.Moment,
endTime: moment.Moment,
triggeredByURLChange = false,
) => {
props.updateTimeInterval("custom", [startTime.valueOf(), endTime.valueOf()]);
setEndTime(endTime);
setStartTime(startTime);
};
@ -105,10 +107,17 @@ const _DateTimeSelector = (props: DateTimeSelectorProps) => {
}); //pass time in URL query param for all choices except custom in datetime picker
};
const updateUrlForCustomTime= (startTime: moment.Moment, endTime: moment.Moment, triggeredByURLChange = false) => {
props.history.push(`?${METRICS_PAGE_QUERY_PARAM.startTime}=${startTime.valueOf()}&${METRICS_PAGE_QUERY_PARAM.endTime}=${endTime.valueOf()}`);
}
const updateUrlForCustomTime = (
startTime: moment.Moment,
endTime: moment.Moment,
triggeredByURLChange = false,
) => {
props.history.push(
`?${METRICS_PAGE_QUERY_PARAM.startTime}=${startTime.valueOf()}&${
METRICS_PAGE_QUERY_PARAM.endTime
}=${endTime.valueOf()}`,
);
};
const handleOnSelect = (value: string) => {
if (value === "custom") {
@ -131,7 +140,7 @@ const _DateTimeSelector = (props: DateTimeSelectorProps) => {
const startTime = dateTimeRange[0].valueOf();
const endTime = dateTimeRange[1].valueOf();
updateUrlForCustomTime(moment(startTime),moment(endTime))
updateUrlForCustomTime(moment(startTime), moment(endTime));
//setting globaltime
setRefreshButtonHidden(true);
form_dtselector.setFieldsValue({
@ -146,35 +155,35 @@ const _DateTimeSelector = (props: DateTimeSelectorProps) => {
const timeSinceLastRefresh = () => {
const currentTime = moment();
const lastRefresh = moment(props.globalTime.maxTime / 1000000)
const lastRefresh = moment(props.globalTime.maxTime / 1000000);
const duration = moment.duration(currentTime.diff(lastRefresh));
const secondsDiff = Math.floor(duration.asSeconds());
const minutedDiff = Math.floor(duration.asMinutes());
const hoursDiff = Math.floor(duration.asHours())
const hoursDiff = Math.floor(duration.asHours());
if(hoursDiff>0){
return `Last refresh - ${hoursDiff} hrs ago`
}else if(minutedDiff>0){
return `Last refresh - ${minutedDiff} mins ago`
if (hoursDiff > 0) {
return `Last refresh - ${hoursDiff} hrs ago`;
} else if (minutedDiff > 0) {
return `Last refresh - ${minutedDiff} mins ago`;
}
return `Last refresh - ${secondsDiff} sec ago`
return `Last refresh - ${secondsDiff} sec ago`;
};
const handleRefresh = () => {
setRefreshButtoClick(refreshButtonClick+1);
setRefreshButtoClick(refreshButtonClick + 1);
setMetricsTimeInterval(timeInterval);
};
useEffect(()=>{
setRefreshText("")
const interval = setInterval(()=>{
setRefreshText(timeSinceLastRefresh())
}, 2000)
return ()=>{
clearInterval(interval)
}
},[props.location,refreshButtonClick])
useEffect(() => {
setRefreshText("");
const interval = setInterval(() => {
setRefreshText(timeSinceLastRefresh());
}, 2000);
return () => {
clearInterval(interval);
};
}, [props.location, refreshButtonClick]);
const options = [
{ value: "custom", label: "Custom" },
@ -188,36 +197,47 @@ const _DateTimeSelector = (props: DateTimeSelectorProps) => {
if (props.location.pathname.startsWith("/usage-explorer")) {
return null;
} else {
const inputLabeLToShow = startTime && endTime? (`${startTime.format("YYYY/MM/DD HH:mm")} - ${endTime.format("YYYY/MM/DD HH:mm")}`):timeInterval
const inputLabeLToShow =
startTime && endTime
? `${startTime.format("YYYY/MM/DD HH:mm")} - ${endTime.format(
"YYYY/MM/DD HH:mm",
)}`
: timeInterval;
return (
<DateTimeWrapper>
<Space style={{float: "right", display:"block"}}>
<Space>
<Form
form={form_dtselector}
layout="inline"
initialValues={{ interval: "15min" }}
style={{ marginTop: 10, marginBottom: 10 }}
>
<Select onSelect={handleOnSelect} value={inputLabeLToShow}>
{options.map(({ value, label }) => (
<Option value={value}>{label}</Option>
))}
</Select>
<Space style={{ float: "right", display: "block" }}>
<Space>
<Form
form={form_dtselector}
layout="inline"
initialValues={{ interval: "15min" }}
style={{ marginTop: 10, marginBottom: 10 }}
>
<Select onSelect={handleOnSelect} value={inputLabeLToShow}>
{options.map(({ value, label }) => (
<Option value={value}>{label}</Option>
))}
</Select>
<FormItem hidden={refreshButtonHidden} name="refresh_button">
<Button type="primary" onClick={handleRefresh}>
Refresh
</Button>
</FormItem>
</Form>
</Space>
<Space style={{float: "right", display:"block", marginRight: 20, minHeight: 23, width: 200, textAlign: "right"}}>
{refreshText}
</Space>
<FormItem hidden={refreshButtonHidden} name="refresh_button">
<Button type="primary" onClick={handleRefresh}>
Refresh
</Button>
</FormItem>
</Form>
</Space>
<Space
style={{
float: "right",
display: "block",
marginRight: 20,
minHeight: 23,
width: 200,
textAlign: "right",
}}
>
{refreshText}
</Space>
<CustomDateTimeModal
visible={customDTPickerVisible}
onCreate={handleCustomDate}

View File

@ -250,4 +250,4 @@ const Signup = (props: SignUpProps) => {
);
};
export default withRouter(Signup);
export default withRouter(Signup);

View File

@ -1,9 +1,5 @@
const Test = () => {
return (
<div>
INSIDE PUBLIC CODE
</div>
)
}
return <div>INSIDE PUBLIC CODE</div>;
};
export default Test;
export default Test;

View File

@ -1,20 +1,28 @@
import React, { ReactElement, useState } from "react";
import { Modal, Button } from 'antd';
import { Modal, Button } from "antd";
export const CustomModal = ({title,children, isModalVisible, setIsModalVisible, footer, closable=true}:{
isModalVisible: boolean,
closable?: boolean,
setIsModalVisible: Function,
footer?: any,
title: string,
children: ReactElement
export const CustomModal = ({
title,
children,
isModalVisible,
setIsModalVisible,
footer,
closable = true,
}: {
isModalVisible: boolean;
closable?: boolean;
setIsModalVisible: Function;
footer?: any;
title: string;
children: ReactElement;
}) => {
return (
<>
<Modal title={title} visible={isModalVisible} footer={footer}
closable={closable}
<Modal
title={title}
visible={isModalVisible}
footer={footer}
closable={closable}
>
{children}
</Modal>

View File

@ -187,7 +187,7 @@ class LatencyLineChart extends React.Component<LatencyLineChartProps> {
>
<PopUpElements
onClick={() => {
this.props.popupClickHandler(this.state.firstpoint_ts)
this.props.popupClickHandler(this.state.firstpoint_ts);
}}
>
View Traces

View File

@ -31,7 +31,7 @@ interface ServicesMetricsProps extends RouteComponentProps<any> {
}
const _ServiceMetrics = (props: ServicesMetricsProps) => {
const {servicename} = useParams<{ servicename?: string }>();
const { servicename } = useParams<{ servicename?: string }>();
useEffect(() => {
props.getServicesMetrics(servicename, props.globalTime);
props.getTopEndpoints(servicename, props.globalTime);
@ -41,22 +41,18 @@ const _ServiceMetrics = (props: ServicesMetricsProps) => {
const tMinus15Min = timestamp / 1000000 - 15 * 60 * 1000;
const currentTime = timestamp / 1000000;
props.updateTimeInterval("custom", [
tMinus15Min,
currentTime,
]); // updateTimeInterval takes second range in ms -- give -5 min to selected time,
props.updateTimeInterval("custom", [tMinus15Min, currentTime]); // updateTimeInterval takes second range in ms -- give -5 min to selected time,
const urlParams = new URLSearchParams();
urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime,tMinus15Min.toString())
urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime,currentTime.toString())
if(servicename){
urlParams.set(METRICS_PAGE_QUERY_PARAM.service,servicename)
urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, tMinus15Min.toString());
urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, currentTime.toString());
if (servicename) {
urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename);
}
props.history.push(`/traces?${urlParams.toString()}`);
};
return (
<Tabs defaultActiveKey="1">
<TabPane tab="Application Metrics" key="1">

View File

@ -32,14 +32,14 @@ const Wrapper = styled.div`
`;
const TableLoadingWrapper = styled.div`
display: flex;
justify-content: center;
margin-top: 80px;
`
display: flex;
justify-content: center;
margin-top: 80px;
`;
const LoadingText = styled.div`
margin-left: 16px;
`
margin-left: 16px;
`;
const columns = [
{
@ -81,71 +81,93 @@ const columns = [
const _ServicesTable = (props: ServicesTableProps) => {
const search = useLocation().search;
const time_interval = new URLSearchParams(search).get("time");
const [initialDataFetch, setDataFetched] = useState(false)
const [errorObject, setErrorObject] = useState({message: "", isError: false});
const isEmptyServiceList = !initialDataFetch && props.servicesList.length === 0;
const [initialDataFetch, setDataFetched] = useState(false);
const [errorObject, setErrorObject] = useState({
message: "",
isError: false,
});
const isEmptyServiceList =
!initialDataFetch && props.servicesList.length === 0;
const refetchFromBackend = isEmptyServiceList || errorObject.isError;
const [skipOnboarding, setSkipOnboarding ] = useState(localStorage.getItem('skip_onboarding') === "true");
const [skipOnboarding, setSkipOnboarding] = useState(
localStorage.getItem("skip_onboarding") === "true",
);
const onContinueClick = ()=>{
localStorage.setItem('skip_onboarding', 'true');
setSkipOnboarding(true)
}
const onContinueClick = () => {
localStorage.setItem("skip_onboarding", "true");
setSkipOnboarding(true);
};
function getApiServiceData() {
props.getServicesList(props.globalTime).then(() => {
props
.getServicesList(props.globalTime)
.then(() => {
setDataFetched(true);
setErrorObject({ message: "", isError: false });
}).catch((e: string) => {
})
.catch((e: string) => {
setErrorObject({ message: e, isError: true });
setDataFetched(true);
});
}
useEffect(getApiServiceData, [props.globalTime]);
useEffect(()=>{
if(props.servicesList.length > 1 ){
localStorage.removeItem('skip_onboarding') ;
useEffect(() => {
if (props.servicesList.length > 1) {
localStorage.removeItem("skip_onboarding");
}
refetchFromBackend && setTimeout(getApiServiceData, 50000)
}, [props.servicesList,errorObject])
refetchFromBackend && setTimeout(getApiServiceData, 50000);
}, [props.servicesList, errorObject]);
if(!initialDataFetch){
if (!initialDataFetch) {
return (
<TableLoadingWrapper>
<Spin/>
<LoadingText>Fetching data</LoadingText>
</TableLoadingWrapper>
)
<TableLoadingWrapper>
<Spin />
<LoadingText>Fetching data</LoadingText>
</TableLoadingWrapper>
);
}
if(refetchFromBackend && !skipOnboarding){
if (refetchFromBackend && !skipOnboarding) {
return (
<CustomModal title={"Setup instrumentation"}
isModalVisible={true}
closable={false}
setIsModalVisible={()=>{}}
footer={[
<Button key="submit" type="primary" onClick={onContinueClick}>
Continue without instrumentation
</Button>,
]}
<CustomModal
title={"Setup instrumentation"}
isModalVisible={true}
closable={false}
setIsModalVisible={() => {}}
footer={[
<Button key="submit" type="primary" onClick={onContinueClick}>
Continue without instrumentation
</Button>,
]}
>
<div>
<iframe width="100%" height="265" src="https://www.youtube.com/embed/Ly34WBQ2640" frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen></iframe>
<div style={{margin: "20px 0"}}>
<Spin/>
<iframe
width="100%"
height="265"
src="https://www.youtube.com/embed/Ly34WBQ2640"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
<div style={{ margin: "20px 0" }}>
<Spin />
</div>
<div>
No instrumentation data.<br/>
Please instrument your application as mentioned <a href={"https://signoz.io/docs/instrumentation/overview"} target={"_blank"}>here</a>
No instrumentation data.
<br />
Please instrument your application as mentioned{" "}
<a
href={"https://signoz.io/docs/instrumentation/overview"}
target={"_blank"}
>
here
</a>
</div>
</div>
</CustomModal>
)
);
}
return (
@ -157,9 +179,19 @@ const _ServicesTable = (props: ServicesTableProps) => {
/>
{props.servicesList[0].numCalls === 0 && (
<Space style={{width: '100%', margin: "40px 0", justifyContent: "center"}}>
<Space
style={{ width: "100%", margin: "40px 0", justifyContent: "center" }}
>
No applications present. Please add instrumentation (follow this
<a href={"https://signoz.io/docs/instrumentation/overview"} target={"_blank"} style={{marginLeft: 3}}>guide</a>)</Space>
<a
href={"https://signoz.io/docs/instrumentation/overview"}
target={"_blank"}
style={{ marginLeft: 3 }}
>
guide
</a>
)
</Space>
)}
</Wrapper>
);

View File

@ -1,72 +1,63 @@
import React, { useEffect, useState } from "react";
import { Form, Input, Space } from "antd";
import { Form, Input, Space } from "antd";
import { connect } from "react-redux";
import { Tooltip } from 'antd';
import { InfoCircleOutlined,EyeTwoTone,EyeInvisibleOutlined } from '@ant-design/icons';
import { Tooltip } from "antd";
import {
InfoCircleOutlined,
EyeTwoTone,
EyeInvisibleOutlined,
} from "@ant-design/icons";
import { StoreState } from "../../reducers";
import { useAuthenticationData } from "../../hooks/authentication";
import { TOKEN_DATE } from "../../../../../sass/frontend/src/constants/accessToken";
import { Alert } from 'antd';
import { Alert } from "antd";
interface SettingsPageProps {
}
interface SettingsPageProps {}
const layout = {
labelCol: { span: 3 },
wrapperCol: { span: 6 },
};
const SettingsPage = (props: SettingsPageProps) => {
const [form] = Form.useForm();
useEffect(()=>{
useEffect(() => {
form.setFieldsValue({
retention_period: "3 days"
retention_period: "3 days",
});
})
});
return (
<React.Fragment>
<Form
{...layout}
name="basic"
initialValues={{ remember: true }}
style={{ marginLeft: 20}}
style={{ marginLeft: 20 }}
form={form}
>
<Form.Item
label="Retention Period"
name="retention_period"
rules={[{ required: false }]}
>
<Input style={{ marginLeft: 60}} disabled={true}/>
<Input style={{ marginLeft: 60 }} disabled={true} />
</Form.Item>
</Form>
<Space style={{ marginLeft: 60, marginTop: 48}}>
<Space style={{ marginLeft: 60, marginTop: 48 }}>
<Alert
message="Mail us at plans@signoz.io to change your plan or retention period"
type="info"
/>
</Space>
</React.Fragment>
);
};
const mapStateToProps = (
state: StoreState
): { } => {
const mapStateToProps = (state: StoreState): {} => {
return {};
};
export default connect(mapStateToProps, {
})(SettingsPage);
export default connect(mapStateToProps, {})(SettingsPage);

View File

@ -5,12 +5,13 @@ import { Store } from "antd/lib/form/interface";
interface LatencyModalFormProps {
onCreate: (values: Store) => void; //Store is defined in antd forms library
onCancel: () => void;
latencyFilterValues: {min: string, max: string}
latencyFilterValues: { min: string; max: string };
}
const LatencyModalForm: React.FC<LatencyModalFormProps> = ({
onCreate,
onCancel,latencyFilterValues
onCancel,
latencyFilterValues,
}) => {
const [form] = Form.useForm();
return (

View File

@ -43,18 +43,21 @@ const _TraceFilter = (props: TraceFilterProps) => {
const [serviceList, setServiceList] = useState<string[]>([]);
const [operationList, setOperationsList] = useState<string[]>([]);
const [tagKeyOptions, setTagKeyOptions] = useState<TagKeyOptionItem[]>([]);
const location = useLocation()
const location = useLocation();
const urlParams = new URLSearchParams(location.search.split("?")[1]);
useEffect(() => {
metricsAPI.get<string[]>("services/list").then((response) => {
setServiceList(response.data);
}).then(()=>{
const serviceName =urlParams.get(METRICS_PAGE_QUERY_PARAM.service);
if(serviceName){
handleChangeService(serviceName)
metricsAPI
.get<string[]>("services/list")
.then((response) => {
setServiceList(response.data);
})
.then(() => {
const serviceName = urlParams.get(METRICS_PAGE_QUERY_PARAM.service);
if (serviceName) {
handleChangeService(serviceName);
}
});
});
}, []);
useEffect(() => {
@ -120,7 +123,10 @@ const _TraceFilter = (props: TraceFilterProps) => {
const [loading] = useState(false);
const [tagKeyValueApplied, setTagKeyValueApplied] = useState([""]);
const [latencyFilterValues, setLatencyFilterValues] = useState<{min: string, max: string}>({
const [latencyFilterValues, setLatencyFilterValues] = useState<{
min: string;
max: string;
}>({
min: "100",
max: "500",
});
@ -158,7 +164,7 @@ const _TraceFilter = (props: TraceFilterProps) => {
const onLatencyModalApply = (values: Store) => {
setModalVisible(false);
const { min, max}= values
const { min, max } = values;
props.updateTraceFilters({
...props.traceFilters,
latency: {
@ -167,7 +173,7 @@ const _TraceFilter = (props: TraceFilterProps) => {
},
});
setLatencyFilterValues({min, max})
setLatencyFilterValues({ min, max });
};
const onTagFormSubmit = (values: any) => {
@ -382,13 +388,15 @@ const _TraceFilter = (props: TraceFilterProps) => {
</FormItem>
</Form>
{modalVisible && <LatencyModalForm
onCreate={onLatencyModalApply}
latencyFilterValues={latencyFilterValues}
onCancel={() => {
setModalVisible(false);
}}
/>}
{modalVisible && (
<LatencyModalForm
onCreate={onLatencyModalApply}
latencyFilterValues={latencyFilterValues}
onCancel={() => {
setModalVisible(false);
}}
/>
)}
</div>
);
};

View File

@ -46,7 +46,7 @@ const _TraceGraph = (props: TraceGraphProps) => {
.default()
.attr("class", "d3-tip")
.html(function (d: any) {
return d.data.name + "<br>duration: " + d.data.value/1000000+'ms';
return d.data.name + "<br>duration: " + d.data.value / 1000000 + "ms";
});
const onClick = (z: any) => {
@ -59,8 +59,8 @@ const _TraceGraph = (props: TraceGraphProps) => {
.cellHeight(18)
.transitionDuration(500)
.inverted(true)
.tooltip(tip)
.minFrameSize(10)
.tooltip(tip)
.minFrameSize(10)
.elided(false)
.differential(false)
.sort(true)
@ -69,7 +69,7 @@ const _TraceGraph = (props: TraceGraphProps) => {
// Source flamegraph.js line 557 and 573.
// .selfValue(true)
.onClick(onClick)
.title("Trace Flame graph");
.title("Trace Flame graph");
return (
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
@ -84,7 +84,7 @@ const _TraceGraph = (props: TraceGraphProps) => {
<Button type="primary" onClick={setResetZoom.bind(this, true)}>
Reset Zoom
</Button>
<div id="chart" style={{ fontSize: 12, marginTop: 20}}></div>
<div id="chart" style={{ fontSize: 12, marginTop: 20 }}></div>
</Card>
<SelectedSpanDetails clickedSpanTags={clickedSpanTags} />

View File

@ -107,11 +107,22 @@ const _TraceList = (props: TraceListProps) => {
return <Table dataSource={dataSource} columns={columns} size="middle" />;
} else {
if(isOnboardingSkipped()){
return ( <Space style={{width: '100%', margin: "40px 0", justifyContent: "center"}}>
No spans found. Please add instrumentation (follow this
<a href={"https://signoz.io/docs/instrumentation/overview"} target={"_blank"} style={{marginLeft: 3}}>guide</a>)</Space>)
if (isOnboardingSkipped()) {
return (
<Space
style={{ width: "100%", margin: "40px 0", justifyContent: "center" }}
>
No spans found. Please add instrumentation (follow this
<a
href={"https://signoz.io/docs/instrumentation/overview"}
target={"_blank"}
style={{ marginLeft: 3 }}
>
guide
</a>
)
</Space>
);
}
return <div> No spans found for given filter!</div>;
}

View File

@ -3,7 +3,13 @@ import { Bar } from "react-chartjs-2";
import { Card, Form, Select, Space } from "antd";
import { connect } from "react-redux";
import { getServicesList, getUsageData, GlobalTime, servicesListItem, usageDataItem } from "../../actions";
import {
getServicesList,
getUsageData,
GlobalTime,
servicesListItem,
usageDataItem,
} from "../../actions";
import { StoreState } from "../../reducers";
import moment from "moment";
import { isOnboardingSkipped } from "../../utils/app";
@ -20,37 +26,58 @@ interface UsageExplorerProps {
const timeDaysOptions = [
{ value: 30, label: "Last 30 Days" },
{ value: 7, label: "Last week" },
{ value: 1, label: "Last day" }
{ value: 1, label: "Last day" },
];
const interval = [
{ value: 604800, chartDivideMultiplier: 1, label: "Weekly", applicableOn: [timeDaysOptions[0]] },
{ value: 86400, chartDivideMultiplier: 30,label: "Daily", applicableOn: [timeDaysOptions[0],timeDaysOptions[1]] },
{ value: 3600, chartDivideMultiplier: 10,label: "Hours", applicableOn: [timeDaysOptions[2],timeDaysOptions[1]] },,
{
value: 604800,
chartDivideMultiplier: 1,
label: "Weekly",
applicableOn: [timeDaysOptions[0]],
},
{
value: 86400,
chartDivideMultiplier: 30,
label: "Daily",
applicableOn: [timeDaysOptions[0], timeDaysOptions[1]],
},
{
value: 3600,
chartDivideMultiplier: 10,
label: "Hours",
applicableOn: [timeDaysOptions[2], timeDaysOptions[1]],
},
,
];
const _UsageExplorer = (props: UsageExplorerProps) => {
const [selectedTime, setSelectedTime] = useState(timeDaysOptions[1])
const [selectedInterval, setSelectedInterval] = useState(interval[2])
const [selectedService, setSelectedService] = useState<string>("")
const [selectedTime, setSelectedTime] = useState(timeDaysOptions[1]);
const [selectedInterval, setSelectedInterval] = useState(interval[2]);
const [selectedService, setSelectedService] = useState<string>("");
useEffect(() => {
if(selectedTime && selectedInterval) {
const maxTime = new Date().getTime() * 1000000 ;
const minTime = (maxTime - (selectedTime.value * 24 * 3600000 * 1000000 ) );
if (selectedTime && selectedInterval) {
const maxTime = new Date().getTime() * 1000000;
const minTime = maxTime - selectedTime.value * 24 * 3600000 * 1000000;
props.getUsageData(minTime, maxTime, selectedInterval!.value, selectedService);
props.getUsageData(
minTime,
maxTime,
selectedInterval!.value,
selectedService,
);
}
}, [selectedTime,selectedInterval,selectedService]);
}, [selectedTime, selectedInterval, selectedService]);
useEffect(() => {
props.getServicesList(props.globalTime);
props.getServicesList(props.globalTime);
}, []);
const data = {
labels: props.usageData.map((s) => moment(s.timestamp/1000000).format("MMM Do h a")),
labels: props.usageData.map((s) =>
moment(s.timestamp / 1000000).format("MMM Do h a"),
),
datasets: [
{
label: "Span Count",
@ -62,7 +89,6 @@ const _UsageExplorer = (props: UsageExplorerProps) => {
],
};
const options = {
scales: {
yAxes: [
@ -82,50 +108,78 @@ const _UsageExplorer = (props: UsageExplorerProps) => {
return (
<React.Fragment>
{/* PNOTE - TODO - Keep it in reponsive row column tab */}
<Space style={{marginTop: 40, marginLeft: 20}}>
<Space>
<Select onSelect={(value, option)=>{
setSelectedTime(timeDaysOptions.filter((item)=>item.value == parseInt(value))[0])
}} value={selectedTime.label}>
{timeDaysOptions.map(({ value, label }) => (
<Option value={value}>{label}</Option>
))}
</Select>
</Space>
<Space style={{ marginTop: 40, marginLeft: 20 }}>
<Space>
<Select onSelect={(value)=>{
setSelectedInterval(interval.filter((item)=>item!.value === parseInt(value))[0])
}} value={selectedInterval!.label}>
<Select
onSelect={(value, option) => {
setSelectedTime(
timeDaysOptions.filter((item) => item.value == parseInt(value))[0],
);
}}
value={selectedTime.label}
>
{timeDaysOptions.map(({ value, label }) => (
<Option value={value}>{label}</Option>
))}
</Select>
</Space>
<Space>
<Select
onSelect={(value) => {
setSelectedInterval(
interval.filter((item) => item!.value === parseInt(value))[0],
);
}}
value={selectedInterval!.label}
>
{interval
.filter((interval) => interval!.applicableOn.includes(selectedTime))
.map((item) => (
<Option value={item!.value}>{item!.label}</Option>
))}
</Select>
</Space>
{interval.filter((interval)=>interval!.applicableOn.includes(selectedTime)).map((item) => (
<Option value={item!.value}>{item!.label}</Option>
<Space>
<Select
onSelect={(value) => {
setSelectedService(value);
}}
value={selectedService || "All Services"}
>
<Option value={""}>All Services</Option>
{props.servicesList.map((service) => (
<Option value={service.serviceName}>{service.serviceName}</Option>
))}
</Select>
</Space>
<Space>
<Select onSelect={(value) => {
setSelectedService(value)
}} value={ selectedService || "All Services"}>
<Option value={""}>All Services</Option>
{props.servicesList.map((service)=><Option value={service.serviceName}>{service.serviceName}</Option>)}
</Select>
</Space>
{ isOnboardingSkipped() && props.totalCount === 0 ? (
<Space style={{width: '100%', margin: "40px 0", marginLeft: 20, justifyContent: "center"}}>
{isOnboardingSkipped() && props.totalCount === 0 ? (
<Space
style={{
width: "100%",
margin: "40px 0",
marginLeft: 20,
justifyContent: "center",
}}
>
No spans found. Please add instrumentation (follow this
<a href={"https://signoz.io/docs/instrumentation/overview"} target={"_blank"} style={{marginLeft: 3}}>guide</a>)</Space>
): (
<Space style={{display: "block", marginLeft: 20, width:200}}>
<a
href={"https://signoz.io/docs/instrumentation/overview"}
target={"_blank"}
style={{ marginLeft: 3 }}
>
guide
</a>
)
</Space>
) : (
<Space style={{ display: "block", marginLeft: 20, width: 200 }}>
{`Total count is ${props.totalCount}`}
</Space>
)
}
)}
</Space>
<Card style={{ width: "90%", margin: 20 }} bodyStyle={{ padding: 20 }}>
<Bar data={data} options={options} />
</Card>
@ -134,16 +188,26 @@ const _UsageExplorer = (props: UsageExplorerProps) => {
};
const mapStateToProps = (
state: StoreState
): { totalCount: number; globalTime: GlobalTime; servicesList: servicesListItem[]; usageData: usageDataItem[]; } => {
state: StoreState,
): {
totalCount: number;
globalTime: GlobalTime;
servicesList: servicesListItem[];
usageData: usageDataItem[];
} => {
let totalCount = 0;
for(let item of state.usageDate){
totalCount = totalCount+item.count;
for (let item of state.usageDate) {
totalCount = totalCount + item.count;
}
return {totalCount:totalCount, usageData: state.usageDate, globalTime: state.globalTime, servicesList: state.servicesList };
return {
totalCount: totalCount,
usageData: state.usageDate,
globalTime: state.globalTime,
servicesList: state.servicesList,
};
};
export const UsageExplorer = connect(mapStateToProps, {
getUsageData: getUsageData,
getServicesList: getServicesList
getServicesList: getServicesList,
})(_UsageExplorer);

View File

@ -1,6 +1,4 @@
export const WITHOUT_SESSION_PATH = [
"/redirect"
]
export const WITHOUT_SESSION_PATH = ["/redirect"];
export const AUTH0_REDIRECT_PATH = "/redirect";

View File

@ -2,6 +2,5 @@ export enum METRICS_PAGE_QUERY_PARAM {
interval = "interval",
startTime = "startTime",
endTime = "endTime",
service = "service"
service = "service",
}

View File

@ -10,7 +10,7 @@ import { Auth0Provider } from "@auth0/auth0-react";
import AppWrapper from "Src/components/AppWrapper";
import "Src/assets/index.css";
import { reducers } from "./reducers";
import {BrowserRouter as Router} from "react-router-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { AUTH0_CLIENT_ID, AUTH0_DOMAIN } from "./constants/env";
// import Signup from './components/Signup';
// @ts-ignore
@ -31,7 +31,6 @@ ReactDOM.render(
</Router>
</ThemeSwitcherProvider>
</React.StrictMode>
</Provider>
,
</Provider>,
document.querySelector("#root"),
);

View File

@ -3,7 +3,7 @@ import { ActionTypes, Action, GlobalTime } from "../actions";
export const updateGlobalTimeReducer = (
state: GlobalTime = {
maxTime: Date.now() * 1000000,
minTime: (Date.now() - 15 * 60 * 1000) * 1000000
minTime: (Date.now() - 15 * 60 * 1000) * 1000000,
},
action: Action,
) => {

View File

@ -1 +1,3 @@
export const isOnboardingSkipped = ()=>{return localStorage.getItem('skip_onboarding') === "true"}
export const isOnboardingSkipped = () => {
return localStorage.getItem("skip_onboarding") === "true";
};

View File

@ -1,4 +1,4 @@
export const toUTCEpoch = (time: number):number=>{
const x = new Date()
return (time + x.getTimezoneOffset()*60*1000);
}
export const toUTCEpoch = (time: number): number => {
const x = new Date();
return time + x.getTimezoneOffset() * 60 * 1000;
};

View File

@ -1,3 +1,3 @@
export class Token{
static auth0Token = ""
};
export class Token {
static auth0Token = "";
}

View File

@ -1,58 +1,58 @@
// shared config (dev and prod)
const { resolve } = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
console.log(resolve(__dirname, './src/'));
console.log(resolve(__dirname, "./src/"));
module.exports = {
mode: "development",
devtool: "source-map",
entry: resolve(__dirname, "./src/index.tsx"),
devServer: {
historyApiFallback: true,
publicPath: "/",
transportMode: 'ws',
contentBase: [resolve(__dirname, "./public")],
hot: true,
liveReload: false,
inline:true,
port: 3000,
},
output: {
filename: "js/bundle.[chunkhash].min.js",
path: resolve(__dirname, "./build"),
publicPath: "/",
},
resolve: {
alias: {
Src: resolve(__dirname, './src/')
},
extensions: [".ts", ".tsx",".js", ".jsx"],
},
module: {
rules: [
{
test: [/\.jsx?$/, /\.tsx?$/],
use: ["babel-loader"],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(scss|sass)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: [
"file-loader?hash=sha512&digest=hex&name=img/[chunkhash].[ext]",
"image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false",
],
},
],
},
plugins: [new HtmlWebpackPlugin({ template: "src/index.html.ejs" })],
performance: {
hints: false,
},
};
mode: "development",
devtool: "source-map",
entry: resolve(__dirname, "./src/index.tsx"),
devServer: {
historyApiFallback: true,
publicPath: "/",
transportMode: "ws",
contentBase: [resolve(__dirname, "./public")],
hot: true,
liveReload: false,
inline: true,
port: 3000,
},
output: {
filename: "js/bundle.[chunkhash].min.js",
path: resolve(__dirname, "./build"),
publicPath: "/",
},
resolve: {
alias: {
Src: resolve(__dirname, "./src/"),
},
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
module: {
rules: [
{
test: [/\.jsx?$/, /\.tsx?$/],
use: ["babel-loader"],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(scss|sass)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: [
"file-loader?hash=sha512&digest=hex&name=img/[chunkhash].[ext]",
"image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false",
],
},
],
},
plugins: [new HtmlWebpackPlugin({ template: "src/index.html.ejs" })],
performance: {
hints: false,
},
};

View File

@ -4,54 +4,52 @@ const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
mode: "production",
devtool: "source-map",
entry: resolve(__dirname, "./src/index.tsx"),
output: {
filename: "js/bundle.[chunkhash].min.js",
path: resolve(__dirname, "./build"),
publicPath: "/",
},
mode: "production",
devtool: "source-map",
entry: resolve(__dirname, "./src/index.tsx"),
output: {
filename: "js/bundle.[chunkhash].min.js",
path: resolve(__dirname, "./build"),
publicPath: "/",
},
resolve: {
alias: {
Src: resolve(__dirname, './src/'),
},
extensions: [".ts", ".tsx",".js", ".jsx"],
},
module: {
rules: [
{
test: [/\.jsx?$/, /\.tsx?$/],
use: ["babel-loader"],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(scss|sass)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: [
"file-loader?hash=sha512&digest=hex&name=img/[chunkhash].[ext]",
"image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false",
],
},
],
},
plugins: [
new HtmlWebpackPlugin({ template: "src/index.html.ejs" }),
new CopyPlugin({
patterns: [
{ from: resolve(__dirname, "public/"), to: "." },
],
})
],
performance: {
hints: false,
},
};
resolve: {
alias: {
Src: resolve(__dirname, "./src/"),
},
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
module: {
rules: [
{
test: [/\.jsx?$/, /\.tsx?$/],
use: ["babel-loader"],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(scss|sass)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: [
"file-loader?hash=sha512&digest=hex&name=img/[chunkhash].[ext]",
"image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false",
],
},
],
},
plugins: [
new HtmlWebpackPlugin({ template: "src/index.html.ejs" }),
new CopyPlugin({
patterns: [{ from: resolve(__dirname, "public/"), to: "." }],
}),
],
performance: {
hints: false,
},
};