This commit is contained in:
Ankit Nayan 2021-09-23 16:19:42 +05:30
commit 3b7484f423
184 changed files with 7044 additions and 1807 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules node_modules
yarn.lock yarn.lock
package.json
deploy/docker/environment_tiny/common_test deploy/docker/environment_tiny/common_test
frontend/node_modules frontend/node_modules

View File

@ -9,6 +9,7 @@ module.exports = {
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/eslint-recommended',
'plugin:prettier/recommended',
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
@ -18,7 +19,18 @@ module.exports = {
ecmaVersion: 12, ecmaVersion: 12,
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['react', '@typescript-eslint', 'simple-import-sort'], plugins: [
'react',
'@typescript-eslint',
'simple-import-sort',
'react-hooks',
'prettier',
],
settings: {
react: {
version: 'latest',
},
},
rules: { rules: {
'react/jsx-filename-extension': [ 'react/jsx-filename-extension': [
'error', 'error',
@ -30,12 +42,21 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-var-requires': 0, '@typescript-eslint/no-var-requires': 0,
'linebreak-style': ['error', 'unix'], 'linebreak-style': ['error', 'unix'],
indent: ['error', 'tab'],
quotes: ['error', 'single'],
semi: ['error', 'always'],
// simple sort error // simple sort error
'simple-import-sort/imports': 'error', 'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error', 'simple-import-sort/exports': 'error',
// hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'prettier/prettier': [
'error',
{},
{
usePrettierrc: true,
},
],
}, },
}; };

View File

@ -3,5 +3,6 @@
"useTabs": true, "useTabs": true,
"tabWidth": 1, "tabWidth": 1,
"singleQuote": true, "singleQuote": true,
"jsxSingleQuote": false "jsxSingleQuote": false,
"semi": true
} }

View File

@ -1,38 +1,38 @@
import ROUTES from "constants/routes"; import ROUTES from 'constants/routes';
const Login = ({ email, name }: LoginProps): void => { const Login = ({ email, name }: LoginProps): void => {
const emailInput = cy.findByPlaceholderText("mike@netflix.com"); const emailInput = cy.findByPlaceholderText('mike@netflix.com');
emailInput.then((emailInput) => { emailInput.then((emailInput) => {
const element = emailInput[0]; const element = emailInput[0];
// element is present // element is present
expect(element).not.undefined; expect(element).not.undefined;
expect(element.nodeName).to.be.equal("INPUT"); expect(element.nodeName).to.be.equal('INPUT');
}); });
emailInput.type(email).then((inputElements) => { emailInput.type(email).then((inputElements) => {
const inputElement = inputElements[0]; const inputElement = inputElements[0];
const inputValue = inputElement.getAttribute("value"); const inputValue = inputElement.getAttribute('value');
expect(inputValue).to.be.equals(email); expect(inputValue).to.be.equals(email);
}); });
const firstNameInput = cy.findByPlaceholderText("Mike"); const firstNameInput = cy.findByPlaceholderText('Mike');
firstNameInput.then((firstNameInput) => { firstNameInput.then((firstNameInput) => {
const element = firstNameInput[0]; const element = firstNameInput[0];
// element is present // element is present
expect(element).not.undefined; expect(element).not.undefined;
expect(element.nodeName).to.be.equal("INPUT"); expect(element.nodeName).to.be.equal('INPUT');
}); });
firstNameInput.type(name).then((inputElements) => { firstNameInput.type(name).then((inputElements) => {
const inputElement = inputElements[0]; const inputElement = inputElements[0];
const inputValue = inputElement.getAttribute("value"); const inputValue = inputElement.getAttribute('value');
expect(inputValue).to.be.equals(name); expect(inputValue).to.be.equals(name);
}); });
const gettingStartedButton = cy.get("button"); const gettingStartedButton = cy.get('button');
gettingStartedButton.click(); gettingStartedButton.click();
cy.location("pathname").then((e) => { cy.location('pathname').then((e) => {
expect(e).to.be.equal(ROUTES.APPLICATION); expect(e).to.be.equal(ROUTES.APPLICATION);
}); });
}; };

View File

@ -1,20 +1,20 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
import ROUTES from "constants/routes"; import ROUTES from 'constants/routes';
describe("App Layout", () => { describe('App Layout', () => {
beforeEach(() => { beforeEach(() => {
cy.visit(Cypress.env("baseUrl")); cy.visit(Cypress.env('baseUrl'));
}); });
it("Check the user is in Logged Out State", async () => { it('Check the user is in Logged Out State', async () => {
cy.location("pathname").then((e) => { cy.location('pathname').then((e) => {
expect(e).to.be.equal(ROUTES.SIGN_UP); expect(e).to.be.equal(ROUTES.SIGN_UP);
}); });
}); });
it("Logged In State", () => { it('Logged In State', () => {
const testEmail = "test@test.com"; const testEmail = 'test@test.com';
const firstName = "Test"; const firstName = 'Test';
cy.login({ cy.login({
email: testEmail, email: testEmail,

View File

@ -1,16 +1,17 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
import defaultApps from "../../fixtures/defaultApp.json"; import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
import convertToNanoSecondsToSecond from "lib/convertToNanoSecondsToSecond";
describe("Metrics", () => { import defaultApps from '../../fixtures/defaultApp.json';
describe('Metrics', () => {
beforeEach(() => { beforeEach(() => {
cy.visit(Cypress.env("baseUrl")); cy.visit(Cypress.env('baseUrl'));
const testEmail = "test@test.com"; const testEmail = 'test@test.com';
const firstName = "Test"; const firstName = 'Test';
cy cy
.intercept("GET", "/api/v1//services?start*", { fixture: "defaultApp.json" }) .intercept('GET', '/api/v1//services?start*', { fixture: 'defaultApp.json' })
.as("defaultApps"); .as('defaultApps');
cy.login({ cy.login({
email: testEmail, email: testEmail,
@ -18,10 +19,10 @@ describe("Metrics", () => {
}); });
}); });
it("Default Apps", () => { it('Default Apps', () => {
cy.wait("@defaultApps"); cy.wait('@defaultApps');
cy.get("tbody").then((elements) => { cy.get('tbody').then((elements) => {
const trElements = elements.children(); const trElements = elements.children();
expect(trElements.length).to.be.equal(defaultApps.length); expect(trElements.length).to.be.equal(defaultApps.length);

View File

@ -1,4 +1,5 @@
import '@testing-library/cypress/add-commands'; import '@testing-library/cypress/add-commands';
import Login, { LoginProps } from '../CustomFunctions/Login'; import Login, { LoginProps } from '../CustomFunctions/Login';
Cypress.Commands.add('login', Login); Cypress.Commands.add('login', Login);

View File

@ -26,10 +26,8 @@
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/chart.js": "^2.9.28",
"@types/d3": "^6.2.0", "@types/d3": "^6.2.0",
"@types/jest": "^26.0.15", "@types/jest": "^26.0.15",
"@types/node": "^14.14.7",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9", "@types/react-dom": "^16.9.9",
"@types/react-redux": "^7.1.11", "@types/react-redux": "^7.1.11",
@ -37,7 +35,7 @@
"@types/redux": "^3.6.0", "@types/redux": "^3.6.0",
"@types/styled-components": "^5.1.4", "@types/styled-components": "^5.1.4",
"@types/vis": "^4.21.21", "@types/vis": "^4.21.21",
"antd": "^4.8.0", "antd": "^4.16.13",
"axios": "^0.21.0", "axios": "^0.21.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-jest": "^26.6.0", "babel-jest": "^26.6.0",
@ -48,7 +46,8 @@
"bfj": "^7.0.2", "bfj": "^7.0.2",
"camelcase": "^6.1.0", "camelcase": "^6.1.0",
"case-sensitive-paths-webpack-plugin": "2.3.0", "case-sensitive-paths-webpack-plugin": "2.3.0",
"chart.js": "^2.9.4", "chart.js": "^3.4.0",
"chartjs-adapter-date-fns": "^2.0.0",
"css-loader": "4.3.0", "css-loader": "4.3.0",
"d3": "^6.2.0", "d3": "^6.2.0",
"d3-flame-graph": "^3.1.1", "d3-flame-graph": "^3.1.1",
@ -64,6 +63,7 @@
"eslint-webpack-plugin": "^2.1.0", "eslint-webpack-plugin": "^2.1.0",
"file-loader": "6.1.1", "file-loader": "6.1.1",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"history": "4.10.1",
"html-webpack-plugin": "5.1.0", "html-webpack-plugin": "5.1.0",
"identity-obj-proxy": "3.0.0", "identity-obj-proxy": "3.0.0",
"jest": "26.6.0", "jest": "26.6.0",
@ -78,13 +78,13 @@
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "17.0.0", "react": "17.0.0",
"react-app-polyfill": "^2.0.0", "react-app-polyfill": "^2.0.0",
"react-chartjs-2": "^2.11.1",
"react-chips": "^0.8.0", "react-chips": "^0.8.0",
"react-css-theme-switcher": "^0.1.6", "react-css-theme-switcher": "^0.1.6",
"react-dev-utils": "^11.0.0", "react-dev-utils": "^11.0.0",
"react-dom": "17.0.0", "react-dom": "17.0.0",
"react-force-graph": "^1.41.0", "react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5", "react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.2.5",
"react-modal": "^3.12.1", "react-modal": "^3.12.1",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-refresh": "^0.8.3", "react-refresh": "^0.8.3",
@ -103,6 +103,7 @@
"tsconfig-paths-webpack-plugin": "^3.5.1", "tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.0.5", "typescript": "^4.0.5",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"uuid": "^8.3.2",
"web-vitals": "^0.2.4", "web-vitals": "^0.2.4",
"webpack": "^5.23.0", "webpack": "^5.23.0",
"webpack-dev-server": "^3.11.2", "webpack-dev-server": "^3.11.2",
@ -129,7 +130,11 @@
"@babel/preset-react": "^7.12.13", "@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.12.17", "@babel/preset-typescript": "^7.12.17",
"@testing-library/cypress": "^8.0.0", "@testing-library/cypress": "^8.0.0",
"@types/d3-tip": "^3.5.5",
"@types/lodash-es": "^4.17.4", "@types/lodash-es": "^4.17.4",
"@types/node": "^14.17.12",
"@types/react-grid-layout": "^1.1.2",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2", "@typescript-eslint/parser": "^4.28.2",
"autoprefixer": "^9.0.0", "autoprefixer": "^9.0.0",
@ -138,9 +143,11 @@
"copy-webpack-plugin": "^7.0.0", "copy-webpack-plugin": "^7.0.0",
"cypress": "^8.3.0", "cypress": "^8.3.0",
"eslint": "^7.30.0", "eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^5.1.0", "eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-simple-import-sort": "^7.0.0",

View File

@ -2,28 +2,31 @@ import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { IS_LOGGED_IN } from 'constants/auth'; import { IS_LOGGED_IN } from 'constants/auth';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import history from 'lib/history';
import AppLayout from 'modules/AppLayout'; import AppLayout from 'modules/AppLayout';
import { RouteProvider } from 'modules/RouteProvider'; import { RouteProvider } from 'modules/RouteProvider';
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import { BrowserRouter, Redirect,Route, Switch } from 'react-router-dom'; import { Redirect, Route, Router, Switch } from 'react-router-dom';
import routes from './routes'; import routes from './routes';
const App = () => ( const App = (): JSX.Element => (
<BrowserRouter basename="/"> <Router history={history}>
<RouteProvider> <RouteProvider>
<AppLayout> <AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}> <Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch> <Switch>
{routes.map(({ path, component, exact }) => { {routes.map(({ path, component, exact }, index) => {
return <Route exact={exact} path={path} component={component} />; return (
<Route key={index} exact={exact} path={path} component={component} />
);
})} })}
{/* This logic should be moved to app layout */} {/* This logic should be moved to app layout */}
<Route <Route
path="/" path="/"
exact exact
render={() => { render={(): JSX.Element => {
return localStorage.getItem(IS_LOGGED_IN) === 'yes' ? ( return localStorage.getItem(IS_LOGGED_IN) === 'yes' ? (
<Redirect to={ROUTES.APPLICATION} /> <Redirect to={ROUTES.APPLICATION} />
) : ( ) : (
@ -31,12 +34,12 @@ const App = () => (
); );
}} }}
/> />
<Route path="*" component={NotFound} /> <Route path="*" exact component={NotFound} />
</Switch> </Switch>
</Suspense> </Suspense>
</AppLayout> </AppLayout>
</RouteProvider> </RouteProvider>
</BrowserRouter> </Router>
); );
export default App; export default App;

View File

@ -1,4 +1,4 @@
import Loadable from './components/Loadable'; import Loadable from 'components/Loadable';
export const ServiceMetricsPage = Loadable( export const ServiceMetricsPage = Loadable(
() => () =>
@ -59,3 +59,16 @@ export const InstrumentationPage = Loadable(
/* webpackChunkName: "InstrumentationPage" */ 'modules/add-instrumentation/instrumentationPage' /* webpackChunkName: "InstrumentationPage" */ 'modules/add-instrumentation/instrumentationPage'
), ),
); );
export const DashboardPage = Loadable(
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/Dashboard'),
);
export const NewDashboardPage = Loadable(
() => import(/* webpackChunkName: "New DashboardPage" */ 'pages/NewDashboard'),
);
export const DashboardWidget = Loadable(
() =>
import(/* webpackChunkName: "New DashboardPage" */ 'pages/DashboardWidget'),
);

View File

@ -1,6 +1,11 @@
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import DashboardWidget from 'pages/DashboardWidget';
import { RouteProps } from 'react-router-dom';
import { import {
DashboardPage,
InstrumentationPage, InstrumentationPage,
NewDashboardPage,
ServiceMapPage, ServiceMapPage,
ServiceMetricsPage, ServiceMetricsPage,
ServicesTablePage, ServicesTablePage,
@ -9,8 +14,7 @@ import {
TraceDetailPage, TraceDetailPage,
TraceGraphPage, TraceGraphPage,
UsageExplorerPage, UsageExplorerPage,
} from 'pages'; } from './pageComponents';
import { RouteProps } from 'react-router-dom';
const routes: AppRoutes[] = [ const routes: AppRoutes[] = [
{ {
@ -58,6 +62,21 @@ const routes: AppRoutes[] = [
exact: true, exact: true,
component: TraceDetailPage, component: TraceDetailPage,
}, },
{
path: ROUTES.ALL_DASHBOARD,
exact: true,
component: DashboardPage,
},
{
path: ROUTES.DASHBOARD,
exact: true,
component: NewDashboardPage,
},
{
path: ROUTES.DASHBOARD_WIDGET,
exact: true,
component: DashboardWidget,
},
]; ];
interface AppRoutes { interface AppRoutes {

View File

@ -0,0 +1,57 @@
import { AxiosError } from 'axios';
import { ErrorResponse } from 'types/api';
import { ErrorStatusCode } from 'types/common';
export const ErrorResponseHandler = (error: AxiosError): ErrorResponse => {
if (error.response) {
// client received an error response (5xx, 4xx)
// making the error status code as standard Error Status Code
const statusCode = error.response.status as ErrorStatusCode;
if (statusCode >= 400 && statusCode < 500) {
const { data } = error.response;
if (statusCode === 404) {
return {
statusCode,
payload: null,
error: 'Not Found',
message: null,
};
}
return {
statusCode,
payload: null,
error: data.error,
message: null,
};
}
return {
statusCode,
payload: null,
error: 'Something went wrong',
message: null,
};
}
if (error.request) {
// client never received a response, or request never left
console.error('client never received a response, or request never left');
return {
statusCode: 500,
payload: null,
error: 'Something went wrong',
message: null,
};
}
// anything else
console.error('any');
return {
statusCode: 500,
payload: null,
error: error.toString(),
message: null,
};
};

View File

@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/create';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/dashboards', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default create;

View File

@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Props } from 'types/api/dashboard/delete';
const deleteDashboard = async (
props: Props,
): Promise<SuccessResponse<undefined> | ErrorResponse> => {
try {
const response = await axios.delete(`/dashboards/${props.uuid}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default deleteDashboard;

View File

@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/get';
const get = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`/dashboards/${props.uuid}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default get;

View File

@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/dashboard/getAll';
const getAll = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get('/dashboards');
return {
statusCode: 200,
error: null,
message: response.data.message,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getAll;

View File

@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const update = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/dashboards/${props.uuid}`, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default update;

View File

@ -1,10 +1,10 @@
import axios, { AxiosRequestConfig } from 'axios'; import axios from 'axios';
import { ENVIRONMENT } from 'constants/env'; import { ENVIRONMENT } from 'constants/env';
import apiV1 from './apiV1'; import apiV1 from './apiV1';
export default axios.create({ export default axios.create({
baseURL: `${ENVIRONMENT.baseURL}`, baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
}); });
export { apiV1 }; export { apiV1 };

View File

@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/widgets/getQuery';
const getQuery = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/query_range?query=${props.query}&start=${props.start}&end=${props.end}&step=${props.step}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getQuery;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
import React from 'react';
const Value = (): JSX.Element => (
<svg
width="78"
height="32"
viewBox="0 0 78 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.0215 17.875C14.2285 18.8184 13.2783 19.5771 12.1709 20.1514C11.0771 20.7256 9.87402 21.0127 8.56152 21.0127C6.83887 21.0127 5.33496 20.5889 4.0498 19.7412C2.77832 18.8936 1.79395 17.7041 1.09668 16.1729C0.399414 14.6279 0.0507812 12.9258 0.0507812 11.0664C0.0507812 9.07031 0.426758 7.27246 1.17871 5.67285C1.94434 4.07324 3.02441 2.84961 4.41895 2.00195C5.81348 1.1543 7.44043 0.730469 9.2998 0.730469C12.2529 0.730469 14.5771 1.83789 16.2725 4.05273C17.9814 6.25391 18.8359 9.26172 18.8359 13.0762V14.1836C18.8359 19.9941 17.6875 24.2393 15.3906 26.9189C13.0938 29.585 9.62793 30.9521 4.99316 31.0205H4.25488V27.8213H5.05469C8.18555 27.7666 10.5918 26.9531 12.2734 25.3809C13.9551 23.7949 14.8711 21.293 15.0215 17.875ZM9.17676 17.875C10.4482 17.875 11.6172 17.4854 12.6836 16.7061C13.7637 15.9268 14.5498 14.9629 15.042 13.8145V12.2969C15.042 9.80859 14.502 7.78516 13.4219 6.22656C12.3418 4.66797 10.9746 3.88867 9.32031 3.88867C7.65234 3.88867 6.3125 4.53125 5.30078 5.81641C4.28906 7.08789 3.7832 8.76953 3.7832 10.8613C3.7832 12.8984 4.26855 14.5801 5.23926 15.9062C6.22363 17.2188 7.53613 17.875 9.17676 17.875ZM24.5371 29.0107C24.5371 28.3545 24.7285 27.8076 25.1113 27.3701C25.5078 26.9326 26.0957 26.7139 26.875 26.7139C27.6543 26.7139 28.2422 26.9326 28.6387 27.3701C29.0488 27.8076 29.2539 28.3545 29.2539 29.0107C29.2539 29.6396 29.0488 30.166 28.6387 30.5898C28.2422 31.0137 27.6543 31.2256 26.875 31.2256C26.0957 31.2256 25.5078 31.0137 25.1113 30.5898C24.7285 30.166 24.5371 29.6396 24.5371 29.0107ZM51.1562 20.9717H55.2988V24.0684H51.1562V31H47.3418V24.0684H33.7451V21.833L47.1162 1.14062H51.1562V20.9717ZM38.0518 20.9717H47.3418V6.3291L46.8906 7.14941L38.0518 20.9717ZM73.6123 1.12012V4.33984H72.915C69.9619 4.39453 67.6104 5.26953 65.8604 6.96484C64.1104 8.66016 63.0986 11.0459 62.8252 14.1221C64.3975 12.3174 66.5439 11.415 69.2646 11.415C71.8623 11.415 73.9336 12.3311 75.4785 14.1631C77.0371 15.9951 77.8164 18.3604 77.8164 21.2588C77.8164 24.335 76.9756 26.7959 75.2939 28.6416C73.626 30.4873 71.3838 31.4102 68.5674 31.4102C65.71 31.4102 63.3926 30.3164 61.6152 28.1289C59.8379 25.9277 58.9492 23.0977 58.9492 19.6387V18.1826C58.9492 12.6865 60.1182 8.48926 62.4561 5.59082C64.8076 2.67871 68.3008 1.18848 72.9355 1.12012H73.6123ZM68.6289 14.5732C67.3301 14.5732 66.1338 14.9629 65.04 15.7422C63.9463 16.5215 63.1875 17.499 62.7637 18.6748V20.0693C62.7637 22.5303 63.3174 24.5127 64.4248 26.0166C65.5322 27.5205 66.9131 28.2725 68.5674 28.2725C70.2764 28.2725 71.6162 27.6436 72.5869 26.3857C73.5713 25.1279 74.0635 23.4805 74.0635 21.4434C74.0635 19.3926 73.5645 17.7383 72.5664 16.4805C71.582 15.209 70.2695 14.5732 68.6289 14.5732Z"
fill="white"
/>
</svg>
);
export default Value;

View File

@ -1,4 +1,4 @@
@import "~antd/dist/antd.dark.css"; @import '~antd/dist/antd.dark.css';
.ant-space-item { .ant-space-item {
margin-right: 0 !important; margin-right: 0 !important;
@ -10,3 +10,11 @@
#chart { #chart {
width: 100%; width: 100%;
} }
.ant-tabs-tab {
margin: 0 0 0 32px !important;
}
.ant-tabs-nav-list > .ant-tabs-tab:first-child {
margin: 0 !important;
}

View File

@ -0,0 +1,24 @@
import { Typography } from 'antd';
import React from 'react';
import { ColorContainer, Container } from './styles';
const Legend = ({ text, color }: LegendProps): JSX.Element => {
if (text.length === 0) {
return <></>;
}
return (
<Container>
<ColorContainer color={color}></ColorContainer>
<Typography>{text}</Typography>
</Container>
);
};
interface LegendProps {
text: string;
color: string;
}
export default Legend;

View File

@ -0,0 +1,20 @@
import styled from 'styled-components';
export const Container = styled.div`
margin-left: 2rem;
margin-right: 2rem;
display: flex;
cursor: pointer;
`;
interface Props {
color: string;
}
export const ColorContainer = styled.div<Props>`
background-color: ${({ color }): string => color};
border-radius: 50%;
width: 20px;
height: 20px;
margin-right: 0.5rem;
`;

View File

@ -0,0 +1,235 @@
import {
BarController,
BarElement,
CategoryScale,
Chart,
ChartOptions,
ChartType,
Decimation,
Filler,
Legend,
// LegendItem,
LinearScale,
LineController,
LineElement,
PointElement,
ScaleOptions,
SubTitle,
TimeScale,
TimeSeriesScale,
Title,
Tooltip,
} from 'chart.js';
import chartjsAdapter from 'chartjs-adapter-date-fns';
// import { colors } from 'lib/getRandomColor';
// import stringToHTML from 'lib/stringToHTML';
import React, { useCallback, useEffect, useRef } from 'react';
import { useThemeSwitcher } from 'react-css-theme-switcher';
// import Legends from './Legend';
// import { LegendsContainer } from './styles';
const Graph = ({
data,
type,
title,
isStacked,
label,
xAxisType,
onClickHandler,
}: GraphProps): JSX.Element => {
const chartRef = useRef<HTMLCanvasElement>(null);
const { currentTheme } = useThemeSwitcher();
// const [tooltipVisible, setTooltipVisible] = useState<boolean>(false);
const lineChartRef = useRef<Chart>();
const getGridColor = useCallback(() => {
if (currentTheme === undefined) {
return 'rgba(231,233,237,0.1)';
}
if (currentTheme === 'dark') {
return 'rgba(231,233,237,0.1)';
}
return 'rgba(231,233,237,0.8)';
}, [currentTheme]);
const buildChart = useCallback(() => {
if (lineChartRef.current !== undefined) {
lineChartRef.current.destroy();
}
if (chartRef.current !== null) {
Chart.register(
LineElement,
PointElement,
LineController,
CategoryScale,
LinearScale,
TimeScale,
TimeSeriesScale,
Decimation,
Filler,
Legend,
Title,
Tooltip,
SubTitle,
BarController,
BarElement,
);
const options: ChartOptions = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
title: {
display: title === undefined ? false : true,
text: title,
},
legend: {
// just making sure that label is present
display: !(
data.datasets.find((e) => e.label !== undefined) === undefined
),
labels: {
usePointStyle: true,
pointStyle: 'circle',
},
position: 'bottom',
// labels: {
// generateLabels: (chart: Chart): LegendItem[] => {
// return (data.datasets || []).map((e, index) => {
// return {
// text: e.label || '',
// datasetIndex: index,
// };
// });
// },
// pointStyle: 'circle',
// usePointStyle: true,
// },
},
},
layout: {
padding: 0,
},
scales: {
x: {
animate: false,
grid: {
display: true,
color: getGridColor(),
},
labels: label,
adapters: {
date: chartjsAdapter,
},
type: xAxisType,
},
y: {
display: true,
grid: {
display: true,
color: getGridColor(),
},
},
stacked: {
display: isStacked === undefined ? false : 'auto',
},
},
elements: {
line: {
tension: 0,
cubicInterpolationMode: 'monotone',
},
},
onClick: onClickHandler,
};
lineChartRef.current = new Chart(chartRef.current, {
type: type,
data: data,
options,
// plugins: [
// {
// id: 'htmlLegendPlugin',
// afterUpdate: (chart: Chart): void => {
// if (
// chart &&
// chart.options &&
// chart.options.plugins &&
// chart.options.plugins.legend &&
// chart.options.plugins.legend.labels &&
// chart.options.plugins.legend.labels.generateLabels
// ) {
// const labels = chart.options.plugins?.legend?.labels?.generateLabels(
// chart,
// );
// const id = 'htmlLegend';
// const response = document.getElementById(id);
// if (labels && response && response?.childNodes.length === 0) {
// const labelComponent = labels.map((e, index) => {
// return {
// element: Legends({
// text: e.text,
// color: colors[index] || 'white',
// }),
// dataIndex: e.datasetIndex,
// };
// });
// labelComponent.map((e) => {
// const el = stringToHTML(e.element);
// if (el) {
// el.addEventListener('click', () => {
// chart.setDatasetVisibility(
// e.dataIndex,
// !chart.isDatasetVisible(e.dataIndex),
// );
// chart.update();
// });
// response.append(el);
// }
// });
// }
// }
// },
// },
// ],
});
}
}, [chartRef, data, type, title, isStacked, label, xAxisType, getGridColor]);
useEffect(() => {
buildChart();
}, [buildChart]);
return (
<>
<canvas ref={chartRef} />
{/* <LegendsContainer id="htmlLegend" /> */}
</>
);
};
interface GraphProps {
type: ChartType;
data: Chart['data'];
title?: string;
isStacked?: boolean;
label?: string[];
xAxisType?: ScaleOptions['type'];
onClickHandler?: ChartOptions['onClick'];
}
export default Graph;

View File

@ -0,0 +1,8 @@
import styled from 'styled-components';
export const LegendsContainer = styled.div`
display: flex;
overflow-y: scroll;
margin-right: 1rem;
margin-bottom: 1rem;
`;

View File

@ -0,0 +1,48 @@
import { Form, Input, InputProps } from 'antd';
import React from 'react';
const InputComponent = ({
value,
type = 'text',
onChangeHandler,
placeholder,
ref,
size = 'small',
onBlurHandler,
onPressEnterHandler,
label,
labelOnTop,
addonBefore,
...props
}: InputComponentProps): JSX.Element => (
<Form.Item labelCol={{ span: labelOnTop ? 24 : 4 }} label={label}>
<Input
placeholder={placeholder}
type={type}
onChange={onChangeHandler}
value={value}
ref={ref}
size={size}
addonBefore={addonBefore}
onBlur={onBlurHandler}
onPressEnter={onPressEnterHandler}
{...props}
/>
</Form.Item>
);
interface InputComponentProps extends InputProps {
value: InputProps['value'];
type?: InputProps['type'];
onChangeHandler?: React.ChangeEventHandler<HTMLInputElement>;
placeholder?: InputProps['placeholder'];
ref?: React.LegacyRef<Input>;
size?: InputProps['size'];
onBlurHandler?: React.FocusEventHandler<HTMLInputElement>;
onPressEnterHandler?: React.KeyboardEventHandler<HTMLInputElement>;
label?: string;
labelOnTop?: boolean;
addonBefore?: React.ReactNode;
}
export default InputComponent;

View File

@ -1,4 +1,4 @@
import { ComponentType,lazy } from 'react'; import { ComponentType, lazy } from 'react';
function Loadable(importPath: { function Loadable(importPath: {
(): LoadableProps; (): LoadableProps;

View File

@ -1,21 +1,13 @@
import { Modal } from 'antd'; import { Modal, ModalProps as Props } from 'antd';
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
export const CustomModal = ({ const CustomModal = ({
title, title,
children, children,
isModalVisible, isModalVisible,
setIsModalVisible,
footer, footer,
closable = true, closable = true,
}: { }: ModalProps): JSX.Element => {
isModalVisible: boolean;
closable?: boolean;
setIsModalVisible: Function;
footer?: any;
title: string;
children: ReactElement;
}) => {
return ( return (
<> <>
<Modal <Modal
@ -29,3 +21,13 @@ export const CustomModal = ({
</> </>
); );
}; };
interface ModalProps {
isModalVisible: boolean;
closable?: boolean;
footer?: Props['footer'];
title: string;
children: ReactElement;
}
export default CustomModal;

View File

@ -2,7 +2,7 @@ import NotFoundImage from 'assets/NotFound';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import React from 'react'; import React from 'react';
import { Button, Container,Text, TextContainer } from './styles'; import { Button, Container, Text, TextContainer } from './styles';
const NotFound = (): JSX.Element => { const NotFound = (): JSX.Element => {
return ( return (

View File

@ -1,13 +1,15 @@
import React, { useState, useCallback } from "react"; import { Layout, Menu, Switch as ToggleButton, Typography } from 'antd';
import { Layout, Menu, Switch as ToggleButton } from "antd"; import ROUTES from 'constants/routes';
import { NavLink } from "react-router-dom"; import React, { useCallback, useState } from 'react';
import { useThemeSwitcher } from "react-css-theme-switcher"; import { useThemeSwitcher } from 'react-css-theme-switcher';
import { useLocation } from "react-router-dom"; import { NavLink } from 'react-router-dom';
import ROUTES from "constants/routes"; import { useLocation } from 'react-router-dom';
import { ThemeSwitcherWrapper, Logo } from "./styles"; import { Logo, ThemeSwitcherWrapper } from './styles';
const { Sider } = Layout; const { Sider } = Layout;
import menus from "./menuItems"; import history from 'lib/history';
import menus from './menuItems';
const SideNav = (): JSX.Element => { const SideNav = (): JSX.Element => {
const { switcher, currentTheme, themes } = useThemeSwitcher(); const { switcher, currentTheme, themes } = useThemeSwitcher();
@ -15,14 +17,21 @@ const SideNav = (): JSX.Element => {
const [collapsed, setCollapsed] = useState<boolean>(false); const [collapsed, setCollapsed] = useState<boolean>(false);
const { pathname } = useLocation(); const { pathname } = useLocation();
const toggleTheme = useCallback((isChecked: boolean) => { const toggleTheme = useCallback(
switcher({ theme: isChecked ? themes.dark : themes.light }); (isChecked: boolean) => {
}, []); switcher({ theme: isChecked ? themes.dark : themes.light });
},
[switcher, themes],
);
const onCollapse = useCallback(() => { const onCollapse = useCallback(() => {
setCollapsed((collapsed) => !collapsed); setCollapsed((collapsed) => !collapsed);
}, []); }, []);
const onClickHandler = useCallback((to: string) => {
history.push(to);
}, []);
return ( return (
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={160}> <Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={160}>
<ThemeSwitcherWrapper> <ThemeSwitcherWrapper>
@ -32,7 +41,7 @@ const SideNav = (): JSX.Element => {
/> />
</ThemeSwitcherWrapper> </ThemeSwitcherWrapper>
<NavLink to="/"> <NavLink to="/">
<Logo src={"/signoz.svg"} alt="SigNoz" collapsed={collapsed} /> <Logo src={'/signoz.svg'} alt="SigNoz" collapsed={collapsed} />
</NavLink> </NavLink>
<Menu <Menu
@ -43,9 +52,9 @@ const SideNav = (): JSX.Element => {
> >
{menus.map(({ to, Icon, name }) => ( {menus.map(({ to, Icon, name }) => (
<Menu.Item key={to} icon={<Icon />}> <Menu.Item key={to} icon={<Icon />}>
<NavLink to={to} style={{ fontSize: 12, textDecoration: "none" }}> <div onClick={(): void => onClickHandler(to)}>
{name} <Typography>{name}</Typography>
</NavLink> </div>
</Menu.Item> </Menu.Item>
))} ))}
</Menu> </Menu>

View File

@ -1,43 +1,49 @@
import { import {
BarChartOutlined,
AlignLeftOutlined, AlignLeftOutlined,
ApiOutlined,
BarChartOutlined,
DashboardFilled,
DeploymentUnitOutlined, DeploymentUnitOutlined,
LineChartOutlined, LineChartOutlined,
SettingOutlined, SettingOutlined,
ApiOutlined, } from '@ant-design/icons';
} from "@ant-design/icons"; import ROUTES from 'constants/routes';
import ROUTES from "constants/routes";
const menus: SidebarMenu[] = [ const menus: SidebarMenu[] = [
{ {
Icon: BarChartOutlined, Icon: BarChartOutlined,
to: ROUTES.APPLICATION, to: ROUTES.APPLICATION,
name: "Metrics", name: 'Metrics',
}, },
{ {
Icon: AlignLeftOutlined, Icon: AlignLeftOutlined,
to: ROUTES.TRACES, to: ROUTES.TRACES,
name: "Traces", name: 'Traces',
}, },
{ {
to: ROUTES.SERVICE_MAP, to: ROUTES.SERVICE_MAP,
name: "Service Map", name: 'Service Map',
Icon: DeploymentUnitOutlined, Icon: DeploymentUnitOutlined,
}, },
{ {
Icon: LineChartOutlined, Icon: LineChartOutlined,
to: ROUTES.USAGE_EXPLORER, to: ROUTES.USAGE_EXPLORER,
name: "Usage Explorer", name: 'Usage Explorer',
}, },
{ {
Icon: SettingOutlined, Icon: SettingOutlined,
to: ROUTES.SETTINGS, to: ROUTES.SETTINGS,
name: "Settings", name: 'Settings',
}, },
{ {
Icon: ApiOutlined, Icon: ApiOutlined,
to: ROUTES.INSTRUMENTATION, to: ROUTES.INSTRUMENTATION,
name: "Add instrumentation", name: 'Add instrumentation',
},
{
Icon: DashboardFilled,
to: ROUTES.ALL_DASHBOARD,
name: 'Dashboard',
}, },
]; ];

View File

@ -1,4 +1,4 @@
import styled from "styled-components"; import styled from 'styled-components';
export const ThemeSwitcherWrapper = styled.div` export const ThemeSwitcherWrapper = styled.div`
display: flex; display: flex;
@ -10,7 +10,7 @@ export const ThemeSwitcherWrapper = styled.div`
export const Logo = styled.img<LogoProps>` export const Logo = styled.img<LogoProps>`
width: 100px; width: 100px;
margin: 5%; margin: 5%;
display: ${({ collapsed }) => (!collapsed ? "block" : "none")}; display: ${({ collapsed }): string => (!collapsed ? 'block' : 'none')};
`; `;
interface LogoProps { interface LogoProps {

View File

@ -0,0 +1,52 @@
import { Button, Dropdown, Menu, Typography } from 'antd';
import timeItems, {
timePreferance,
timePreferenceType,
} from 'container/NewWidget/RightContainer/timeItems';
import React, { useCallback } from 'react';
import { TextContainer } from './styles';
const TimePreference = ({
setSelectedTime,
selectedTime,
}: TimePreferenceDropDownProps): JSX.Element => {
const timeMenuItemOnChangeHandler = useCallback(
(event: TimeMenuItemOnChangeHandlerEvent) => {
const selectedTime = timeItems.find((e) => e.enum === event.key);
if (selectedTime !== undefined) {
setSelectedTime(selectedTime);
}
},
[setSelectedTime],
);
return (
<TextContainer noButtonMargin>
<Dropdown
overlay={
<Menu>
{timeItems.map((item) => (
<Menu.Item onClick={timeMenuItemOnChangeHandler} key={item.enum}>
<Typography>{item.name}</Typography>
</Menu.Item>
))}
</Menu>
}
>
<Button>{selectedTime.name}</Button>
</Dropdown>
</TextContainer>
);
};
interface TimeMenuItemOnChangeHandlerEvent {
key: timePreferenceType | string;
}
interface TimePreferenceDropDownProps {
setSelectedTime: React.Dispatch<React.SetStateAction<timePreferance>>;
selectedTime: timePreferance;
}
export default TimePreference;

View File

@ -0,0 +1,14 @@
import styled from 'styled-components';
interface TextContainerProps {
noButtonMargin?: boolean;
}
export const TextContainer = styled.div<TextContainerProps>`
display: flex;
> button {
margin-left: ${({ noButtonMargin }): string => {
return noButtonMargin ? '0' : '0.5rem';
}}
`;

View File

@ -0,0 +1,13 @@
import React from 'react';
import { Value } from './styles';
const ValueGraph = ({ value }: ValueGraphProps): JSX.Element => (
<Value>{value}</Value>
);
interface ValueGraphProps {
value: string;
}
export default ValueGraph;

View File

@ -0,0 +1,6 @@
import { Typography } from 'antd';
import styled from 'styled-components';
export const Value = styled(Typography)`
font-size: 3rem;
`;

View File

@ -5,5 +5,5 @@ export enum METRICS_PAGE_QUERY_PARAM {
service = 'service', service = 'service',
error = 'error', error = 'error',
operation = 'operation', operation = 'operation',
kind = 'kind' kind = 'kind',
} }

View File

@ -8,6 +8,9 @@ const ROUTES = {
INSTRUMENTATION: '/add-instrumentation', INSTRUMENTATION: '/add-instrumentation',
USAGE_EXPLORER: '/usage-explorer', USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/application', APPLICATION: '/application',
ALL_DASHBOARD: '/dashboard',
DASHBOARD: '/dashboard/:dashboardId',
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
}; };
export default ROUTES; export default ROUTES;

View File

@ -0,0 +1,71 @@
import { Typography } from 'antd';
import { ChartData } from 'chart.js';
import Graph from 'components/Graph';
import ValueGraph from 'components/ValueGraph';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import history from 'lib/history';
import React from 'react';
import { TitleContainer, ValueContainer } from './styles';
const GridGraphComponent = ({
GRAPH_TYPES,
data,
title,
opacity,
isStacked,
}: GridGraphComponentProps): JSX.Element | null => {
const location = history.location.pathname;
const isDashboardPage = location.split('/').length === 3;
if (GRAPH_TYPES === 'TIME_SERIES') {
return (
<Graph
{...{
data,
title,
type: 'line',
isStacked,
opacity,
xAxisType: 'time',
}}
/>
);
}
if (GRAPH_TYPES === 'VALUE') {
const value = (((data.datasets[0] || []).data || [])[0] || 0) as number;
if (data.datasets.length === 0) {
return (
<ValueContainer isDashboardPage={isDashboardPage}>
<Typography>No Data</Typography>
</ValueContainer>
);
}
return (
<>
<TitleContainer isDashboardPage={isDashboardPage}>
<Typography>{title}</Typography>
</TitleContainer>
<ValueContainer isDashboardPage={isDashboardPage}>
<ValueGraph value={value.toString()} />
</ValueContainer>
</>
);
}
return null;
};
export interface GridGraphComponentProps {
GRAPH_TYPES: GRAPH_TYPES;
data: ChartData;
title?: string;
opacity?: string;
isStacked?: boolean;
}
export default GridGraphComponent;

View File

@ -0,0 +1,19 @@
import styled from 'styled-components';
interface Props {
isDashboardPage: boolean;
}
export const ValueContainer = styled.div<Props>`
height: ${({ isDashboardPage }): string =>
isDashboardPage ? '100%' : '55vh'};
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;
export const TitleContainer = styled.div<Props>`
text-align: center;
padding-top: ${({ isDashboardPage }): string =>
!isDashboardPage ? '1rem' : '0rem'};
`;

View File

@ -0,0 +1,55 @@
import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import React, { useCallback } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
ToggleAddWidget,
ToggleAddWidgetProps,
} from 'store/actions/dashboard/toggleAddWidget';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import DashboardReducer from 'types/reducer/dashboards';
import { Button, Container } from './styles';
const AddWidget = ({ toggleAddWidget }: Props): JSX.Element => {
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const onToggleHandler = useCallback(() => {
toggleAddWidget(true);
}, [toggleAddWidget]);
return (
<Container>
{!isAddWidget ? (
<>
<Button onClick={onToggleHandler} icon={<PlusOutlined />}>
Add Widgets
</Button>
</>
) : (
<Typography>Click a widget icon to add it here</Typography>
)}
</Container>
);
};
interface DispatchProps {
toggleAddWidget: (
props: ToggleAddWidgetProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch),
});
type Props = DispatchProps;
export default connect(null, mapDispatchToProps)(AddWidget);

View File

@ -0,0 +1,18 @@
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
export const Button = styled(ButtonComponent)`
&&& {
display: flex;
justify-content: center;
align-items: center;
border: none;
}
`;
export const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`;

View File

@ -0,0 +1,40 @@
import {
DeleteOutlined,
EditFilled,
FullscreenOutlined,
} from '@ant-design/icons';
import React, { useCallback } from 'react';
import { useHistory, useLocation } from 'react-router';
import { Widgets } from 'types/api/dashboard/getAll';
import { Container } from './styles';
const Bar = ({
widget,
onViewFullScreenHandler,
onDeleteHandler,
}: BarProps): JSX.Element => {
const { push } = useHistory();
const { pathname } = useLocation();
const onEditHandler = useCallback(() => {
const widgetId = widget.id;
push(`${pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`);
}, [push, pathname, widget]);
return (
<Container>
<FullscreenOutlined onClick={onViewFullScreenHandler} />
<EditFilled onClick={onEditHandler} />
<DeleteOutlined onClick={onDeleteHandler} />
</Container>
);
};
interface BarProps {
widget: Widgets;
onViewFullScreenHandler: () => void;
onDeleteHandler: () => void;
}
export default Bar;

View File

@ -0,0 +1,15 @@
import styled from 'styled-components';
export const Container = styled.div`
height: 15%;
align-items: center;
justify-content: flex-end;
display: flex;
gap: 1rem;
padding-right: 1rem;
padding-left: 1rem;
padding-top: 0.5rem;
position: absolute;
top: 0;
right: 0;
`;

View File

@ -0,0 +1,181 @@
import { Button, Typography } from 'antd';
import getQueryResult from 'api/widgets/getQuery';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import GridGraphComponent from 'container/GridGraphComponent';
import {
timeItems,
timePreferance,
} from 'container/NewWidget/RightContainer/timeItems';
import getChartData from 'lib/getChartData';
import GetMaxMinTime from 'lib/getMaxMinTime';
import getStartAndEndTime from 'lib/getStartAndEndTime';
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { GlobalTime } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GraphContainer, NotFoundContainer, TimeContainer } from './styles';
const FullView = ({ widget }: FullViewProps): JSX.Element => {
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
(state) => state.globalTime,
);
const [state, setState] = useState<FullViewState>({
error: false,
errorMessage: '',
loading: true,
payload: undefined,
});
const getSelectedTime = useCallback(
() =>
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
[widget],
);
const [selectedTime, setSelectedTime] = useState<timePreferance>({
name: getSelectedTime()?.name || '',
enum: widget?.timePreferance || 'GLOBAL_TIME',
});
const onFetchDataHandler = useCallback(async () => {
try {
const maxMinTime = GetMaxMinTime({
graphType: widget.panelTypes,
maxTime,
minTime,
});
const { end, start } = getStartAndEndTime({
type: selectedTime.enum,
maxTime: maxMinTime.maxTime,
minTime: maxMinTime.minTime,
});
const response = await Promise.all(
widget.query
.filter((e) => e.query.length !== 0)
.map(async (query) => {
const result = await getQueryResult({
end,
query: query.query,
start: start,
step: '30',
});
return {
query: query.query,
queryData: result,
legend: query.legend,
};
}),
);
const isError = response.find((e) => e.queryData.statusCode !== 200);
if (isError !== undefined) {
setState((state) => ({
...state,
error: true,
errorMessage: isError.queryData.error || 'Something went wrong',
loading: false,
}));
} else {
const chartDataSet = getChartData({
queryData: {
data: response.map((e) => ({
query: e.query,
legend: e.legend,
queryData: e.queryData.payload?.result || [],
})),
error: false,
errorMessage: '',
loading: false,
},
});
setState((state) => ({
...state,
loading: false,
payload: chartDataSet,
}));
}
} catch (error) {
setState((state) => ({
...state,
error: true,
errorMessage: (error as AxiosError).toString(),
loading: false,
}));
}
}, [widget, maxTime, minTime, selectedTime.enum]);
useEffect(() => {
onFetchDataHandler();
}, [onFetchDataHandler]);
if (state.loading || state.payload === undefined) {
return <Spinner height="80vh" size="large" tip="Loading..." />;
}
if (state.loading === false && state.payload.datasets.length === 0) {
return (
<>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
<NotFoundContainer>
<Typography>No Data</Typography>
</NotFoundContainer>
</>
);
}
return (
<>
<TimeContainer>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
<Button onClick={onFetchDataHandler} type="primary">
Refresh
</Button>
</TimeContainer>
<GraphContainer>
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: widget.title,
}}
/>
</GraphContainer>
</>
);
};
interface FullViewState {
loading: boolean;
error: boolean;
errorMessage: string;
payload: ChartData | undefined;
}
interface FullViewProps {
widget: Widgets;
}
export default FullView;

View File

@ -0,0 +1,21 @@
import styled from 'styled-components';
export const GraphContainer = styled.div`
min-height: 70vh;
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
`;
export const NotFoundContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 55vh;
`;
export const TimeContainer = styled.div`
display: flex;
justify-content: flex-end;
`;

View File

@ -0,0 +1,209 @@
import { Typography } from 'antd';
import getQueryResult from 'api/widgets/getQuery';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import getChartData from 'lib/getChartData';
import GetMaxMinTime from 'lib/getMaxMinTime';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTime } from 'store/actions';
import {
DeleteWidget,
DeleteWidgetProps,
} from 'store/actions/dashboard/deleteWidget';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import Bar from './Bar';
import FullView from './FullView';
import { Modal } from './styles';
const GridCardGraph = ({
widget,
deleteWidget,
isDeleted,
}: GridCardGraphProps): JSX.Element => {
const [state, setState] = useState<GridCardGraphState>({
loading: true,
errorMessage: '',
error: false,
payload: undefined,
});
const [modal, setModal] = useState(false);
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
(state) => state.globalTime,
);
const [deleteModal, setDeletModal] = useState(false);
useEffect(() => {
(async (): Promise<void> => {
try {
const getMaxMinTime = GetMaxMinTime({
graphType: widget.panelTypes,
maxTime,
minTime,
});
const { start, end } = GetStartAndEndTime({
type: widget.timePreferance,
maxTime: getMaxMinTime.maxTime,
minTime: getMaxMinTime.minTime,
});
const response = await Promise.all(
widget.query
.filter((e) => e.query.length !== 0)
.map(async (query) => {
const result = await getQueryResult({
end,
query: query.query,
start: start,
step: '30',
});
return {
query: query.query,
queryData: result,
legend: query.legend,
};
}),
);
const isError = response.find((e) => e.queryData.statusCode !== 200);
if (isError !== undefined) {
setState((state) => ({
...state,
error: true,
errorMessage: isError.queryData.error || 'Something went wrong',
loading: false,
}));
} else {
const chartDataSet = getChartData({
queryData: {
data: response.map((e) => ({
query: e.query,
legend: e.legend,
queryData: e.queryData.payload?.result || [],
})),
error: false,
errorMessage: '',
loading: false,
},
});
setState((state) => ({
...state,
loading: false,
payload: chartDataSet,
}));
}
} catch (error) {
setState((state) => ({
...state,
error: true,
errorMessage: (error as AxiosError).toString(),
loading: false,
}));
}
})();
}, [widget, maxTime, minTime]);
const onToggleModal = useCallback(
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
func((value) => !value);
},
[],
);
const onDeleteHandler = useCallback(() => {
deleteWidget({ widgetId: widget.id });
onToggleModal(setDeletModal);
isDeleted.current = true;
}, [deleteWidget, widget, onToggleModal, isDeleted]);
if (state.error) {
return <div>{state.errorMessage}</div>;
}
if (state.loading === true || state.payload === undefined) {
return <Spinner height="20vh" tip="Loading..." />;
}
return (
<>
<Bar
onViewFullScreenHandler={(): void => onToggleModal(setModal)}
widget={widget}
onDeleteHandler={(): void => onToggleModal(setDeletModal)}
/>
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeletModal)}
visible={deleteModal}
title="Delete"
height="10vh"
onOk={onDeleteHandler}
centered
>
<Typography>Are you sure you want to delete this widget</Typography>
</Modal>
<Modal
title="View"
footer={[]}
centered
visible={modal}
onCancel={(): void => onToggleModal(setModal)}
width="85%"
destroyOnClose
>
<FullView widget={widget} />
</Modal>
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: widget.title,
}}
/>
</>
);
};
interface GridCardGraphState {
loading: boolean;
error: boolean;
errorMessage: string;
payload: ChartData | undefined;
}
interface DispatchProps {
deleteWidget: ({
widgetId,
}: DeleteWidgetProps) => (dispatch: Dispatch<AppActions>) => void;
}
interface GridCardGraphProps extends DispatchProps {
widget: Widgets;
isDeleted: React.MutableRefObject<boolean>;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
});
export default connect(null, mapDispatchToProps)(GridCardGraph);

View File

@ -0,0 +1,13 @@
import { Modal as ModalComponent } from 'antd';
import styled from 'styled-components';
interface Props {
height?: string;
}
export const Modal = styled(ModalComponent)<Props>`
.ant-modal-content,
.ant-modal-body {
min-height: ${({ height = '80vh' }): string => height};
}
`;

View File

@ -0,0 +1,130 @@
import Spinner from 'components/Spinner';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';
import { v4 } from 'uuid';
import AddWidget from './AddWidget';
import Graph from './Graph';
import { Card, CardContainer, ReactGridLayout } from './styles';
const GridGraph = (): JSX.Element => {
const { push } = useHistory();
const { pathname } = useLocation();
const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
const { widgets } = data;
const [layouts, setLayout] = useState<LayoutProps[]>([]);
const AddWidgetWrapper = useCallback(() => <AddWidget />, []);
const isMounted = useRef(true);
const isDeleted = useRef(false);
useEffect(() => {
if (
loading === false &&
(isMounted.current === true || isDeleted.current === true)
) {
const getPreLayouts = (): LayoutProps[] => {
if (widgets === undefined) {
return [];
}
return widgets.map((e, index) => {
return {
h: 2,
w: 6,
y: Infinity,
i: (index + 1).toString(),
x: (index % 2) * 6,
// eslint-disable-next-line react/display-name
Component: (): JSX.Element => (
<Graph isDeleted={isDeleted} widget={widgets[index]} />
),
};
});
};
const preLayouts = getPreLayouts();
setLayout(() => [
...preLayouts,
{
i: (preLayouts.length + 1).toString(),
x: (preLayouts.length % 2) * 6,
y: Infinity,
w: 6,
h: 2,
Component: AddWidgetWrapper,
},
]);
}
return (): void => {
isMounted.current = false;
};
}, [widgets, layouts.length, AddWidgetWrapper, loading]);
const onDropHandler = useCallback(
(allLayouts: Layout[], currectLayout: Layout, event: DragEvent) => {
event.preventDefault();
if (event.dataTransfer) {
const graphType = event.dataTransfer.getData('text') as GRAPH_TYPES;
const generateWidgetId = v4();
push(`${pathname}/new?graphType=${graphType}&widgetId=${generateWidgetId}`);
}
},
[pathname, push],
);
if (layouts.length === 0) {
return <Spinner height="40vh" size="large" tip="Loading..." />;
}
return (
<ReactGridLayout
isResizable
isDraggable
cols={12}
rowHeight={100}
autoSize
width={100}
isDroppable
useCSSTransforms
onDrop={onDropHandler}
>
{layouts.map(({ Component, ...rest }, index) => {
const widget = (widgets || [])[index] || {};
const type = widget.panelTypes;
const isQueryType = type === 'VALUE';
return (
<CardContainer key={rest.i} data-grid={rest}>
<Card isQueryType={isQueryType}>
<Component />
</Card>
</CardContainer>
);
})}
</ReactGridLayout>
);
};
interface LayoutProps extends Layout {
Component: () => JSX.Element;
}
export default memo(GridGraph);

View File

@ -0,0 +1,42 @@
import { Card as CardComponent } from 'antd';
import RGL, { WidthProvider } from 'react-grid-layout';
import styled from 'styled-components';
const ReactGridLayoutComponent = WidthProvider(RGL);
interface Props {
isQueryType: boolean;
}
export const Card = styled(CardComponent)<Props>`
&&& {
height: 100%;
}
.ant-card-body {
height: 100%;
padding: 0;
}
`;
export const CardContainer = styled.div`
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=');
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: se-resize;
}
`;
export const ReactGridLayout = styled(ReactGridLayoutComponent)`
border: 1px solid #434343;
margin-top: 1rem;
position: relative;
`;

View File

@ -0,0 +1,17 @@
import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import React from 'react';
import { Data } from '..';
const Created = (createdBy: Data['createdBy']): JSX.Element => {
const time = new Date(createdBy);
return (
<Typography>{`${time.toLocaleDateString()} ${convertDateToAmAndPm(
time,
)}`}</Typography>
);
};
export default Created;

View File

@ -0,0 +1,14 @@
import { Typography } from 'antd';
import React from 'react';
import { Data } from '..';
const DateComponent = (
lastUpdatedTime: Data['lastUpdatedTime'],
): JSX.Element => {
const date = new Date(lastUpdatedTime).toDateString();
return <Typography>{date}</Typography>;
};
export default DateComponent;

View File

@ -0,0 +1,56 @@
import { Button } from 'antd';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { DeleteDashboard, DeleteDashboardProps } from 'store/actions';
import AppActions from 'types/actions';
import { Data } from '../index';
const DeleteButton = ({
deleteDashboard,
id,
}: DeleteButtonProps): JSX.Element => {
const onClickHandler = useCallback(() => {
deleteDashboard({
uuid: id,
});
}, [id, deleteDashboard]);
return (
<Button onClick={onClickHandler} type="link">
Delete
</Button>
);
};
interface DispatchProps {
deleteDashboard: ({
uuid,
}: DeleteDashboardProps) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
deleteDashboard: bindActionCreators(DeleteDashboard, dispatch),
});
type DeleteButtonProps = Data & DispatchProps;
const WrapperDeleteButton = connect(null, mapDispatchToProps)(DeleteButton);
// This is to avoid the type collision
const Wrapper = (props: Data): JSX.Element => {
return (
<WrapperDeleteButton
{...{
...props,
}}
/>
);
};
export default Wrapper;

View File

@ -0,0 +1,23 @@
import { Button } from 'antd';
import ROUTES from 'constants/routes';
import updateUrl from 'lib/updateUrl';
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { Data } from '..';
const Name = (name: Data['name'], data: Data): JSX.Element => {
const { push } = useHistory();
const onClickHandler = useCallback(() => {
push(updateUrl(ROUTES.DASHBOARD, ':dashboardId', data.id));
}, []);
return (
<Button onClick={onClickHandler} type="link">
{name}
</Button>
);
};
export default Name;

View File

@ -0,0 +1,16 @@
import { Tag } from 'antd';
import React from 'react';
import { Data } from '../index';
const Tags = (props: Data['tags']): JSX.Element => {
return (
<>
{props.map((e) => (
<Tag key={e}>{e}</Tag>
))}
</>
);
};
export default Tags;

View File

@ -0,0 +1,171 @@
import { PlusOutlined } from '@ant-design/icons';
import { Row, Table, TableColumnProps, Typography } from 'antd';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import ROUTES from 'constants/routes';
import updateUrl from 'lib/updateUrl';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';
import { v4 } from 'uuid';
import { NewDashboardButton, TableContainer } from './styles';
import Createdby from './TableComponents/CreatedBy';
import DateComponent from './TableComponents/Date';
import DeleteButton from './TableComponents/DeleteButton';
import Name from './TableComponents/Name';
import Tags from './TableComponents/Tags';
const ListOfAllDashboard = (): JSX.Element => {
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [newDashboardState, setNewDashboardState] = useState({
loading: false,
error: false,
errorMessage: '',
});
const { push } = useHistory();
const columns: TableColumnProps<Data>[] = [
{
title: 'Name',
dataIndex: 'name',
render: Name,
},
{
title: 'Description',
dataIndex: 'description',
},
{
title: 'Tags (can be multiple)',
dataIndex: 'tags',
render: Tags,
},
{
title: 'Created By',
dataIndex: 'createdBy',
render: Createdby,
},
{
title: 'Last Updated Time',
dataIndex: 'lastUpdatedTime',
sorter: (a: Data, b: Data): number => {
return parseInt(a.lastUpdatedTime, 10) - parseInt(b.lastUpdatedTime, 10);
},
render: DateComponent,
},
{
title: 'Action',
dataIndex: '',
key: 'x',
render: DeleteButton,
},
];
const data: Data[] = dashboards.map((e) => ({
createdBy: e.created_at,
description: e.data.description || '',
id: e.uuid,
lastUpdatedTime: e.updated_at,
name: e.data.title,
tags: e.data.tags || [],
key: e.uuid,
}));
const onNewDashboardHandler = useCallback(async () => {
try {
const newDashboardId = v4();
setNewDashboardState({
...newDashboardState,
loading: true,
});
const response = await createDashboard({
uuid: newDashboardId,
title: 'Sample Title',
});
if (response.statusCode === 200) {
setNewDashboardState({
...newDashboardState,
loading: false,
});
push(updateUrl(ROUTES.DASHBOARD, ':dashboardId', newDashboardId));
} else {
setNewDashboardState({
...newDashboardState,
loading: false,
error: true,
errorMessage: response.error || 'Something went wrong',
});
}
} catch (error) {
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, push]);
const getText = (): string => {
if (!newDashboardState.error && !newDashboardState.loading) {
return 'New Dashboard';
}
if (newDashboardState.loading) {
return 'Loading';
}
return newDashboardState.errorMessage;
};
return (
<TableContainer>
<Table
pagination={{
pageSize: 9,
defaultPageSize: 9,
}}
showHeader
bordered
sticky
title={(): JSX.Element => {
return (
<Row justify="space-between">
<Typography>Dashboard List</Typography>
<NewDashboardButton
onClick={onNewDashboardHandler}
icon={<PlusOutlined />}
type="primary"
loading={newDashboardState.loading}
danger={newDashboardState.error}
>
{getText()}
</NewDashboardButton>
</Row>
);
}}
columns={columns}
dataSource={data}
showSorterTooltip
/>
</TableContainer>
);
};
export interface Data {
key: React.Key;
name: string;
description: string;
tags: string[];
createdBy: string;
lastUpdatedTime: string;
id: string;
}
export default ListOfAllDashboard;

View File

@ -0,0 +1,16 @@
import { Button, Row } from 'antd';
import styled from 'styled-components';
export const NewDashboardButton = styled(Button)`
&&& {
display: flex;
justify-content: center;
align-items: center;
}
`;
export const TableContainer = styled(Row)`
&&& {
margin-top: 1rem;
}
`;

View File

@ -0,0 +1,46 @@
import React, { useCallback } from 'react';
import { useHistory, useLocation } from 'react-router';
import { v4 } from 'uuid';
import menuItems, { ITEMS } from './menuItems';
import { Card, Container, Text } from './styles';
const DashboardGraphSlider = (): JSX.Element => {
const onDragStartHandler: React.DragEventHandler<HTMLDivElement> = useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
event.dataTransfer.setData('text/plain', event.currentTarget.id);
},
[],
);
const { push } = useHistory();
const { pathname } = useLocation();
const onClickHandler = useCallback(
(name: ITEMS) => {
const generateWidgetId = v4();
push(`${pathname}/new?graphType=${name}&widgetId=${generateWidgetId}`);
},
[push, pathname],
);
return (
<Container>
{menuItems.map(({ name, Icon, display }) => (
<Card
onClick={(): void => onClickHandler(name)}
id={name}
onDragStart={onDragStartHandler}
key={name}
draggable
>
<Icon />
<Text>{display}</Text>
</Card>
))}
</Container>
);
};
export type GRAPH_TYPES = ITEMS;
export default DashboardGraphSlider;

View File

@ -0,0 +1,25 @@
import TimeSeries from 'assets/Dashboard/TimeSeries';
import ValueIcon from 'assets/Dashboard/Value';
const Items: ItemsProps[] = [
{
name: 'TIME_SERIES',
Icon: TimeSeries,
display: 'Time Series',
},
{
name: 'VALUE',
Icon: ValueIcon,
display: 'Value',
},
];
export type ITEMS = 'TIME_SERIES' | 'VALUE';
interface ItemsProps {
name: ITEMS;
Icon: () => JSX.Element;
display: string;
}
export default Items;

View File

@ -0,0 +1,25 @@
import { Card as CardComponent, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
gap: 0.6rem;
`;
export const Card = styled(CardComponent)`
min-height: 10vh;
overflow-y: auto;
cursor: pointer;
.ant-card-body {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
`;
export const Text = styled(Typography)`
text-align: center;
margin-top: 1rem;
`;

View File

@ -0,0 +1,120 @@
import { PlusOutlined } from '@ant-design/icons';
import { Col, Tooltip, Typography } from 'antd';
import Input from 'components/Input';
import React, { useState } from 'react';
import { InputContainer, NewTagContainer, TagsContainer } from './styles';
const AddTags = ({ tags, setTags }: AddTagsProps): JSX.Element => {
const [inputValue, setInputValue] = useState<string>('');
const [inputVisible, setInputVisible] = useState<boolean>(false);
const [editInputIndex, setEditInputIndex] = useState(-1);
const [editInputValue, setEditInputValue] = useState('');
const handleInputConfirm = (): void => {
if (inputValue) {
setTags([...tags, inputValue]);
}
setInputVisible(false);
setInputValue('');
};
const handleEditInputConfirm = (): void => {
const newTags = [...tags];
newTags[editInputIndex] = editInputValue;
setTags(newTags);
setEditInputIndex(-1);
setInputValue('');
};
const handleClose = (removedTag: string): void => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
};
const showInput = (): void => {
setInputVisible(true);
};
const onChangeHandler = (
value: string,
func: React.Dispatch<React.SetStateAction<string>>,
): void => {
func(value);
};
return (
<TagsContainer>
{tags.map((tag, index) => {
if (editInputIndex === index) {
return (
<Col lg={4}>
<Input
key={tag}
size="small"
value={editInputValue}
onChangeHandler={(event): void =>
onChangeHandler(event.target.value, setEditInputValue)
}
onBlurHandler={handleEditInputConfirm}
onPressEnterHandler={handleEditInputConfirm}
/>
</Col>
);
}
const isLongTag = tag.length > 20;
const tagElem = (
<NewTagContainer closable key={tag} onClose={(): void => handleClose(tag)}>
<span
onDoubleClick={(e): void => {
setEditInputIndex(index);
setEditInputValue(tag);
e.preventDefault();
}}
>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</NewTagContainer>
);
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
{inputVisible && (
<InputContainer lg={4}>
<Input
type="text"
size="small"
value={inputValue}
onChangeHandler={(event): void =>
onChangeHandler(event.target.value, setInputValue)
}
onBlurHandler={handleInputConfirm}
onPressEnterHandler={handleInputConfirm}
/>
</InputContainer>
)}
{!inputVisible && (
<NewTagContainer icon={<PlusOutlined />} onClick={showInput}>
<Typography>New Tag</Typography>
</NewTagContainer>
)}
</TagsContainer>
);
};
interface AddTagsProps {
tags: string[];
setTags: React.Dispatch<React.SetStateAction<string[]>>;
}
export default AddTags;

View File

@ -0,0 +1,26 @@
import { Col, Tag } from 'antd';
import styled from 'styled-components';
export const TagsContainer = styled.div`
display: flex;
align-items: center;
`;
export const NewTagContainer = styled(Tag)`
&&& {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
svg {
margin-right: 0.2rem;
}
}
`;
export const InputContainer = styled(Col)`
> div {
margin: 0;
}
`;

View File

@ -0,0 +1,34 @@
import { Input } from 'antd';
import React, { useCallback } from 'react';
import { Container } from './styles';
const { TextArea } = Input;
const Description = ({
description,
setDescription,
}: DescriptionProps): JSX.Element => {
const onChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setDescription(e.target.value);
},
[setDescription],
);
return (
<Container>
<TextArea
placeholder={'Description of the dashboard'}
onChange={onChangeHandler}
value={description}
></TextArea>
</Container>
);
};
interface DescriptionProps {
description: string;
setDescription: React.Dispatch<React.SetStateAction<string>>;
}
export default Description;

View File

@ -0,0 +1,5 @@
import styled from 'styled-components';
export const Container = styled.div`
margin-top: 1rem;
`;

View File

@ -0,0 +1,30 @@
import Input from 'components/Input';
import React, { useCallback } from 'react';
const NameOfTheDashboard = ({
setName,
name,
}: NameOfTheDashboardProps): JSX.Element => {
const onChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
},
[setName],
);
return (
<Input
size="middle"
placeholder="Title"
value={name}
onChangeHandler={onChangeHandler}
/>
);
};
interface NameOfTheDashboardProps {
name: string;
setName: React.Dispatch<React.SetStateAction<string>>;
}
export default NameOfTheDashboard;

View File

@ -0,0 +1,122 @@
import { EditOutlined, SaveOutlined } from '@ant-design/icons';
import { Card, Col, Row, Tag, Typography } from 'antd';
import AddTags from 'container/NewDashboard/DescriptionOfDashboard/AddTags';
import NameOfTheDashboard from 'container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard';
import React, { useCallback, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
ToggleEditMode,
UpdateDashboardTitleDescriptionTags,
UpdateDashboardTitleDescriptionTagsProps,
} from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import DashboardReducer from 'types/reducer/dashboards';
import Description from './Description';
import { Button, Container } from './styles';
const DescriptionOfDashboard = ({
updateDashboardTitleDescriptionTags,
toggleEditMode,
}: DescriptionOfDashboardProps): JSX.Element => {
const { dashboards, isEditMode } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const selectedData = selectedDashboard.data;
const title = selectedData.title;
const tags = selectedData.tags;
const description = selectedData.description;
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []);
const [updatedDescription, setUpdtatedDescription] = useState(
description || '',
);
const onClickEditHandler = useCallback(() => {
if (isEditMode) {
const dashboard = selectedDashboard;
// @TODO need to update this function to take title,description,tags only
updateDashboardTitleDescriptionTags({
dashboard: {
...dashboard,
data: {
...dashboard.data,
description: updatedDescription,
tags: updatedTags,
title: updatedTitle,
},
},
});
} else {
toggleEditMode();
}
}, [isEditMode, updatedTitle, updatedTags, updatedDescription]);
return (
<>
<Card>
<Row align="top" justify="space-between">
{!isEditMode ? (
<>
<Col>
<Typography>{title}</Typography>
<Container>
{tags?.map((e) => (
<Tag key={e}>{e}</Tag>
))}
</Container>
<Container>
<Typography>{description}</Typography>
</Container>
</Col>
</>
) : (
<Col lg={8}>
<NameOfTheDashboard name={updatedTitle} setName={setUpdatedTitle} />
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
<Description
description={updatedDescription}
setDescription={setUpdtatedDescription}
/>
</Col>
)}
<Col>
<Button
icon={!isEditMode ? <EditOutlined /> : <SaveOutlined />}
onClick={onClickEditHandler}
>
{isEditMode ? 'Save' : 'Edit'}
</Button>
</Col>
</Row>
</Card>
</>
);
};
interface DispatchProps {
updateDashboardTitleDescriptionTags: (
props: UpdateDashboardTitleDescriptionTagsProps,
) => (dispatch: Dispatch<AppActions>) => void;
toggleEditMode: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardTitleDescriptionTags: bindActionCreators(
UpdateDashboardTitleDescriptionTags,
dispatch,
),
toggleEditMode: bindActionCreators(ToggleEditMode, dispatch),
});
type DescriptionOfDashboardProps = DispatchProps;
export default connect(null, mapDispatchToProps)(DescriptionOfDashboard);

View File

@ -0,0 +1,13 @@
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
margin-top: 0.5rem;
`;
export const Button = styled(ButtonComponent)`
&&& {
display: flex;
align-items: center;
}
`;

View File

@ -0,0 +1,26 @@
import GridGraphLayout from 'container/GridGraphLayout';
import ComponentsSlider from 'container/NewDashboard/ComponentsSlider';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';
import { GridComponentSliderContainer } from './styles';
const GridGraphs = (): JSX.Element => {
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
return (
<>
<GridComponentSliderContainer>
{isAddWidget && <ComponentsSlider />}
<GridGraphLayout />
</GridComponentSliderContainer>
</>
);
};
export default GridGraphs;

View File

@ -0,0 +1,28 @@
import { Card as CardComponent, Row } from 'antd';
import styled from 'styled-components';
export const CardContainer = styled(Row)`
&&& {
margin-top: 1rem;
}
`;
export const Card = styled(CardComponent)`
&&& {
cursor: pointer;
}
.ant-card-body {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
> span {
margin-right: 0.5rem;
}
}
`;
export const GridComponentSliderContainer = styled.div`
margin-top: 1rem;
`;

View File

@ -0,0 +1,15 @@
import React from 'react';
import Description from './DescriptionOfDashboard';
import GridGraphs from './GridGraphs';
const NewDashboard = (): JSX.Element => {
return (
<div>
<Description />
<GridGraphs />
</div>
);
};
export default NewDashboard;

View File

@ -0,0 +1,95 @@
import { Divider } from 'antd';
import Input from 'components/Input';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import React, { useCallback, useState } from 'react';
import { connect } from 'react-redux';
import { useLocation } from 'react-router';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
UpdateQuery,
UpdateQueryProps,
} from 'store/actions/dashboard/updateQuery';
import AppActions from 'types/actions';
import { Container, InputContainer } from './styles';
const Query = ({
currentIndex,
preLegend,
preQuery,
updateQuery,
}: QueryProps): JSX.Element => {
const [promqlQuery, setPromqlQuery] = useState(preQuery);
const [legendFormat, setLegendFormat] = useState(preLegend);
const { search } = useLocation();
const query = new URLSearchParams(search);
const widgetId = query.get('widgetId') || '';
const onChangeHandler = useCallback(
(setFunc: React.Dispatch<React.SetStateAction<string>>, value: string) => {
setFunc(value);
},
[],
);
const onBlurHandler = (): void => {
updateQuery({
currentIndex,
legend: legendFormat,
query: promqlQuery,
widgetId,
});
};
return (
<Container>
<InputContainer>
<Input
onChangeHandler={(event): void =>
onChangeHandler(setPromqlQuery, event.target.value)
}
size="middle"
value={promqlQuery}
addonBefore={'PromQL Query'}
onBlur={(): void => onBlurHandler()}
/>
</InputContainer>
<InputContainer>
<Input
onChangeHandler={(event): void =>
onChangeHandler(setLegendFormat, event.target.value)
}
size="middle"
value={legendFormat}
addonBefore={'Legend Format'}
onBlur={(): void => onBlurHandler()}
/>
</InputContainer>
<Divider />
</Container>
);
};
interface DispatchProps {
updateQuery: (
props: UpdateQueryProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateQuery: bindActionCreators(UpdateQuery, dispatch),
});
interface QueryProps extends DispatchProps {
selectedTime: timePreferance;
currentIndex: number;
preQuery: string;
preLegend: string;
}
export default connect(null, mapDispatchToProps)(Query);

View File

@ -0,0 +1,89 @@
import { PlusOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import React, { useCallback, useMemo } from 'react';
import { connect, useSelector } from 'react-redux';
import { useLocation } from 'react-router';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { CreateQuery, CreateQueryProps } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import DashboardReducer from 'types/reducer/dashboards';
import Query from './Query';
import { QueryButton } from './styles';
const QuerySection = ({
selectedTime,
createQuery,
}: QueryProps): JSX.Element => {
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboards] = dashboards;
const { search } = useLocation();
const widgets = selectedDashboards.data.widgets;
const urlQuery = useMemo(() => {
return new URLSearchParams(search);
}, [search]);
const getWidget = useCallback(() => {
const widgetId = urlQuery.get('widgetId');
return widgets?.find((e) => e.id === widgetId);
}, [widgets, urlQuery]);
const selectedWidget = getWidget() as Widgets;
const { query = [] } = selectedWidget || {};
const queryOnClickHandler = useCallback(() => {
const widgetId = urlQuery.get('widgetId');
createQuery({
widgetId: String(widgetId),
});
}, [createQuery, urlQuery]);
if (query.length === 0) {
return <Spinner size="small" height="30vh" tip="Loading..." />;
}
return (
<div>
{query.map((e, index) => (
<Query
currentIndex={index}
selectedTime={selectedTime}
key={e.query + index}
preQuery={e.query}
preLegend={e.legend || ''}
/>
))}
<QueryButton onClick={queryOnClickHandler} icon={<PlusOutlined />}>
Query
</QueryButton>
</div>
);
};
interface DispatchProps {
createQuery: ({
widgetId,
}: CreateQueryProps) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
createQuery: bindActionCreators(CreateQuery, dispatch),
});
interface QueryProps extends DispatchProps {
selectedTime: timePreferance;
}
export default connect(null, mapDispatchToProps)(QuerySection);

View File

@ -0,0 +1,17 @@
import { Button } from 'antd';
import styled from 'styled-components';
export const InputContainer = styled.div`
width: 50%;
`;
export const Container = styled.div`
margin-top: 1rem;
`;
export const QueryButton = styled(Button)`
&&& {
display: flex;
align-items: center;
}
`;

View File

@ -0,0 +1,59 @@
import { Card, Typography } from 'antd';
import GridGraphComponent from 'container/GridGraphComponent';
import { NewWidgetProps } from 'container/NewWidget';
import getChartData from 'lib/getChartData';
import React from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router';
import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';
import { NotFoundContainer } from './styles';
const WidgetGraph = ({ selectedGraph }: WidgetGraphProps): JSX.Element => {
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
const { widgets = [] } = data;
const { search } = useLocation();
const params = new URLSearchParams(search);
const widgetId = params.get('widgetId');
const selectedWidget = widgets.find((e) => e.id === widgetId);
if (selectedWidget === undefined) {
return <Card>Invalid widget</Card>;
}
const { queryData, title, opacity, isStacked } = selectedWidget;
if (queryData.data.length === 0) {
return (
<NotFoundContainer>
<Typography>No Data</Typography>
</NotFoundContainer>
);
}
const chartDataSet = getChartData({
queryData,
});
return (
<GridGraphComponent
title={title}
isStacked={isStacked}
opacity={opacity}
data={chartDataSet}
GRAPH_TYPES={selectedGraph}
/>
);
};
type WidgetGraphProps = NewWidgetProps;
export default WidgetGraph;

View File

@ -0,0 +1,57 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { Card } from 'container/GridGraphLayout/styles';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';
import { NewWidgetProps } from '../../index';
import { AlertIconContainer, Container, NotFoundContainer } from './styles';
import WidgetGraphComponent from './WidgetGraph';
const WidgetGraph = ({ selectedGraph }: WidgetGraphProps): JSX.Element => {
const { dashboards, isQueryFired } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const { search } = useLocation();
const { data } = selectedDashboard;
const { widgets = [] } = data;
const params = new URLSearchParams(search);
const widgetId = params.get('widgetId');
const selectedWidget = widgets.find((e) => e.id === widgetId);
if (selectedWidget === undefined) {
return <Card isQueryType={false}>Invalid widget</Card>;
}
const { queryData } = selectedWidget;
return (
<Container>
{queryData.error && (
<AlertIconContainer color="red" title={queryData.errorMessage}>
<InfoCircleOutlined />
</AlertIconContainer>
)}
{!isQueryFired && (
<NotFoundContainer>
<Typography>No Data</Typography>
</NotFoundContainer>
)}
{isQueryFired && <WidgetGraphComponent selectedGraph={selectedGraph} />}
</Container>
);
};
type WidgetGraphProps = NewWidgetProps;
export default memo(WidgetGraph);

View File

@ -0,0 +1,27 @@
import { Card, Tooltip } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`
&&& {
position: relative;
}
.ant-card-body {
padding: 0;
height: 55vh;
/* padding-bottom: 2rem; */
}
`;
export const AlertIconContainer = styled(Tooltip)`
position: absolute;
top: 10px;
left: 10px;
`;
export const NotFoundContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 55vh;
`;

View File

@ -0,0 +1,28 @@
import React, { memo } from 'react';
import { NewWidgetProps } from '../index';
import { timePreferance } from '../RightContainer/timeItems';
import QuerySection from './QuerySection';
import { QueryContainer } from './styles';
import WidgetGraph from './WidgetGraph';
const LeftContainer = ({
selectedGraph,
selectedTime,
}: LeftContainerProps): JSX.Element => {
return (
<>
<WidgetGraph selectedGraph={selectedGraph} />
<QueryContainer>
<QuerySection selectedTime={selectedTime} />
</QueryContainer>
</>
);
};
interface LeftContainerProps extends NewWidgetProps {
selectedTime: timePreferance;
}
export default memo(LeftContainer);

View File

@ -0,0 +1,9 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const QueryContainer = styled(Card)`
&&& {
margin-top: 1rem;
min-height: 23.5%;
}
`;

View File

@ -0,0 +1,157 @@
import { Button, Input, Slider, Switch, Typography } from 'antd';
import InputComponent from 'components/Input';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems';
import React, { useCallback } from 'react';
import { timePreferance } from './timeItems';
const { TextArea } = Input;
import TimePreference from 'components/TimePreferenceDropDown';
import { Container, NullButtonContainer, TextContainer, Title } from './styles';
const RightContainer = ({
description,
opacity,
selectedNullZeroValue,
setDescription,
setOpacity,
setSelectedNullZeroValue,
setStacked,
setTitle,
stacked,
title,
selectedGraph,
setSelectedTime,
selectedTime,
}: RightContainerProps): JSX.Element => {
const onChangeHandler = useCallback(
(setFunc: React.Dispatch<React.SetStateAction<string>>, value: string) => {
setFunc(value);
},
[],
);
const nullValueButtons = [
{
check: 'zero',
name: 'Zero',
},
{
check: 'interpolate',
name: 'Interpolate',
},
{
check: 'blank',
name: 'Blank',
},
];
const selectedGraphType =
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
return (
<Container>
<InputComponent
labelOnTop
label="Panel Type"
size="middle"
value={selectedGraphType}
disabled
/>
<Title>Panel Attributes</Title>
<InputComponent
label="Panel Title"
size="middle"
placeholder="Title"
labelOnTop
onChangeHandler={(event): void =>
onChangeHandler(setTitle, event.target.value)
}
value={title}
/>
<Title light={'true'}>Description</Title>
<TextArea
placeholder="Write something describing the panel"
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
/>
<TextContainer>
<Typography>Stacked Graphs :</Typography>
<Switch
checked={stacked}
onChange={(): void => {
setStacked((value) => !value);
}}
/>
</TextContainer>
<Title light={'true'}>Fill Opacity: </Title>
<Slider
value={parseInt(opacity, 10)}
marks={{
0: '0',
33: '33',
66: '66',
100: '100',
}}
onChange={(number): void => onChangeHandler(setOpacity, number.toString())}
step={1}
/>
<Title light={'true'}>Null/Zero values: </Title>
<NullButtonContainer>
{nullValueButtons.map((button) => (
<Button
type={button.check === selectedNullZeroValue ? 'primary' : 'default'}
key={button.name}
onClick={(): void =>
onChangeHandler(setSelectedNullZeroValue, button.check)
}
>
{button.name}
</Button>
))}
</NullButtonContainer>
<Title light={'true'}>Panel Time Preference</Title>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</Container>
);
};
interface RightContainerProps {
title: string;
setTitle: React.Dispatch<React.SetStateAction<string>>;
description: string;
setDescription: React.Dispatch<React.SetStateAction<string>>;
stacked: boolean;
setStacked: React.Dispatch<React.SetStateAction<boolean>>;
opacity: string;
setOpacity: React.Dispatch<React.SetStateAction<string>>;
selectedNullZeroValue: string;
setSelectedNullZeroValue: React.Dispatch<React.SetStateAction<string>>;
selectedGraph: GRAPH_TYPES;
setSelectedTime: React.Dispatch<React.SetStateAction<timePreferance>>;
selectedTime: timePreferance;
}
export default RightContainer;

View File

@ -0,0 +1,40 @@
import { Card, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`
height: 100%;
.ant-card-body {
height: 100%;
}
`;
interface TitleProps {
light?: string;
}
export const Title = styled(Typography)<TitleProps>`
&&& {
margin-top: 0.5rem;
margin-bottom: 1rem;
font-weight: ${({ light }): string => (light === 'true' ? 'none' : 'bold')};
}
`;
interface TextContainerProps {
noButtonMargin?: boolean;
}
export const TextContainer = styled.div<TextContainerProps>`
display: flex;
margin-top: 1rem;
margin-bottom: 1rem;
> button {
margin-left: ${({ noButtonMargin }): string => {
return noButtonMargin ? '0' : '0.5rem';
}}
`;
export const NullButtonContainer = styled.div`
margin-bottom: 1rem;
`;

View File

@ -0,0 +1,60 @@
export const timeItems: timePreferance[] = [
{
name: 'Global Time',
enum: 'GLOBAL_TIME',
},
{
name: 'Last 5 min',
enum: 'LAST_5_MIN',
},
{
name: 'Last 15 min',
enum: 'LAST_15_MIN',
},
{
name: 'Last 30 min',
enum: 'LAST_30_MIN',
},
{
name: 'Last 1 hr',
enum: 'LAST_1_HR',
},
{
name: 'Last 6 hr',
enum: 'LAST_6_HR',
},
{
name: 'Last 1 day',
enum: 'LAST_1_DAY',
},
{
name: 'Last 1 week',
enum: 'LAST_1_WEEK',
},
];
export interface timePreferance {
name: string;
enum: timePreferenceType;
}
export type timePreferenceType =
| GLOBAL_TIME
| LAST_5_MIN
| LAST_15_MIN
| LAST_30_MIN
| LAST_1_HR
| LAST_6_HR
| LAST_1_DAY
| LAST_1_WEEK;
type GLOBAL_TIME = 'GLOBAL_TIME';
type LAST_5_MIN = 'LAST_5_MIN';
type LAST_15_MIN = 'LAST_15_MIN';
type LAST_30_MIN = 'LAST_30_MIN';
type LAST_1_HR = 'LAST_1_HR';
type LAST_6_HR = 'LAST_6_HR';
type LAST_1_DAY = 'LAST_1_DAY';
type LAST_1_WEEK = 'LAST_1_WEEK';
export default timeItems;

View File

@ -0,0 +1,237 @@
import { Button } from 'antd';
import ROUTES from 'constants/routes';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import updateUrl from 'lib/updateUrl';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { useHistory, useLocation, useParams } from 'react-router';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
ApplySettingsToPanel,
ApplySettingsToPanelProps,
GlobalTime,
} from 'store/actions';
import {
GetQueryResults,
GetQueryResultsProps,
} from 'store/actions/dashboard/getQueryResults';
import {
SaveDashboard,
SaveDashboardProps,
} from 'store/actions/dashboard/saveDashboard';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import DashboardReducer from 'types/reducer/dashboards';
import LeftContainer from './LeftContainer';
import RightContainer from './RightContainer';
import timeItems, { timePreferance } from './RightContainer/timeItems';
import {
ButtonContainer,
Container,
LeftContainerWrapper,
PanelContainer,
RightContainerWrapper,
} from './styles';
const NewWidget = ({
selectedGraph,
applySettingsToPanel,
saveSettingOfPanel,
getQueryResults,
}: Props): JSX.Element => {
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const { maxTime, minTime } = useSelector<AppState, GlobalTime>(
(state) => state.globalTime,
);
const [selectedDashboard] = dashboards;
const widgets = selectedDashboard.data.widgets;
const { push } = useHistory();
const { search } = useLocation();
const query = useMemo(() => {
return new URLSearchParams(search);
}, [search]);
const { dashboardId } = useParams<DashboardWidgetPageParams>();
const getWidget = useCallback(() => {
const widgetId = query.get('widgetId');
return widgets?.find((e) => e.id === widgetId);
}, [query, widgets]);
const selectedWidget = getWidget();
const [title, setTitle] = useState<string>(selectedWidget?.title || '');
const [description, setDescription] = useState<string>(
selectedWidget?.description || '',
);
const [stacked, setStacked] = useState<boolean>(
selectedWidget?.isStacked || false,
);
const [opacity, setOpacity] = useState<string>(selectedWidget?.opacity || '1');
const [selectedNullZeroValue, setSelectedNullZeroValue] = useState<string>(
selectedWidget?.nullZeroValues || 'zero',
);
const getSelectedTime = useCallback(
() =>
timeItems.find(
(e) => e.enum === (selectedWidget?.timePreferance || 'GLOBAL_TIME'),
),
[selectedWidget],
);
const [selectedTime, setSelectedTime] = useState<timePreferance>({
name: getSelectedTime()?.name || '',
enum: selectedWidget?.timePreferance || 'GLOBAL_TIME',
});
const onClickSaveHandler = useCallback(() => {
// update the global state
saveSettingOfPanel({
uuid: selectedDashboard.uuid,
description,
isStacked: stacked,
nullZeroValues: selectedNullZeroValue,
opacity,
timePreferance: selectedTime.enum,
title,
widgetId: query.get('widgetId') || '',
dashboardId: dashboardId,
});
}, [
opacity,
description,
query,
selectedTime,
stacked,
title,
selectedNullZeroValue,
saveSettingOfPanel,
selectedDashboard,
dashboardId,
]);
const onClickApplyHandler = useCallback(() => {
applySettingsToPanel({
description,
isStacked: stacked,
nullZeroValues: selectedNullZeroValue,
opacity,
timePreferance: selectedTime.enum,
title,
widgetId: selectedWidget?.id || '',
});
}, [
applySettingsToPanel,
description,
opacity,
selectedTime,
selectedWidget?.id,
selectedNullZeroValue,
stacked,
title,
]);
const onClickDiscardHandler = useCallback(() => {
push(updateUrl(ROUTES.DASHBOARD, ':dashboardId', dashboardId));
}, [dashboardId, push]);
const getQueryResult = useCallback(() => {
if (selectedWidget?.id.length !== 0) {
getQueryResults({
maxTime,
minTime,
query: selectedWidget?.query || [],
selectedTime: selectedTime.enum,
widgetId: selectedWidget?.id || '',
graphType: selectedGraph,
});
}
}, [
selectedWidget?.query,
selectedTime.enum,
maxTime,
minTime,
selectedWidget?.id,
selectedGraph,
getQueryResults,
]);
useEffect(() => {
getQueryResult();
}, [getQueryResult]);
return (
<Container>
<ButtonContainer>
<Button onClick={onClickSaveHandler}>Save</Button>
<Button onClick={onClickApplyHandler}>Apply</Button>
<Button onClick={onClickDiscardHandler}>Discard</Button>
</ButtonContainer>
<PanelContainer>
<LeftContainerWrapper flex={5}>
<LeftContainer selectedTime={selectedTime} selectedGraph={selectedGraph} />
</LeftContainerWrapper>
<RightContainerWrapper flex={1}>
<RightContainer
{...{
title,
setTitle,
description,
setDescription,
stacked,
setStacked,
opacity,
setOpacity,
selectedNullZeroValue,
setSelectedNullZeroValue,
selectedGraph,
setSelectedTime,
selectedTime,
}}
/>
</RightContainerWrapper>
</PanelContainer>
</Container>
);
};
export interface NewWidgetProps {
selectedGraph: GRAPH_TYPES;
}
interface DispatchProps {
applySettingsToPanel: (
props: ApplySettingsToPanelProps,
) => (dispatch: Dispatch<AppActions>) => void;
saveSettingOfPanel: (
props: SaveDashboardProps,
) => (dispatch: Dispatch<AppActions>) => void;
getQueryResults: (
props: GetQueryResultsProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
applySettingsToPanel: bindActionCreators(ApplySettingsToPanel, dispatch),
saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch),
getQueryResults: bindActionCreators(GetQueryResults, dispatch),
});
type Props = DispatchProps & NewWidgetProps;
export default connect(null, mapDispatchToProps)(NewWidget);

View File

@ -0,0 +1,33 @@
import { Col } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
min-height: 78vh;
display: flex;
margin-top: 1rem;
flex-direction: column;
`;
export const RightContainerWrapper = styled(Col)`
&&& {
min-width: 200px;
}
`;
export const LeftContainerWrapper = styled(Col)`
&&& {
margin-right: 1rem;
max-width: 70%;
}
`;
export const ButtonContainer = styled.div`
display: flex;
gap: 1rem;
margin-bottom: 1rem;
justify-content: flex-end;
`;
export const PanelContainer = styled.div`
display: flex;
`;

View File

@ -0,0 +1,85 @@
import { useEffect, useRef, useState } from 'react';
import { ErrorResponse, SuccessResponse } from 'types/api';
function useFetch<PayloadProps, FunctionParams>(
functions: {
(props: FunctionParams): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(arg0: any): Promise<SuccessResponse<PayloadProps> | ErrorResponse>;
},
param?: FunctionParams,
): State<PayloadProps | undefined> {
const [state, setStates] = useState<State<PayloadProps | undefined>>({
loading: true,
success: null,
errorMessage: '',
error: null,
payload: undefined,
});
const loadingRef = useRef(0);
useEffect(() => {
let abortController = new window.AbortController();
const { signal } = abortController;
try {
(async (): Promise<void> => {
if (state.loading) {
const response = await functions(param);
if (!signal.aborted && loadingRef.current === 0) {
loadingRef.current = 1;
if (response.statusCode === 200) {
setStates({
loading: false,
error: false,
success: true,
payload: response.payload,
errorMessage: '',
});
} else {
setStates({
loading: false,
error: true,
success: false,
payload: undefined,
errorMessage: response.error as string,
});
}
}
}
})();
} catch (error) {
if (!signal.aborted) {
setStates({
payload: undefined,
loading: false,
success: false,
error: true,
errorMessage: error,
});
}
}
return (): void => {
abortController.abort();
abortController = new window.AbortController();
};
}, [functions, param, state.loading]);
return {
...state,
};
}
export interface State<T> {
loading: boolean | null;
error: boolean | null;
success: boolean | null;
payload: T;
errorMessage: string;
}
export default useFetch;

View File

@ -0,0 +1,18 @@
import { useCallback, useEffect, useRef } from 'react';
function useMountedState(): () => boolean {
const mountedRef = useRef<boolean>(false);
const get = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return (): void => {
mountedRef.current = false;
};
}, []);
return get;
}
export default useMountedState;

View File

@ -0,0 +1,9 @@
const convertDateToAmAndPm = (date: Date): string => {
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
});
};
export default convertDateToAmAndPm;

View File

@ -1,4 +1,4 @@
const convertToNanoSecondsToSecond = (number: number) => { const convertToNanoSecondsToSecond = (number: number): string => {
return parseFloat((number / 1000000).toString()).toFixed(2); return parseFloat((number / 1000000).toString()).toFixed(2);
}; };

View File

@ -0,0 +1,5 @@
const convertIntoEpoc = (number: number): string => {
return number.toString().split('.').join('').toString();
};
export default convertIntoEpoc;

View File

@ -0,0 +1,71 @@
import { ChartData } from 'chart.js';
import getLabelName from 'lib/getLabelName';
import { Widgets } from 'types/api/dashboard/getAll';
import convertIntoEpoc from './covertIntoEpoc';
import { colors } from './getRandomColor';
const getChartData = ({ queryData }: GetChartDataProps): ChartData => {
const response = queryData.data.map(({ query, queryData, legend }) => {
return queryData.map((e, index) => {
const { values = [], metric } = e || {};
const labelNames = getLabelName(
metric,
query, //query
legend || '', // legends
);
const dataValue = values?.map((e) => {
const [first = 0, second = ''] = e || [];
return {
first: new Date(parseInt(convertIntoEpoc(first), 10)),
second: Number(parseFloat(second).toFixed(2)),
};
});
const color = colors[index] || 'red';
return {
label: labelNames,
first: dataValue.map((e) => e.first),
borderColor: color,
second: dataValue.map((e) => e.second),
};
});
});
const allLabels = response
.map((e) => e.map((e) => e.label))
.reduce((a, b) => [...a, ...b], []);
const allColor = response
.map((e) => e.map((e) => e.borderColor))
.reduce((a, b) => [...a, ...b], []);
const alldata = response
.map((e) => e.map((e) => e.second))
.reduce((a, b) => [...a, ...b], []);
return {
datasets: alldata.map((e, index) => {
return {
data: e,
label: allLabels[index],
borderWidth: 1.5,
spanGaps: true,
animations: false,
borderColor: allColor[index],
showLine: true,
pointRadius: 0,
};
}),
labels: response
.map((e) => e.map((e) => e.first))
.reduce((a, b) => [...a, ...b], [])[0],
};
};
interface GetChartDataProps {
queryData: Widgets['queryData'];
}
export default getChartData;

View File

@ -0,0 +1,52 @@
import { QueryData } from 'types/api/widgets/getQuery';
const getLabelName = (
metric: QueryData['metric'],
query: string,
legends: string,
): string => {
if (metric === undefined) {
return '';
}
const keysArray = Object.keys(metric);
if (legends.length !== 0) {
const variables = legends
.split('{{')
.filter((e) => e)
.map((e) => e.split('}}')[0]);
const results = variables.map((variable) => metric[variable]);
let endResult = legends;
variables.forEach((e, index) => {
endResult = endResult.replace(`{{${e}}}`, results[index]);
});
return endResult;
}
const index = keysArray.findIndex((e) => e === '__name__');
const preArray = keysArray.slice(0, index);
const postArray = keysArray.slice(index + 1, keysArray.length);
if (index === undefined && preArray.length === 0 && postArray.length) {
return query;
}
const post = postArray.map((e) => `${e}="${metric[e]}"`).join(',');
const pre = preArray.map((e) => `${e}="${metric[e]}"`).join(',');
const result = `${metric[keysArray[index]]}`;
if (post.length === 0 && pre.length === 0) {
return result;
}
return `${result}{${pre}${post}}`;
};
export default getLabelName;

View File

@ -0,0 +1,27 @@
import { GlobalTime } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
const GetMaxMinTime = ({
graphType,
minTime,
maxTime,
}: GetMaxMinProps): GlobalTime => {
if (graphType === 'VALUE') {
return {
maxTime: maxTime,
minTime: maxTime,
};
}
return {
maxTime: maxTime,
minTime: minTime,
};
};
interface GetMaxMinProps {
graphType: Widgets['panelTypes'];
maxTime: GlobalTime['maxTime'];
minTime: GlobalTime['minTime'];
}
export default GetMaxMinTime;

View File

@ -0,0 +1,21 @@
export const colors = [
'#F2994A',
'#56CCF2',
'#F2C94C',
'#219653',
'#2F80ED',
'#EB5757',
'#BB6BD9',
'#BDBDBD',
];
export function getRandomNumber(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
const getRandomColor = (): string => {
const index = parseInt(getRandomNumber(0, colors.length - 1).toString(), 10);
return colors[index];
};
export default getRandomColor;

View File

@ -0,0 +1,9 @@
const getMicroSeconds = ({ time }: getMicroSecondsProps): string => {
return (time / 1000).toString();
};
interface getMicroSecondsProps {
time: number;
}
export default getMicroSeconds;

View File

@ -0,0 +1,13 @@
const getMinAgo = ({ minutes }: getMinAgoProps): Date => {
const currentDate = new Date();
const agoDate = new Date(currentDate.getTime() - minutes * 60000);
return agoDate;
};
interface getMinAgoProps {
minutes: number;
}
export default getMinAgo;

View File

@ -0,0 +1,101 @@
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import getMicroSeconds from './getMicroSeconds';
import getMinAgo from './getMinAgo';
const GetStartAndEndTime = ({
type,
minTime,
maxTime,
}: GetStartAndEndTimeProps): Payload => {
const end = new Date().getTime();
const endString = getMicroSeconds({ time: end });
if (type === 'LAST_5_MIN') {
const agodate = getMinAgo({ minutes: 5 }).getTime();
const agoString = getMicroSeconds({ time: agodate });
return {
start: agoString,
end: endString,
};
}
if (type === 'LAST_30_MIN') {
const agodate = getMinAgo({ minutes: 30 }).getTime();
const agoString = getMicroSeconds({ time: agodate });
return {
start: agoString,
end: endString,
};
}
if (type === 'LAST_1_HR') {
const agodate = getMinAgo({ minutes: 60 }).getTime();
const agoString = getMicroSeconds({ time: agodate });
return {
start: agoString,
end: endString,
};
}
if (type === 'LAST_15_MIN') {
const agodate = getMinAgo({ minutes: 15 }).getTime();
const agoString = getMicroSeconds({ time: agodate });
return {
start: agoString,
end: endString,
};
}
if (type === 'LAST_6_HR') {
const agoDate = getMinAgo({ minutes: 6 * 60 }).getTime();
const agoString = getMicroSeconds({ time: agoDate });
return {
start: agoString,
end: endString,
};
}
if (type === 'LAST_1_DAY') {
const agoDate = getMinAgo({ minutes: 24 * 60 }).getTime();
const agoString = getMicroSeconds({ time: agoDate });
return {
start: agoString,
end: endString,
};
}
if (type === 'LAST_1_WEEK') {
const agoDate = getMinAgo({ minutes: 24 * 60 * 7 }).getTime();
const agoString = getMicroSeconds({ time: agoDate });
return {
start: agoString,
end: endString,
};
}
return {
start: getMicroSeconds({ time: minTime / 1000000 }),
end: getMicroSeconds({ time: maxTime / 1000000 }),
};
};
interface GetStartAndEndTimeProps {
type: timePreferenceType;
minTime: number;
maxTime: number;
}
interface Payload {
start: string;
end: string;
}
export default GetStartAndEndTime;

View File

@ -1,2 +1,3 @@
import { createBrowserHistory } from 'history'; import { createBrowserHistory } from 'history';
export default createBrowserHistory(); export default createBrowserHistory();

View File

@ -0,0 +1,9 @@
import { renderToString } from 'react-dom/server';
const stringToHTML = function (str: JSX.Element): HTMLElement {
const parser = new DOMParser();
const doc = parser.parseFromString(renderToString(str), 'text/html');
return doc.body.firstChild as HTMLElement;
};
export default stringToHTML;

View File

@ -0,0 +1,9 @@
const updateUrl = (
routes: string,
variables: string,
value: string,
): string => {
return routes.replace(variables, value);
};
export default updateUrl;

View File

@ -1,8 +1,8 @@
import { Layout } from 'antd'; import { Layout } from 'antd';
import SideNav from 'components/SideNav';
import React, { ReactNode, useEffect } from 'react'; import React, { ReactNode, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import SideNav from 'components/SideNav';
import TopNav from './Nav/TopNav'; import TopNav from './Nav/TopNav';
import { useRoute } from './RouteProvider'; import { useRoute } from './RouteProvider';
@ -19,7 +19,7 @@ const BaseLayout: React.FC<BaseLayoutProps> = ({ children }) => {
useEffect(() => { useEffect(() => {
dispatch({ type: 'ROUTE_IS_LOADED', payload: location.pathname }); dispatch({ type: 'ROUTE_IS_LOADED', payload: location.pathname });
}, [location]); }, [location, dispatch]);
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>

Some files were not shown because too many files have changed in this diff Show More