mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-12 14:05:16 +03:00
platform(nx): initial oss migration
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5429 GitOrigin-RevId: 3df08906d9c3cd6a9f75b933469bce4782c4a8d5
This commit is contained in:
parent
dea80bfac7
commit
38ffe84ce3
1
frontend/.nvmrc
Normal file
1
frontend/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v16.15.1
|
69
frontend/apps/console-ce/custom-webpack.config.js
Normal file
69
frontend/apps/console-ce/custom-webpack.config.js
Normal file
@ -0,0 +1,69 @@
|
||||
const { merge } = require('webpack-merge');
|
||||
const util = require('util');
|
||||
|
||||
const log = (value) =>
|
||||
console.log(
|
||||
util.inspect(value, { showHidden: false, depth: null, colors: true })
|
||||
);
|
||||
module.exports = (config, context) => {
|
||||
const output = merge(config, {
|
||||
output: {
|
||||
publicPath: '',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
/*
|
||||
Rule taken from the old codebase
|
||||
=> Do we still need the version naming ?
|
||||
|
||||
*/
|
||||
{
|
||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: { limit: 10000, mimetype: 'image/svg+xml' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
fallback: {
|
||||
/*
|
||||
Used by :
|
||||
@graphql-codegen/typescript and it's deps (@graphql-codegen/visitor-plugin-common && parse-filepath)
|
||||
=> one usage is found, so we have to check if the usage is still relevant
|
||||
*/
|
||||
path: require.resolve('path-browserify'),
|
||||
/*
|
||||
Used by :
|
||||
jsonwebtoken deps (jwa && jws)
|
||||
=> we already have an equivalent in the codebases that don't depend on it,jwt-decode.
|
||||
Might be worth using only the latter
|
||||
*/
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
/*
|
||||
Used by :
|
||||
jsonwebtoken deps (jwa && jws)
|
||||
@graphql-tools/merge => dependanci of graphiql & graphql-codegen/core, a package upgrade might fix it
|
||||
*/
|
||||
util: require.resolve('util/'),
|
||||
/*
|
||||
Used by :
|
||||
jsonwebtoken deps (jwa && jws)
|
||||
*/
|
||||
stream: require.resolve('stream-browserify'),
|
||||
},
|
||||
},
|
||||
});
|
||||
// Kept there while we setup everything
|
||||
console.log('----------------NX-CONTEXT-------------------');
|
||||
log(context);
|
||||
console.log('----------------ORIGINAL-WEBPACK-CONFIG------');
|
||||
log(config);
|
||||
console.log('----------------MODIFIED-WEBPACK-CONFIG------');
|
||||
log(output);
|
||||
console.log('---------------------------------------------');
|
||||
return output;
|
||||
};
|
@ -21,7 +21,7 @@
|
||||
],
|
||||
"styles": ["apps/console-ce/src/styles.css"],
|
||||
"scripts": [],
|
||||
"webpackConfig": "@nrwl/react/plugins/webpack"
|
||||
"webpackConfig": "apps/console-ce/custom-webpack.config.js"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
|
@ -1 +0,0 @@
|
||||
/* Your styles goes here. */
|
@ -1,17 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import App from './App';
|
||||
|
||||
describe('App', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<App />);
|
||||
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a greeting as the title', () => {
|
||||
const { getByText } = render(<App />);
|
||||
|
||||
expect(getByText(/Welcome console-ce/gi)).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,16 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import styles from './App.module.css';
|
||||
import NxWelcome from './nx-welcome';
|
||||
import { ConsoleLegacyOss } from '@hasura/console/legacy-oss';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<>
|
||||
<ConsoleLegacyOss />
|
||||
<NxWelcome title="console-ce" />
|
||||
<div />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -1,820 +0,0 @@
|
||||
/*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
This is a starter component and can be deleted.
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
Delete this file and get started with your project!
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*/
|
||||
export function NxWelcome({ title }: { title: string }) {
|
||||
return (
|
||||
<>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
h1,
|
||||
h2 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
shape-rendering: auto;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
pre {
|
||||
background-color: rgba(55, 65, 81, 1);
|
||||
border-radius: 0.25rem;
|
||||
color: rgba(229, 231, 235, 1);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
overflow: scroll;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 768px;
|
||||
padding-bottom: 3rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
width: 100%;
|
||||
}
|
||||
#welcome {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
#welcome h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1;
|
||||
}
|
||||
#welcome span {
|
||||
display: block;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 2.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#hero {
|
||||
align-items: center;
|
||||
background-color: hsla(214, 62%, 21%, 1);
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#hero .text-container {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
#hero .text-container h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
#hero .text-container h2 svg {
|
||||
color: hsla(162, 47%, 50%, 1);
|
||||
height: 2rem;
|
||||
left: -0.25rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
#hero .text-container h2 span {
|
||||
margin-left: 2.5rem;
|
||||
}
|
||||
#hero .text-container a {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border-radius: 0.75rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem 2rem;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
#hero .logo-container svg {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
width: 66.666667%;
|
||||
}
|
||||
#middle-content {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
gap: 4rem;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#learning-materials {
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#learning-materials h2 {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.list-item-link {
|
||||
align-items: center;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 100%;
|
||||
}
|
||||
.list-item-link svg:first-child {
|
||||
margin-right: 1rem;
|
||||
height: 1.5rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1.5rem;
|
||||
}
|
||||
.list-item-link > span {
|
||||
flex-grow: 1;
|
||||
font-weight: 400;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link > span > span {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 300;
|
||||
line-height: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link svg:last-child {
|
||||
height: 1rem;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1rem;
|
||||
}
|
||||
.list-item-link:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-color: hsla(162, 47%, 50%, 1);
|
||||
}
|
||||
.list-item-link:hover > span {}
|
||||
.list-item-link:hover > span > span {
|
||||
color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
.list-item-link:hover svg:last-child {
|
||||
transform: translateX(0.25rem);
|
||||
}
|
||||
#other-links {}
|
||||
.button-pill {
|
||||
padding: 1.5rem 2rem;
|
||||
transition-duration: 300ms;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.button-pill svg {
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
.button-pill > span {
|
||||
letter-spacing: -0.025em;
|
||||
font-weight: 400;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.button-pill span span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.button-pill:hover svg,
|
||||
.button-pill:hover {
|
||||
color: rgba(255, 255, 255, 1) !important;
|
||||
}
|
||||
#nx-console:hover {
|
||||
background-color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-console svg {
|
||||
color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-repo:hover {
|
||||
background-color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
#nx-repo svg {
|
||||
color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
#nx-cloud {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 2rem;
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#nx-cloud > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
#nx-cloud > div svg {
|
||||
border-radius: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
#nx-cloud > div h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#nx-cloud > div h2 span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
#nx-cloud p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud pre {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud a {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
#nx-cloud a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#commands {
|
||||
padding: 2.5rem 2rem;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#commands h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#commands p {
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
details {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
details pre > span {
|
||||
color: rgba(181, 181, 181, 1);
|
||||
display: block;
|
||||
}
|
||||
summary {
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
summary:hover {
|
||||
background-color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
summary svg {
|
||||
height: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
#love {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 3.5rem;
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
}
|
||||
#love svg {
|
||||
color: rgba(252, 165, 165, 1);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: inline;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
#hero {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: flex;
|
||||
}
|
||||
#middle-content {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div className="wrapper">
|
||||
<div className="container">
|
||||
<div id="welcome">
|
||||
<h1>
|
||||
<span> Hello there, </span>
|
||||
Welcome {title} 👋
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div id="hero" className="rounded">
|
||||
<div className="text-container">
|
||||
<h2>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
|
||||
/>
|
||||
</svg>
|
||||
<span>You're up and running</span>
|
||||
</h2>
|
||||
<a href="#commands"> What's next? </a>
|
||||
</div>
|
||||
<div className="logo-container">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="middle-content">
|
||||
<div id="learning-materials" className="rounded shadow">
|
||||
<h2>Learning materials</h2>
|
||||
<a
|
||||
href="https://nx.dev/getting-started/intro?utm_source=nx-project"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="list-item-link"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Documentation
|
||||
<span> Everything is in there </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://blog.nrwl.io/?utm_source=nx-project"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="list-item-link"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Blog
|
||||
<span> Changelog, features & events </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/c/Nrwl_io/videos?utm_source=nx-project&sub_confirmation=1"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="list-item-link"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>YouTube</title>
|
||||
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</svg>
|
||||
<span>
|
||||
YouTube channel
|
||||
<span> Nx Show, talks & tutorials </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://nx.dev/react-tutorial/01-create-application?utm_source=nx-project"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="list-item-link"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Interactive tutorials
|
||||
<span> Create an app, step-by-step </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://nxplaybook.com/?utm_source=nx-project"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="list-item-link"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 14l9-5-9-5-9 5 9 5z" />
|
||||
<path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Video courses
|
||||
<span> Nx custom courses </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div id="other-links">
|
||||
<a
|
||||
id="nx-console"
|
||||
className="button-pill rounded shadow"
|
||||
href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Visual Studio Code</title>
|
||||
<path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z" />
|
||||
</svg>
|
||||
<span>
|
||||
Install Nx Console
|
||||
<span>Plugin for VSCode</span>
|
||||
</span>
|
||||
</a>
|
||||
<div id="nx-cloud" className="rounded shadow">
|
||||
<div>
|
||||
<svg
|
||||
viewBox="0 0 120 120"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M120 15V30C103.44 30 90 43.44 90 60C90 76.56 76.56 90 60 90C43.44 90 30 103.44 30 120H15C6.72 120 0 113.28 0 105V15C0 6.72 6.72 0 15 0H105C113.28 0 120 6.72 120 15Z"
|
||||
fill="#0E2039"
|
||||
/>
|
||||
<path
|
||||
d="M120 30V105C120 113.28 113.28 120 105 120H30C30 103.44 43.44 90 60 90C76.56 90 90 76.56 90 60C90 43.44 103.44 30 120 30Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
<h2>
|
||||
NxCloud
|
||||
<span>Enable faster CI & better DX</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p>
|
||||
You can activate distributed tasks executions and caching by
|
||||
running:
|
||||
</p>
|
||||
<pre>nx connect-to-nx-cloud</pre>
|
||||
<a
|
||||
href="https://nx.app/?utm_source=nx-project"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{' '}
|
||||
What is Nx Cloud?{' '}
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
id="nx-repo"
|
||||
className="button-pill rounded shadow"
|
||||
href="https://github.com/nrwl/nx?utm_source=nx-project"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
<span>
|
||||
Nx is open source
|
||||
<span> Love Nx? Give us a star! </span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="commands" className="rounded shadow">
|
||||
<h2>Next steps</h2>
|
||||
<p>Here are some things you can do with Nx:</p>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Add UI library
|
||||
</summary>
|
||||
<pre>
|
||||
<span># Generate UI lib</span>
|
||||
nx g @nrwl/react:lib ui
|
||||
<span># Add a component</span>
|
||||
nx g @nrwl/react:component button --project ui
|
||||
</pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
View interactive project graph
|
||||
</summary>
|
||||
<pre>nx graph</pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Run affected commands
|
||||
</summary>
|
||||
<pre>
|
||||
<span># see what's been affected by changes</span>
|
||||
nx affected:graph
|
||||
<span># run tests for current changes</span>
|
||||
nx affected:test
|
||||
<span># run e2e tests for current changes</span>
|
||||
nx affected:e2e
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<p id="love">
|
||||
Carefully crafted with
|
||||
<svg
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NxWelcome;
|
@ -1,7 +1,6 @@
|
||||
import { StrictMode } from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
import App from './app/App';
|
||||
import { App } from '@hasura/console/legacy-oss';
|
||||
|
||||
ReactDOM.render(
|
||||
<StrictMode>
|
||||
|
@ -6,7 +6,8 @@
|
||||
},
|
||||
"files": [
|
||||
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||
"../../node_modules/@nrwl/react/typings/image.d.ts",
|
||||
"../../types/graphiql-code-exporter.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
|
@ -19,6 +19,7 @@
|
||||
],
|
||||
"files": [
|
||||
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||
"../../node_modules/@nrwl/react/typings/image.d.ts",
|
||||
"../../types/graphiql-code-exporter.d.ts"
|
||||
]
|
||||
}
|
||||
|
@ -4,15 +4,42 @@
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"@nrwl/nx/enforce-module-boundaries": [
|
||||
"error",
|
||||
{
|
||||
"allowCircularSelfDependency": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"@typescript-eslint/no-empty-function": "warn",
|
||||
"@typescript-eslint/ban-types": "warn",
|
||||
"@typescript-eslint/no-namespace": "warn",
|
||||
"@typescript-eslint/no-empty-interface": "warn",
|
||||
"@typescript-eslint/no-var-requires": "warn",
|
||||
"no-case-declarations": "warn",
|
||||
"no-unsafe-optional-chaining": "warn",
|
||||
"no-useless-catch": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"@typescript-eslint/no-empty-function": "warn",
|
||||
"@typescript-eslint/no-empty-interface": "warn",
|
||||
"no-case-declarations": "warn",
|
||||
"import/first": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-restricted-globals": "warn",
|
||||
"no-prototype-builtins": "warn",
|
||||
"@typescript-eslint/no-this-alias": "warn",
|
||||
"no-unsafe-optional-chaining": "warn",
|
||||
"no-useless-catch": "warn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './lib/console-legacy-oss';
|
||||
export const add = (a: number, b: number): number => a + b;
|
||||
export { App } from './lib/client';
|
||||
|
34
frontend/libs/console/legacy-oss/src/lib/Endpoints.ts
Normal file
34
frontend/libs/console/legacy-oss/src/lib/Endpoints.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import globals from './Globals';
|
||||
|
||||
const baseUrl = globals.dataApiUrl;
|
||||
const hasuractlApiHost = globals.apiHost;
|
||||
const hasuractlApiPort = globals.apiPort;
|
||||
|
||||
const hasuractlUrl = `${hasuractlApiHost}:${hasuractlApiPort}`;
|
||||
|
||||
const Endpoints = {
|
||||
serverConfig: `${baseUrl}/v1alpha1/config`,
|
||||
graphQLUrl: `${baseUrl}/v1/graphql`,
|
||||
relayURL: `${baseUrl}/v1beta1/relay`,
|
||||
query: `${baseUrl}/v2/query`,
|
||||
metadata: `${baseUrl}/v1/metadata`,
|
||||
// metadata: `${baseUrl}/v1/query`,
|
||||
queryV2: `${baseUrl}/v2/query`,
|
||||
version: `${baseUrl}/v1/version`,
|
||||
updateCheck: 'https://releases.hasura.io/graphql-engine',
|
||||
hasuractlMigrate: `${hasuractlUrl}/apis/migrate`,
|
||||
hasuractlMetadata: `${hasuractlUrl}/apis/metadata`,
|
||||
hasuractlMigrateSettings: `${hasuractlUrl}/apis/migrate/settings`,
|
||||
telemetryServer: 'wss://telemetry.hasura.io/v1/ws',
|
||||
consoleNotificationsStg:
|
||||
'https://notifications.hasura-stg.hasura-app.io/v1/graphql',
|
||||
consoleNotificationsProd: 'https://notifications.hasura.io/v1/graphql',
|
||||
luxDataGraphql: globals.luxDataHost
|
||||
? `${window.location.protocol}//${globals.luxDataHost}/v1/graphql`
|
||||
: `${globals.cloudDataApiUrl}/v1/graphql`,
|
||||
};
|
||||
|
||||
const globalCookiePolicy = 'same-origin';
|
||||
|
||||
export default Endpoints;
|
||||
export { globalCookiePolicy, baseUrl, hasuractlUrl };
|
216
frontend/libs/console/legacy-oss/src/lib/Globals.ts
Normal file
216
frontend/libs/console/legacy-oss/src/lib/Globals.ts
Normal file
@ -0,0 +1,216 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
import { SERVER_CONSOLE_MODE } from './constants';
|
||||
import { getFeaturesCompatibility } from './helpers/versionUtils';
|
||||
import { stripTrailingSlash } from './components/Common/utils/urlUtils';
|
||||
import { isEmpty } from './components/Common/utils/jsUtils';
|
||||
|
||||
type ConsoleType = 'oss' | 'cloud' | 'pro' | 'pro-lite';
|
||||
|
||||
type UUID = string;
|
||||
|
||||
type OSSServerEnv = {
|
||||
consoleMode: 'server';
|
||||
consoleType: 'oss';
|
||||
assetsPath: string; // e.g. "https://graphql-engine-cdn.hasura.io/console/assets"
|
||||
consolePath: string; // e.g. "/console"
|
||||
enableTelemetry: boolean;
|
||||
isAdminSecretSet: boolean;
|
||||
serverVersion: string; // e.g. "v2.7.0"
|
||||
urlPrefix: string; // e.g. "/console"
|
||||
cdnAssets: boolean;
|
||||
};
|
||||
|
||||
type ProServerEnv = {
|
||||
consoleType: 'pro';
|
||||
consoleId: string;
|
||||
consoleMode: 'server';
|
||||
assetsPath: string;
|
||||
consolePath: string;
|
||||
enableTelemetry: boolean;
|
||||
isAdminSecretSet: boolean;
|
||||
serverVersion: string;
|
||||
urlPrefix: string;
|
||||
};
|
||||
|
||||
type ProLiteServerEnv = {
|
||||
consoleType: 'pro-lite';
|
||||
consoleId: string;
|
||||
consoleMode: 'server';
|
||||
assetsPath: string;
|
||||
consolePath: string;
|
||||
enableTelemetry: boolean;
|
||||
isAdminSecretSet: boolean;
|
||||
serverVersion: string;
|
||||
urlPrefix: string;
|
||||
};
|
||||
|
||||
type CloudUserRole = 'owner' | 'user';
|
||||
|
||||
type CloudServerEnv = {
|
||||
consoleMode: 'server';
|
||||
consoleType: 'cloud';
|
||||
adminSecret: string;
|
||||
assetsPath: string;
|
||||
cloudRootDomain: string; // e.g. "pro.hasura.io"
|
||||
consoleId: string; // e.g. "40d778e7-1324-4500-bf69-5f9e58f70803_console"
|
||||
consolePath: string;
|
||||
dataApiUrl: string; // e.g. "https://rich-jackass-37.hasura.app"
|
||||
eeMode: string;
|
||||
herokuOAuthClientId: UUID;
|
||||
isAdminSecretSet: boolean;
|
||||
luxDataHost: string; // e.g. "data.pro.hasura.io"
|
||||
projectID: UUID;
|
||||
serverVersion: string;
|
||||
tenantID: UUID;
|
||||
urlPrefix: string;
|
||||
userRole: CloudUserRole;
|
||||
};
|
||||
|
||||
type OSSCliEnv = {
|
||||
consoleMode: 'cli';
|
||||
adminSecret: string;
|
||||
apiHost: string; // e.g. "http://localhost"
|
||||
apiPort: string; // e.g. "9693"
|
||||
assetsPath: string;
|
||||
cliUUID: UUID;
|
||||
consolePath: string;
|
||||
dataApiUrl: string;
|
||||
enableTelemetry: boolean;
|
||||
serverVersion: string;
|
||||
urlPrefix: string;
|
||||
};
|
||||
|
||||
export type CloudCliEnv = {
|
||||
consoleMode: 'cli';
|
||||
adminSecret: string;
|
||||
apiHost: string;
|
||||
apiPort: string;
|
||||
assetsPath: string;
|
||||
cliUUID: string;
|
||||
consolePath: string;
|
||||
dataApiUrl: string;
|
||||
enableTelemetry: boolean;
|
||||
serverVersion: string;
|
||||
urlPrefix: string;
|
||||
/* NOTE
|
||||
While in CLI mode we are relying on the "pro" key to determine if we are in the pro console or not.
|
||||
We could ask the CLI team to add a consoleType env var so that we can rely on values "cloud" | "pro",
|
||||
like in the server console mode
|
||||
*/
|
||||
pro: true;
|
||||
projectId: UUID;
|
||||
isAdminSecretSet: boolean;
|
||||
};
|
||||
|
||||
type ProCliEnv = CloudCliEnv;
|
||||
type ProLiteCliEnv = CloudCliEnv;
|
||||
|
||||
export type EnvVars = {
|
||||
nodeEnv?: string;
|
||||
apiHost?: string;
|
||||
apiPort?: string;
|
||||
dataApiUrl?: string;
|
||||
adminSecret?: string;
|
||||
serverVersion: string;
|
||||
cliUUID?: string;
|
||||
tenantID?: UUID;
|
||||
projectID?: UUID;
|
||||
cloudRootDomain?: string;
|
||||
herokuOAuthClientId?: string;
|
||||
luxDataHost?: string;
|
||||
isAdminSecretSet?: boolean;
|
||||
enableTelemetry?: boolean;
|
||||
consoleType?: ConsoleType;
|
||||
eeMode?: string;
|
||||
consoleId?: string;
|
||||
} & (
|
||||
| OSSServerEnv
|
||||
| CloudServerEnv
|
||||
| ProServerEnv
|
||||
| ProLiteServerEnv
|
||||
| OSSCliEnv
|
||||
| CloudCliEnv
|
||||
| ProCliEnv
|
||||
| ProLiteCliEnv
|
||||
);
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__env: EnvVars;
|
||||
}
|
||||
const CONSOLE_ASSET_VERSION: string;
|
||||
}
|
||||
|
||||
/* initialize globals */
|
||||
|
||||
const isProduction = window.__env?.nodeEnv !== 'development';
|
||||
|
||||
const globals = {
|
||||
apiHost: window.__env?.apiHost,
|
||||
apiPort: window.__env?.apiPort,
|
||||
dataApiUrl: stripTrailingSlash(window.__env?.dataApiUrl || ''), // overridden below if server mode
|
||||
urlPrefix: stripTrailingSlash(window.__env?.urlPrefix || '/'), // overridden below if server mode in production
|
||||
adminSecret: window.__env?.adminSecret || null, // gets updated after login/logout in server mode
|
||||
isAdminSecretSet:
|
||||
window.__env?.isAdminSecretSet ||
|
||||
!isEmpty(window.__env?.adminSecret) ||
|
||||
false,
|
||||
consoleMode: window.__env?.consoleMode || SERVER_CONSOLE_MODE,
|
||||
enableTelemetry: window.__env?.enableTelemetry,
|
||||
telemetryTopic: isProduction ? 'console' : 'console_test',
|
||||
assetsPath: window.__env?.assetsPath,
|
||||
serverVersion: window.__env?.serverVersion || '',
|
||||
consoleAssetVersion: CONSOLE_ASSET_VERSION, // set during console build
|
||||
featuresCompatibility: window.__env?.serverVersion
|
||||
? getFeaturesCompatibility(window.__env?.serverVersion || '')
|
||||
: null,
|
||||
cliUUID: window.__env?.cliUUID || '',
|
||||
hasuraUUID: '',
|
||||
telemetryNotificationShown: false,
|
||||
isProduction,
|
||||
herokuOAuthClientId: window.__env?.herokuOAuthClientId,
|
||||
hasuraCloudTenantId: window.__env?.tenantID,
|
||||
hasuraCloudProjectId: window.__env?.projectID,
|
||||
cloudDataApiUrl: `${window.location?.protocol}//data.${window.__env?.cloudRootDomain}`,
|
||||
luxDataHost: window.__env?.luxDataHost,
|
||||
userRole: undefined, // userRole is not applicable for the OSS console
|
||||
consoleType: window.__env?.consoleType || '',
|
||||
eeMode: window.__env?.eeMode === 'true',
|
||||
};
|
||||
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
|
||||
if (!window.__env?.dataApiUrl) {
|
||||
globals.dataApiUrl = stripTrailingSlash(window.location?.href);
|
||||
}
|
||||
if (isProduction) {
|
||||
const consolePath = window.__env?.consolePath;
|
||||
if (consolePath) {
|
||||
let currentUrl = stripTrailingSlash(window.location?.href);
|
||||
let slicePath = true;
|
||||
if (window.__env?.dataApiUrl) {
|
||||
currentUrl = stripTrailingSlash(window.__env?.dataApiUrl || '');
|
||||
slicePath = false;
|
||||
}
|
||||
const currentPath = stripTrailingSlash(window.location?.pathname);
|
||||
|
||||
// NOTE: perform the slice if not on team console
|
||||
// as on team console, we're using the server
|
||||
// endpoint directly to load the assets of the console
|
||||
if (slicePath) {
|
||||
globals.dataApiUrl = currentUrl.slice(
|
||||
0,
|
||||
currentUrl.lastIndexOf(consolePath)
|
||||
);
|
||||
}
|
||||
|
||||
globals.urlPrefix = `${currentPath.slice(
|
||||
0,
|
||||
currentPath.lastIndexOf(consolePath)
|
||||
)}/console`;
|
||||
} else {
|
||||
const windowHostUrl = `${window.location?.protocol}//${window.location?.host}`;
|
||||
globals.dataApiUrl = windowHostUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default globals;
|
70
frontend/libs/console/legacy-oss/src/lib/client.js
Executable file
70
frontend/libs/console/legacy-oss/src/lib/client.js
Executable file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* THIS IS THE ENTRY POINT FOR THE CLIENT, JUST LIKE server.js IS THE ENTRY POINT FOR THE SERVER.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Router, browserHistory } from 'react-router';
|
||||
import { syncHistoryWithStore } from 'react-router-redux';
|
||||
import { useBasename } from 'history';
|
||||
import { ReactQueryProvider } from './lib/reactQuery';
|
||||
import './theme/tailwind.css';
|
||||
import './theme/legacy-boostrap.css';
|
||||
|
||||
import getRoutes from './routes';
|
||||
|
||||
import globals from './Globals';
|
||||
import { store } from './store';
|
||||
|
||||
const hashLinkScroll = () => {
|
||||
const { hash } = window.location;
|
||||
if (hash !== '') {
|
||||
// Push onto callback queue so it runs after the DOM is updated,
|
||||
// this is required when navigating from a different page so that
|
||||
// the element is rendered on the page before trying to getElementById.
|
||||
setTimeout(() => {
|
||||
const id = hash.replace('#', '');
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView();
|
||||
}
|
||||
}, 0);
|
||||
} else {
|
||||
// This is a hack to solve the issue with scroll retention during page change.
|
||||
setTimeout(() => {
|
||||
const element = document.getElementsByTagName('body');
|
||||
if (element && element.length > 0) {
|
||||
element[0].scrollIntoView();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const history = syncHistoryWithStore(browserHistory, store);
|
||||
|
||||
/* ****************************************************************** */
|
||||
|
||||
// Main routes and rendering
|
||||
const Main = () => {
|
||||
const routeHistory = useBasename(() => history)({
|
||||
basename: globals.urlPrefix,
|
||||
});
|
||||
return (
|
||||
<ReactQueryProvider>
|
||||
<Router
|
||||
history={routeHistory}
|
||||
routes={getRoutes(store)}
|
||||
onUpdate={hashLinkScroll}
|
||||
/>
|
||||
</ReactQueryProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const App = () => (
|
||||
<Provider store={store} key="provider">
|
||||
<Main />
|
||||
</Provider>
|
||||
);
|
@ -0,0 +1,92 @@
|
||||
import defaultState from './State';
|
||||
import { loadConsoleOpts } from '../../telemetry/Actions';
|
||||
import {
|
||||
fetchServerConfig,
|
||||
fetchHerokuSession,
|
||||
initialiseOnboardingSampleDBConfig,
|
||||
} from '../Main/Actions';
|
||||
|
||||
const LOAD_REQUEST = 'App/ONGOING_REQUEST';
|
||||
const DONE_REQUEST = 'App/DONE_REQUEST';
|
||||
const FAILED_REQUEST = 'App/FAILED_REQUEST';
|
||||
const ERROR_REQUEST = 'App/ERROR_REQUEST';
|
||||
const CONNECTION_FAILED = 'App/CONNECTION_FAILED';
|
||||
|
||||
export const requireAsyncGlobals = (
|
||||
{ dispatch },
|
||||
shouldLoadOpts = true,
|
||||
shouldLoadServerConfig = true
|
||||
) => {
|
||||
return (nextState, finalState, callback) => {
|
||||
Promise.all([
|
||||
shouldLoadOpts && dispatch(loadConsoleOpts()),
|
||||
shouldLoadServerConfig && dispatch(fetchServerConfig),
|
||||
dispatch(fetchHerokuSession()),
|
||||
dispatch(initialiseOnboardingSampleDBConfig()),
|
||||
]).finally(callback);
|
||||
};
|
||||
};
|
||||
|
||||
const progressBarReducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case LOAD_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
ongoingRequest: true,
|
||||
percent: 10,
|
||||
requestSuccess: null,
|
||||
requestError: null,
|
||||
connectionFailed: false,
|
||||
};
|
||||
|
||||
case DONE_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
percent: 100,
|
||||
ongoingRequest: false,
|
||||
requestSuccess: true,
|
||||
requestError: null,
|
||||
connectionFailed: false,
|
||||
};
|
||||
|
||||
case FAILED_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
percent: 100,
|
||||
ongoingRequest: false,
|
||||
requestSuccess: null,
|
||||
requestError: true,
|
||||
connectionFailed: false,
|
||||
};
|
||||
|
||||
case ERROR_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
modalOpen: true,
|
||||
error: action.data,
|
||||
reqURL: action.url,
|
||||
reqData: action.params,
|
||||
statusCode: action.statusCode,
|
||||
connectionFailed: false,
|
||||
};
|
||||
case CONNECTION_FAILED:
|
||||
return {
|
||||
...state,
|
||||
modalOpen: true,
|
||||
error: true,
|
||||
ongoingRequest: false,
|
||||
connectionFailed: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default progressBarReducer;
|
||||
export {
|
||||
LOAD_REQUEST,
|
||||
DONE_REQUEST,
|
||||
FAILED_REQUEST,
|
||||
ERROR_REQUEST,
|
||||
CONNECTION_FAILED,
|
||||
};
|
115
frontend/libs/console/legacy-oss/src/lib/components/App/App.js
Normal file
115
frontend/libs/console/legacy-oss/src/lib/components/App/App.js
Normal file
@ -0,0 +1,115 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ProgressBar from 'react-progress-bar-plus';
|
||||
import Notifications from 'react-notification-system-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import ErrorBoundary from '../Error/ErrorBoundary';
|
||||
import { telemetryNotificationShown } from '../../telemetry/Actions';
|
||||
import { showTelemetryNotification } from '../../telemetry/Notifications';
|
||||
import globals from '../../Globals';
|
||||
import styles from './App.module.scss';
|
||||
|
||||
import { theme } from '../UIKit/theme';
|
||||
|
||||
export const GlobalContext = React.createContext(globals);
|
||||
|
||||
const App = ({
|
||||
ongoingRequest,
|
||||
percent,
|
||||
intervalTime,
|
||||
children,
|
||||
notifications,
|
||||
connectionFailed,
|
||||
dispatch,
|
||||
metadata,
|
||||
telemetry,
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
const className = document.getElementById('content').className;
|
||||
document.getElementById('content').className = className + ' show';
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}, []);
|
||||
const telemetryShown = React.useRef(false);
|
||||
// should be true only in the case of hasura cloud
|
||||
const isContextCloud = globals.consoleType === 'cloud';
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
telemetry.console_opts &&
|
||||
!telemetry.console_opts.telemetryNotificationShown &&
|
||||
!telemetryShown.current &&
|
||||
!isContextCloud
|
||||
) {
|
||||
telemetryShown.current = true;
|
||||
dispatch(showTelemetryNotification());
|
||||
dispatch(telemetryNotificationShown());
|
||||
}
|
||||
}, [dispatch, telemetry, isContextCloud]);
|
||||
|
||||
let connectionFailMsg = null;
|
||||
if (connectionFailed) {
|
||||
connectionFailMsg = (
|
||||
<div
|
||||
className={`${styles.alertDanger} ${styles.remove_margin_bottom} alert alert-danger `}
|
||||
>
|
||||
<strong>
|
||||
Hasura console is not able to reach your Hasura GraphQL engine
|
||||
instance. Please ensure that your instance is running and the endpoint
|
||||
is configured correctly.
|
||||
</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={globals}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ErrorBoundary metadata={metadata} dispatch={dispatch}>
|
||||
<div>
|
||||
{connectionFailMsg}
|
||||
{ongoingRequest && (
|
||||
<ProgressBar
|
||||
percent={percent}
|
||||
autoIncrement={true} // eslint-disable-line react/jsx-boolean-value
|
||||
intervalTime={intervalTime}
|
||||
spinner={false}
|
||||
/>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
<Notifications notifications={notifications} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
</GlobalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
App.propTypes = {
|
||||
reqURL: PropTypes.string,
|
||||
reqData: PropTypes.object,
|
||||
statusCode: PropTypes.number,
|
||||
|
||||
ongoingRequest: PropTypes.bool,
|
||||
connectionFailed: PropTypes.bool,
|
||||
|
||||
intervalTime: PropTypes.number,
|
||||
percent: PropTypes.number,
|
||||
|
||||
children: PropTypes.element,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
|
||||
notifications: PropTypes.array,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
...state.progressBar,
|
||||
notifications: state.notifications,
|
||||
telemetry: state.telemetry,
|
||||
metadata: state.metadata,
|
||||
};
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps)(App));
|
@ -0,0 +1,68 @@
|
||||
@import "../Common/Common.module";
|
||||
|
||||
:global {
|
||||
@keyframes react-progress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.react-progress-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: all 400ms;
|
||||
z-index: 9999;
|
||||
|
||||
&.react-progress-bar-on-top {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.react-progress-bar-hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
z-index: -10;
|
||||
}
|
||||
}
|
||||
|
||||
.react-progress-bar-percent {
|
||||
height: 2px;
|
||||
background: #e8694d;
|
||||
box-shadow: 0 2px 5px #d3291c, 0 2px 5px #d3291c;
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
.react-progress-bar-spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.react-progress-bar-spinner-left {
|
||||
left: 15px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.react-progress-bar-spinner-right {
|
||||
left: auto;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.react-progress-bar-spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
border: solid 2px transparent;
|
||||
border-top-color: #d3291c;
|
||||
border-left-color: #d3291c;
|
||||
border-radius: 50%;
|
||||
animation: react-progress-spinner 400ms linear infinite;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
const defaultState = {
|
||||
percent: 0,
|
||||
intervalTime: 200,
|
||||
ongoingRequest: false,
|
||||
requestSuccess: null,
|
||||
requestError: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export default defaultState;
|
@ -0,0 +1,39 @@
|
||||
import globals from '../Globals';
|
||||
import {
|
||||
getLSItem,
|
||||
setLSItem,
|
||||
removeLSItem,
|
||||
LS_KEYS,
|
||||
getParsedLSItem,
|
||||
} from '../utils/localStorage';
|
||||
|
||||
const loadAppState = () => {
|
||||
return getParsedLSItem(LS_KEYS.consoleLocalInfo);
|
||||
};
|
||||
|
||||
const saveAppState = (state: string) => {
|
||||
setLSItem(LS_KEYS.consoleLocalInfo, JSON.stringify(state));
|
||||
};
|
||||
|
||||
const loadAdminSecretState = () => getLSItem(LS_KEYS.consoleAdminSecret);
|
||||
|
||||
const saveAdminSecretState = (state: string) => {
|
||||
setLSItem(LS_KEYS.consoleAdminSecret, state);
|
||||
};
|
||||
|
||||
const clearAdminSecretState = () => {
|
||||
removeLSItem(LS_KEYS.consoleAdminSecret);
|
||||
|
||||
globals.adminSecret = null;
|
||||
};
|
||||
|
||||
const clearState = () => removeLSItem(LS_KEYS.consoleLocalInfo);
|
||||
|
||||
export {
|
||||
saveAppState,
|
||||
saveAdminSecretState,
|
||||
loadAppState,
|
||||
loadAdminSecretState,
|
||||
clearState,
|
||||
clearAdminSecretState,
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import AceEditor, { IAceEditorProps } from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/ext-searchbox';
|
||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||
import 'ace-builds/src-noconflict/ext-error_marker';
|
||||
import 'ace-builds/src-noconflict/ext-beautify';
|
||||
import { ACE_EDITOR_THEME, ACE_EDITOR_FONT_SIZE } from './utils';
|
||||
|
||||
interface EditorProps extends IAceEditorProps {
|
||||
editorRef?: any;
|
||||
}
|
||||
|
||||
const Editor: React.FC<EditorProps> = ({ mode, editorRef, ...props }) => {
|
||||
return (
|
||||
<AceEditor
|
||||
ref={editorRef}
|
||||
mode={mode}
|
||||
theme={ACE_EDITOR_THEME}
|
||||
fontSize={ACE_EDITOR_FONT_SIZE}
|
||||
showGutter
|
||||
tabSize={2}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
@ -0,0 +1,28 @@
|
||||
// eslint-disable-file import/no-extraneous-dependencies
|
||||
|
||||
import 'ace-builds/src-noconflict/theme-eclipse';
|
||||
import 'ace-builds/src-noconflict/mode-graphqlschema';
|
||||
import 'ace-builds/src-noconflict/mode-sql';
|
||||
import 'ace-builds/src-noconflict/ext-searchbox';
|
||||
|
||||
export const ACE_EDITOR_THEME = 'eclipse';
|
||||
export const ACE_EDITOR_FONT_SIZE = 14;
|
||||
|
||||
export const getLanguageModeFromExtension = (extension: string) => {
|
||||
switch (extension) {
|
||||
case 'ts':
|
||||
return 'typescript';
|
||||
case 'go':
|
||||
return 'golang';
|
||||
case 'kt':
|
||||
return 'kotlin';
|
||||
case 'py':
|
||||
return 'python';
|
||||
case 'java':
|
||||
return 'java';
|
||||
case 'ruby':
|
||||
return 'ruby';
|
||||
default:
|
||||
return 'javascript';
|
||||
}
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
export type AlertType = 'warning' | 'danger' | 'success';
|
||||
|
||||
interface AlertProps {
|
||||
type: AlertType;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const Alert: React.FC<AlertProps> = ({ type, text }) => (
|
||||
<div className={`hidden alert alert-${type}`} role="alert">
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Alert;
|
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from '../Common.module.scss';
|
||||
|
||||
import { GlobalContext } from '../../App/App';
|
||||
import { trackRuntimeError } from '../../../telemetry';
|
||||
import { isConsoleError } from '../utils/jsUtils';
|
||||
|
||||
/*
|
||||
This is a Button HOC that takes all the props supported by <button>
|
||||
- color(default: white): color of the button; currently supports yellow, red, green, gray and white
|
||||
- size: size of the button; currently supports xs (extra small), sm(small)
|
||||
- className: although you can provide any CSS classname, it is recommended to use only the positioning related classes
|
||||
and not the ones that change the appearance (color, font, size) of the button
|
||||
*/
|
||||
|
||||
export interface ButtonProps extends React.ComponentProps<'button'> {
|
||||
size?: string;
|
||||
color?: 'yellow' | 'red' | 'green' | 'gray' | 'white' | 'black';
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = props => {
|
||||
const { children, onClick, size, color, className, type = 'button' } = props;
|
||||
let extendedClassName = `${className || ''} btn ${
|
||||
size ? `btn-${size} ` : 'button '
|
||||
}`;
|
||||
switch (color) {
|
||||
case 'yellow':
|
||||
extendedClassName += styles.yellow_button;
|
||||
break;
|
||||
case 'red':
|
||||
extendedClassName += 'btn-danger';
|
||||
break;
|
||||
case 'green':
|
||||
extendedClassName += 'btn-success';
|
||||
break;
|
||||
case 'gray':
|
||||
extendedClassName += styles.gray_button;
|
||||
break;
|
||||
default:
|
||||
extendedClassName += 'btn-default';
|
||||
break;
|
||||
}
|
||||
|
||||
const globals = React.useContext(GlobalContext);
|
||||
|
||||
const trackedOnClick = (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
try {
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (isConsoleError(error)) {
|
||||
trackRuntimeError(globals, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={extendedClassName}
|
||||
type={type}
|
||||
onClick={onClick ? trackedOnClick : undefined}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
@ -0,0 +1,3 @@
|
||||
import Button from './Button';
|
||||
|
||||
export default Button;
|
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { FaChevronRight } from 'react-icons/fa';
|
||||
/**
|
||||
* Accepts following props
|
||||
* `title, string || react-element `: Title of the collapsible toggle
|
||||
* `isOpen`(optional, default to false): Whether the body should be shown or not
|
||||
* `toggleHandler (optional)`: Function to call when the toggle is clicked
|
||||
* `testId, string`: Test identifier
|
||||
* `children, react-element`: The content which needs to be toggled
|
||||
*/
|
||||
|
||||
interface CollapsibleToggleProps {
|
||||
title: React.ReactNode;
|
||||
isOpen?: boolean;
|
||||
toggleHandler?: () => void;
|
||||
testId?: string;
|
||||
useDefaultTitleStyle?: boolean;
|
||||
}
|
||||
|
||||
interface CollapsibleToggleState {
|
||||
isOpen: boolean;
|
||||
toggleHandler: () => void;
|
||||
}
|
||||
|
||||
class CollapsibleToggle extends React.Component<
|
||||
CollapsibleToggleProps,
|
||||
CollapsibleToggleState
|
||||
> {
|
||||
constructor(props: CollapsibleToggleProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isOpen: props.isOpen || false,
|
||||
toggleHandler:
|
||||
props.toggleHandler || this.defaultToggleHandler.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
override componentWillReceiveProps(nextProps: CollapsibleToggleProps) {
|
||||
const { isOpen, toggleHandler } = nextProps;
|
||||
|
||||
if (toggleHandler) {
|
||||
this.setState({ isOpen: !!isOpen, toggleHandler });
|
||||
}
|
||||
}
|
||||
|
||||
defaultToggleHandler() {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { title, children, testId, useDefaultTitleStyle } = this.props;
|
||||
|
||||
const { isOpen, toggleHandler } = this.state;
|
||||
|
||||
const getTitle = () => {
|
||||
let resultTitle;
|
||||
|
||||
if (useDefaultTitleStyle) {
|
||||
resultTitle = <div className="font-semibold">{title}</div>;
|
||||
} else {
|
||||
resultTitle = title;
|
||||
}
|
||||
|
||||
return resultTitle;
|
||||
};
|
||||
|
||||
return (
|
||||
<details
|
||||
onToggle={(event: React.ChangeEvent<HTMLDetailsElement>) => {
|
||||
// it gets called on mount if open=true, so we check if we really need to call handler
|
||||
if (event.target.open !== isOpen) {
|
||||
toggleHandler();
|
||||
}
|
||||
}}
|
||||
open={isOpen}
|
||||
>
|
||||
<summary className="cursor-pointer flex items-start" data-test={testId}>
|
||||
<span className="inline-block text-xs mr-sm mt-0.5">
|
||||
<FaChevronRight className={`${isOpen && 'rotate-90'}`} />
|
||||
</span>
|
||||
<span className="inline-block">{getTitle()}</span>
|
||||
</summary>
|
||||
<div className="mt-sm">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CollapsibleToggle;
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,191 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
RequestTransformMethod,
|
||||
RequestTransformContentType,
|
||||
} from '@/metadata/types';
|
||||
import {
|
||||
KeyValuePair,
|
||||
RequestTransformState,
|
||||
RequestTransformStateBody,
|
||||
TransformationType,
|
||||
} from './stateDefaults';
|
||||
import RequestOptionsTransforms from './RequestOptionsTransforms';
|
||||
import PayloadOptionsTransforms from './PayloadOptionsTransforms';
|
||||
import SampleContextTransforms from './SampleContextTransforms';
|
||||
import Button from '../Button';
|
||||
import AddIcon from '../Icons/Add';
|
||||
|
||||
type ConfigureTransformationProps = {
|
||||
transformationType: TransformationType;
|
||||
state: RequestTransformState;
|
||||
resetSampleInput: () => void;
|
||||
envVarsOnChange: (envVars: KeyValuePair[]) => void;
|
||||
sessionVarsOnChange: (sessionVars: KeyValuePair[]) => void;
|
||||
requestMethodOnChange: (requestMethod: RequestTransformMethod) => void;
|
||||
requestUrlOnChange: (requestUrl: string) => void;
|
||||
requestQueryParamsOnChange: (requestQueryParams: KeyValuePair[]) => void;
|
||||
requestAddHeadersOnChange: (requestAddHeaders: KeyValuePair[]) => void;
|
||||
requestBodyOnChange: (requestBody: RequestTransformStateBody) => void;
|
||||
requestSampleInputOnChange: (requestSampleInput: string) => void;
|
||||
requestContentTypeOnChange?: (
|
||||
requestContentType: RequestTransformContentType
|
||||
) => void;
|
||||
requestUrlTransformOnChange: (data: boolean) => void;
|
||||
requestPayloadTransformOnChange: (data: boolean) => void;
|
||||
};
|
||||
|
||||
const ConfigureTransformation: React.FC<ConfigureTransformationProps> = ({
|
||||
transformationType,
|
||||
state,
|
||||
resetSampleInput,
|
||||
envVarsOnChange,
|
||||
sessionVarsOnChange,
|
||||
requestMethodOnChange,
|
||||
requestUrlOnChange,
|
||||
requestQueryParamsOnChange,
|
||||
requestAddHeadersOnChange,
|
||||
requestBodyOnChange,
|
||||
requestSampleInputOnChange,
|
||||
requestUrlTransformOnChange,
|
||||
requestPayloadTransformOnChange,
|
||||
}) => {
|
||||
const {
|
||||
envVars,
|
||||
sessionVars,
|
||||
requestMethod,
|
||||
requestUrl,
|
||||
requestUrlError,
|
||||
requestUrlPreview,
|
||||
requestQueryParams,
|
||||
requestAddHeaders,
|
||||
requestBody,
|
||||
requestBodyError,
|
||||
requestSampleInput,
|
||||
requestTransformedBody,
|
||||
isRequestUrlTransform,
|
||||
isRequestPayloadTransform,
|
||||
} = state;
|
||||
|
||||
const [isContextAreaActive, toggleContextArea] = useState<boolean>(false);
|
||||
|
||||
const contextAreaText = isContextAreaActive
|
||||
? `Hide Sample Context`
|
||||
: `Show Sample Context`;
|
||||
|
||||
const requestUrlTransformText = isRequestUrlTransform
|
||||
? `Remove Request Options Transform`
|
||||
: `Add Request Options Transform`;
|
||||
|
||||
const requestPayloadTransformText = isRequestPayloadTransform
|
||||
? `Remove Payload Transform`
|
||||
: `Add Payload Transform`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold mb-sm flex items-center">
|
||||
Configure REST Connectors
|
||||
</h2>
|
||||
|
||||
<div className="mb-lg">
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Sample Context
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 mb-sm">
|
||||
Add sample env vars and session vars for testing the connector
|
||||
</p>
|
||||
<Button
|
||||
color="white"
|
||||
size="sm"
|
||||
data-test="toggle-context-area"
|
||||
onClick={() => {
|
||||
toggleContextArea(!isContextAreaActive);
|
||||
}}
|
||||
>
|
||||
{!isContextAreaActive ? <AddIcon /> : null}
|
||||
{contextAreaText}
|
||||
</Button>
|
||||
|
||||
{isContextAreaActive ? (
|
||||
<SampleContextTransforms
|
||||
transformationType={transformationType}
|
||||
envVars={envVars}
|
||||
sessionVars={sessionVars}
|
||||
envVarsOnChange={envVarsOnChange}
|
||||
sessionVarsOnChange={sessionVarsOnChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mb-lg">
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Change Request Options
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 mb-sm">
|
||||
Change the method and URL to adapt to your API's expected format.
|
||||
</p>
|
||||
<Button
|
||||
color="white"
|
||||
size="sm"
|
||||
data-test="toggle-request-transform"
|
||||
onClick={() => {
|
||||
requestUrlTransformOnChange(!isRequestUrlTransform);
|
||||
resetSampleInput();
|
||||
}}
|
||||
>
|
||||
{!isRequestUrlTransform ? <AddIcon /> : null}
|
||||
{requestUrlTransformText}
|
||||
</Button>
|
||||
|
||||
{isRequestUrlTransform ? (
|
||||
<RequestOptionsTransforms
|
||||
requestMethod={requestMethod}
|
||||
requestUrl={requestUrl}
|
||||
requestUrlError={requestUrlError}
|
||||
requestUrlPreview={requestUrlPreview}
|
||||
requestQueryParams={requestQueryParams}
|
||||
requestAddHeaders={requestAddHeaders}
|
||||
requestMethodOnChange={requestMethodOnChange}
|
||||
requestUrlOnChange={requestUrlOnChange}
|
||||
requestQueryParamsOnChange={requestQueryParamsOnChange}
|
||||
requestAddHeadersOnChange={requestAddHeadersOnChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mb-lg">
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Change Payload
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 mb-sm">
|
||||
Change the payload to adapt to your API's expected format.
|
||||
</p>
|
||||
<Button
|
||||
color="white"
|
||||
size="sm"
|
||||
data-test="toggle-payload-transform"
|
||||
onClick={() => {
|
||||
requestPayloadTransformOnChange(!isRequestPayloadTransform);
|
||||
resetSampleInput();
|
||||
}}
|
||||
>
|
||||
{!isRequestPayloadTransform ? <AddIcon /> : null}
|
||||
{requestPayloadTransformText}
|
||||
</Button>
|
||||
{isRequestPayloadTransform ? (
|
||||
<PayloadOptionsTransforms
|
||||
transformationType={transformationType}
|
||||
requestBody={requestBody}
|
||||
requestBodyError={requestBodyError}
|
||||
requestSampleInput={requestSampleInput}
|
||||
requestTransformedBody={requestTransformedBody}
|
||||
resetSampleInput={resetSampleInput}
|
||||
requestBodyOnChange={requestBodyOnChange}
|
||||
requestSampleInputOnChange={requestSampleInputOnChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigureTransformation;
|
@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
|
||||
import AceEditor from '../../AceEditor/BaseEditor';
|
||||
import CrossIcon from '../../Icons/Cross';
|
||||
import { isConsoleError, isJsonString } from '../../utils/jsUtils';
|
||||
import { Nullable } from '../../utils/tsUtils';
|
||||
|
||||
type JsonEditorProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
fontSize?: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
};
|
||||
|
||||
const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
fontSize,
|
||||
height,
|
||||
width,
|
||||
}) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const [error, setError] = useState<Nullable<string>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const onChangeHandler = (val: string) => {
|
||||
setLocalValue(val);
|
||||
setError(null);
|
||||
try {
|
||||
JSON.parse(val);
|
||||
} catch (e) {
|
||||
if (isConsoleError(e)) {
|
||||
setError(e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
if (isJsonString(localValue)) {
|
||||
onChange(localValue);
|
||||
}
|
||||
},
|
||||
// large debounce as it will trigger calculation of autocompleter for Request body (and will trigger validate api)
|
||||
3500,
|
||||
[localValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<div className="mb-sm">
|
||||
<CrossIcon />
|
||||
<span className="text-red-500 ml-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
<AceEditor
|
||||
name="json-editor"
|
||||
mode="json"
|
||||
value={localValue}
|
||||
onChange={onChangeHandler}
|
||||
fontSize={fontSize || '12px'}
|
||||
height={height || '200px'}
|
||||
width={width || '100%'}
|
||||
showPrintMargin={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonEditor;
|
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
buttonShadow,
|
||||
inputStyles,
|
||||
focusYellowRing,
|
||||
addPlaceholderValue,
|
||||
} from '../utils';
|
||||
import { KeyValuePair } from '../stateDefaults';
|
||||
|
||||
interface KeyValueInputProps {
|
||||
pairs: KeyValuePair[];
|
||||
setPairs: (h: KeyValuePair[]) => void;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
const KeyValueInput: React.FC<KeyValueInputProps> = ({
|
||||
pairs,
|
||||
setPairs,
|
||||
testId,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{pairs.map((pair, i) => {
|
||||
const setPairKey = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newPairs = [...pairs];
|
||||
newPairs[i].name = e.target.value;
|
||||
addPlaceholderValue(newPairs);
|
||||
setPairs(newPairs);
|
||||
};
|
||||
|
||||
const setPairValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newPairs = [...pairs];
|
||||
newPairs[i].value = e.target.value;
|
||||
addPlaceholderValue(newPairs);
|
||||
setPairs(newPairs);
|
||||
};
|
||||
|
||||
const removePair = () => {
|
||||
const newPairs = [...pairs];
|
||||
setPairs([...newPairs.slice(0, i), ...newPairs.slice(i + 1)]);
|
||||
};
|
||||
|
||||
const { name, value } = pair;
|
||||
return (
|
||||
<React.Fragment key={`pair-${i.toString()}`}>
|
||||
<div>
|
||||
<input
|
||||
value={name}
|
||||
onChange={setPairKey}
|
||||
type="text"
|
||||
name="table_name"
|
||||
id="table_name"
|
||||
className={`w-full ${inputStyles}`}
|
||||
placeholder="Key..."
|
||||
data-test={`transform-${testId}-kv-key-${i.toString()}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
value={value}
|
||||
onChange={setPairValue}
|
||||
type="text"
|
||||
name="table_name"
|
||||
id="table_name"
|
||||
className={`w-full ${inputStyles}`}
|
||||
placeholder="Value..."
|
||||
data-test={`transform-${testId}-kv-value-${i.toString()}`}
|
||||
/>
|
||||
</div>
|
||||
{i < pairs.length - 1 ? (
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={removePair}
|
||||
data-test={`transform-${testId}-kv-remove-button-${i.toString()}`}
|
||||
className={`flex items-center text-sm font-medium h-btn px-3 ${buttonShadow} ${focusYellowRing}`}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-end" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyValueInput;
|
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import KnowMore from '../../KnowMoreLink/KnowMore';
|
||||
import { sidebarNumberStyles } from '../utils';
|
||||
|
||||
interface NumberedSidebarProps {
|
||||
title: string;
|
||||
description?: string | JSX.Element;
|
||||
number?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const NumberedSidebar: React.FC<NumberedSidebarProps> = ({
|
||||
title,
|
||||
description,
|
||||
number,
|
||||
url,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{number ? <div className={sidebarNumberStyles}>{number}</div> : null}
|
||||
<div className="flex items-center mb-sm">
|
||||
<div>
|
||||
<label className="flex items-center block text-gray-600 font-medium">
|
||||
{title}
|
||||
{url ? <KnowMore url={url} /> : null}
|
||||
</label>
|
||||
{description ? (
|
||||
<p className="text-sm text-gray-600">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NumberedSidebar;
|
@ -0,0 +1,139 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import KeyValueInput from './KeyValueInput';
|
||||
import { Nullable } from '../../utils/tsUtils';
|
||||
import { editorDebounceTime, fixedInputStyles, inputStyles } from '../utils';
|
||||
import { useDebouncedEffect } from '../../../../hooks/useDebounceEffect';
|
||||
import CrossIcon from '../../Icons/Cross';
|
||||
import { KeyValuePair } from '../stateDefaults';
|
||||
|
||||
type RequestUrlEditorProps = {
|
||||
requestUrl: string;
|
||||
requestUrlError: string;
|
||||
requestUrlPreview: string;
|
||||
requestQueryParams: KeyValuePair[];
|
||||
requestUrlOnChange: (requestUrl: string) => void;
|
||||
requestQueryParamsOnChange: (requestQueryParams: KeyValuePair[]) => void;
|
||||
};
|
||||
|
||||
const RequestUrlEditor: React.FC<RequestUrlEditorProps> = ({
|
||||
requestUrl,
|
||||
requestUrlError,
|
||||
requestUrlPreview,
|
||||
requestQueryParams,
|
||||
requestUrlOnChange,
|
||||
requestQueryParamsOnChange,
|
||||
}) => {
|
||||
const [localUrl, setLocalUrl] = useState<string>(requestUrl);
|
||||
const [localError, setLocalError] =
|
||||
useState<Nullable<string>>(requestUrlError);
|
||||
const [localQueryParams, setLocalQueryParams] =
|
||||
useState<KeyValuePair[]>(requestQueryParams);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalUrl(requestUrl);
|
||||
}, [requestUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestUrlError) {
|
||||
setLocalError(requestUrlError);
|
||||
} else {
|
||||
setLocalError(null);
|
||||
}
|
||||
}, [requestUrlError]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalQueryParams(requestQueryParams);
|
||||
}, [requestQueryParams]);
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
requestUrlOnChange(localUrl);
|
||||
},
|
||||
editorDebounceTime,
|
||||
[localUrl]
|
||||
);
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
requestQueryParamsOnChange(localQueryParams);
|
||||
},
|
||||
editorDebounceTime,
|
||||
[localQueryParams]
|
||||
);
|
||||
|
||||
const urlOnChangeHandler = (val: string) => {
|
||||
setLocalUrl(val);
|
||||
};
|
||||
|
||||
const queryParamsOnChangeHandler = (val: KeyValuePair[]) => {
|
||||
setLocalQueryParams(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<div className="col-span-2">
|
||||
<div className="flex shadow-sm rounded w-full">
|
||||
<span className={fixedInputStyles}>
|
||||
{{$base_url}}
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
name="request_url"
|
||||
id="request_url"
|
||||
className={`w-full ${inputStyles}`}
|
||||
placeholder="URL Template (Optional)..."
|
||||
value={localUrl}
|
||||
onChange={e => urlOnChangeHandler(e.target.value)}
|
||||
data-test="transform-requestUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-3">
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Query Params
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">Value</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<KeyValueInput
|
||||
pairs={localQueryParams}
|
||||
setPairs={queryParamsOnChangeHandler}
|
||||
testId="query-params"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<div className="col-span-2">
|
||||
<label
|
||||
htmlFor="request_url_preview"
|
||||
className="block text-gray-600 font-medium mb-xs"
|
||||
>
|
||||
Preview
|
||||
</label>
|
||||
<input
|
||||
disabled
|
||||
type="text"
|
||||
name="request_url_preview"
|
||||
id="request_url_preview"
|
||||
className="w-full block cursor-not-allowed rounded border-gray-200 bg-gray-200"
|
||||
data-test="transform-requestUrl-preview"
|
||||
value={requestUrlPreview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{localError ? (
|
||||
<div className="mb-sm" data-test="transform-requestUrl-error">
|
||||
<CrossIcon />
|
||||
<span className="text-red-500 ml-sm">{localError}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestUrlEditor;
|
@ -0,0 +1,95 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import AceEditor from '../../AceEditor/BaseEditor';
|
||||
import { Nullable } from '../../utils/tsUtils';
|
||||
import { getAceCompleterFromString, editorDebounceTime } from '../utils';
|
||||
import { useDebouncedEffect } from '../../../../hooks/useDebounceEffect';
|
||||
import CrossIcon from '../../Icons/Cross';
|
||||
import { RequestTransformStateBody } from '../stateDefaults';
|
||||
|
||||
type TemplateEditorProps = {
|
||||
requestBody: RequestTransformStateBody;
|
||||
requestBodyError: string;
|
||||
requestSampleInput: string;
|
||||
requestBodyOnChange: (requestBody: RequestTransformStateBody) => void;
|
||||
height?: string;
|
||||
width?: string;
|
||||
};
|
||||
|
||||
const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||||
requestBody,
|
||||
requestBodyError,
|
||||
requestSampleInput,
|
||||
requestBodyOnChange,
|
||||
height,
|
||||
width,
|
||||
}) => {
|
||||
const editorRef = useRef<any>();
|
||||
const [localValue, setLocalValue] = useState<string>(
|
||||
requestBody.template ?? ''
|
||||
);
|
||||
const [localError, setLocalError] =
|
||||
useState<Nullable<string>>(requestBodyError);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(requestBody.template ?? '');
|
||||
}, [requestBody]);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestBodyError) {
|
||||
setLocalError(requestBodyError);
|
||||
} else {
|
||||
setLocalError(null);
|
||||
}
|
||||
}, [requestBodyError]);
|
||||
|
||||
useEffect(() => {
|
||||
const sampleInputWordCompleter =
|
||||
getAceCompleterFromString(requestSampleInput);
|
||||
if (
|
||||
editorRef?.current?.editor?.completers &&
|
||||
Array.isArray(editorRef?.current?.editor?.completers)
|
||||
) {
|
||||
editorRef.current.editor.completers = [sampleInputWordCompleter];
|
||||
}
|
||||
}, [requestSampleInput]);
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
requestBodyOnChange({ ...requestBody, template: localValue });
|
||||
},
|
||||
editorDebounceTime,
|
||||
[localValue]
|
||||
);
|
||||
|
||||
const onChangeHandler = (val: string) => {
|
||||
setLocalValue(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{localError && (
|
||||
<div className="mb-sm" data-test="transform-requestBody-error">
|
||||
<CrossIcon />
|
||||
<span className="text-red-500 ml-sm">{localError}</span>
|
||||
</div>
|
||||
)}
|
||||
<AceEditor
|
||||
name="temp-editor"
|
||||
mode="json"
|
||||
editorRef={editorRef}
|
||||
value={localValue}
|
||||
onChange={onChangeHandler}
|
||||
showPrintMargin={false}
|
||||
height={height || '200px'}
|
||||
width={width || '100%'}
|
||||
fontSize="12px"
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateEditor;
|
@ -0,0 +1,216 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { RequestTransformBodyActions } from '@/metadata/types';
|
||||
import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
|
||||
import { FaExclamationCircle } from 'react-icons/fa';
|
||||
import CrossIcon from '../Icons/Cross';
|
||||
import TemplateEditor from './CustomEditors/TemplateEditor';
|
||||
import JsonEditor from './CustomEditors/JsonEditor';
|
||||
import AceEditor from '../AceEditor/BaseEditor';
|
||||
import ResetIcon from '../Icons/Reset';
|
||||
import {
|
||||
buttonShadow,
|
||||
capitaliseFirstLetter,
|
||||
editorDebounceTime,
|
||||
focusYellowRing,
|
||||
inputStyles,
|
||||
} from './utils';
|
||||
import NumberedSidebar from './CustomEditors/NumberedSidebar';
|
||||
import {
|
||||
KeyValuePair,
|
||||
RequestTransformStateBody,
|
||||
TransformationType,
|
||||
} from './stateDefaults';
|
||||
import KeyValueInput from './CustomEditors/KeyValueInput';
|
||||
import { isEmpty } from '../utils/jsUtils';
|
||||
import { requestBodyActionState } from './requestTransformState';
|
||||
|
||||
type PayloadOptionsTransformsProps = {
|
||||
transformationType: TransformationType;
|
||||
requestBody: RequestTransformStateBody;
|
||||
requestBodyError: string;
|
||||
requestSampleInput: string;
|
||||
requestTransformedBody: string;
|
||||
resetSampleInput: () => void;
|
||||
requestBodyOnChange: (requestBody: RequestTransformStateBody) => void;
|
||||
requestSampleInputOnChange: (requestSampleInput: string) => void;
|
||||
};
|
||||
|
||||
const PayloadOptionsTransforms: React.FC<PayloadOptionsTransformsProps> = ({
|
||||
transformationType,
|
||||
requestBody,
|
||||
requestBodyError,
|
||||
requestSampleInput,
|
||||
requestTransformedBody,
|
||||
resetSampleInput,
|
||||
requestBodyOnChange,
|
||||
requestSampleInputOnChange,
|
||||
}) => {
|
||||
const editorRef = useRef<any>();
|
||||
const requestBodyTypeOptions = [
|
||||
{
|
||||
value: requestBodyActionState.remove,
|
||||
text: 'disabled',
|
||||
},
|
||||
{
|
||||
value: requestBodyActionState.transformApplicationJson,
|
||||
text: 'application/json',
|
||||
},
|
||||
{
|
||||
value: requestBodyActionState.transformFormUrlEncoded,
|
||||
text: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
];
|
||||
const [localFormElements, setLocalFormElements] = React.useState<
|
||||
KeyValuePair[]
|
||||
>(requestBody.form_template ?? [{ name: '', value: '' }]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLocalFormElements(
|
||||
requestBody.form_template ?? [{ name: '', value: '' }]
|
||||
);
|
||||
}, [requestBody]);
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
requestBodyOnChange({ ...requestBody, form_template: localFormElements });
|
||||
},
|
||||
editorDebounceTime,
|
||||
[localFormElements]
|
||||
);
|
||||
|
||||
if (editorRef?.current?.editor?.renderer?.$cursorLayer?.element?.style) {
|
||||
editorRef.current.editor.renderer.$cursorLayer.element.style.display =
|
||||
'none';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="m-md pl-lg pr-sm border-l border-l-gray-400"
|
||||
data-cy="Change Payload"
|
||||
>
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Sample Input"
|
||||
description={`Sample input defined by your ${capitaliseFirstLetter(
|
||||
transformationType
|
||||
)} Defintion.`}
|
||||
number="1"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`ml-auto inline-flex items-center text-sm font-medium h-btnsm px-sm mr-sm ${buttonShadow} ${focusYellowRing}`}
|
||||
onClick={() => {
|
||||
resetSampleInput();
|
||||
}}
|
||||
>
|
||||
<ResetIcon />
|
||||
Reset
|
||||
</button>
|
||||
</NumberedSidebar>
|
||||
<JsonEditor
|
||||
value={requestSampleInput}
|
||||
onChange={requestSampleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Configure Request Body"
|
||||
description={
|
||||
<span>
|
||||
The template which will transform your request body into the
|
||||
required specification. You can use{' '}
|
||||
<code className="text-xs">{{$body}}</code> to
|
||||
access the original request body
|
||||
</span>
|
||||
}
|
||||
number="2"
|
||||
url="https://hasura.io/docs/latest/graphql/core/actions/transforms.html#request-body"
|
||||
>
|
||||
<select
|
||||
className={`ml-auto ${inputStyles}`}
|
||||
value={requestBody.action}
|
||||
onChange={e =>
|
||||
requestBodyOnChange({
|
||||
...requestBody,
|
||||
action: e.target.value as RequestTransformBodyActions,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option disabled>Request Body Type</option>
|
||||
{requestBodyTypeOptions.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</NumberedSidebar>
|
||||
{requestBody.action ===
|
||||
requestBodyActionState.transformApplicationJson ? (
|
||||
<TemplateEditor
|
||||
requestBody={requestBody}
|
||||
requestBodyError={requestBodyError}
|
||||
requestSampleInput={requestSampleInput}
|
||||
requestBodyOnChange={requestBodyOnChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{requestBody.action ===
|
||||
requestBodyActionState.transformFormUrlEncoded ? (
|
||||
<>
|
||||
{!isEmpty(requestBodyError) && (
|
||||
<div className="mb-sm" data-test="transform-requestBody-error">
|
||||
<CrossIcon />
|
||||
<span className="text-red-500 ml-sm">{requestBodyError}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<KeyValueInput
|
||||
pairs={localFormElements}
|
||||
setPairs={setLocalFormElements}
|
||||
testId="add-url-encoded-body"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{requestBody.action === requestBodyActionState.remove ? (
|
||||
<div className="flex items-center text-gray-600 bg-gray-200 border border-gray-400 text-sm rounded p-sm">
|
||||
<FaExclamationCircle className="mr-sm" />
|
||||
The request body is disabled. No request body will be sent with this
|
||||
{transformationType}. Enable the request body to modify your request
|
||||
transformation.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{requestBody.action !== requestBodyActionState.remove ? (
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Transformed Request Body"
|
||||
description="Sample request body to be delivered based on your input and
|
||||
transformation template."
|
||||
number="3"
|
||||
/>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
editorRef={editorRef}
|
||||
value={requestTransformedBody}
|
||||
showPrintMargin={false}
|
||||
highlightActiveLine={false}
|
||||
height="200px"
|
||||
width="100%"
|
||||
fontSize="12px"
|
||||
style={{ background: '#e2e8f0' }}
|
||||
setOptions={{
|
||||
highlightGutterLine: false,
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayloadOptionsTransforms;
|
@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { RequestTransformMethod } from '../../../metadata/types';
|
||||
import { KeyValuePair } from './stateDefaults';
|
||||
import { focusYellowRing } from './utils';
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
import RequestUrlEditor from './CustomEditors/RequestUrlEditor';
|
||||
import KeyValueInput from './CustomEditors/KeyValueInput';
|
||||
import NumberedSidebar from './CustomEditors/NumberedSidebar';
|
||||
|
||||
type RequestOptionsTransformsProps = {
|
||||
requestMethod: Nullable<RequestTransformMethod>;
|
||||
requestUrl: string;
|
||||
requestUrlError: string;
|
||||
requestUrlPreview: string;
|
||||
requestQueryParams: KeyValuePair[];
|
||||
requestAddHeaders: KeyValuePair[];
|
||||
requestMethodOnChange: (requestMethod: RequestTransformMethod) => void;
|
||||
requestUrlOnChange: (requestUrl: string) => void;
|
||||
requestQueryParamsOnChange: (requestQueryParams: KeyValuePair[]) => void;
|
||||
requestAddHeadersOnChange: (requestAddHeaders: KeyValuePair[]) => void;
|
||||
};
|
||||
|
||||
const RequestOptionsTransforms: React.FC<RequestOptionsTransformsProps> = ({
|
||||
requestMethod,
|
||||
requestUrl,
|
||||
requestUrlError,
|
||||
requestUrlPreview,
|
||||
requestQueryParams,
|
||||
requestAddHeaders,
|
||||
requestMethodOnChange,
|
||||
requestUrlOnChange,
|
||||
requestQueryParamsOnChange,
|
||||
requestAddHeadersOnChange,
|
||||
}) => {
|
||||
const showRequestHeaders = false;
|
||||
const requestMethodOptions: RequestTransformMethod[] = [
|
||||
'GET',
|
||||
'POST',
|
||||
'PUT',
|
||||
'PATCH',
|
||||
'DELETE',
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="m-md pl-lg pr-sm border-l border-l-gray-400"
|
||||
data-cy="Change Request Options"
|
||||
>
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Request Method"
|
||||
number="1"
|
||||
url="https://hasura.io/docs/latest/graphql/core/actions/transforms.html#method"
|
||||
/>
|
||||
{requestMethodOptions.map(method => (
|
||||
<div key={method} className="inline-flex items-center mr-md">
|
||||
<input
|
||||
id={method}
|
||||
name={method}
|
||||
type="radio"
|
||||
value={method}
|
||||
checked={requestMethod === method}
|
||||
onChange={() => requestMethodOnChange(method)}
|
||||
className={`mr-sm border-gray-400 ${focusYellowRing}`}
|
||||
/>
|
||||
<label
|
||||
className="ml-sm"
|
||||
htmlFor={method}
|
||||
data-test={`transform-${method}`}
|
||||
>
|
||||
{method}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Request URL Template"
|
||||
number="2"
|
||||
url="https://hasura.io/docs/latest/graphql/core/actions/transforms.html#url"
|
||||
/>
|
||||
<RequestUrlEditor
|
||||
requestUrl={requestUrl}
|
||||
requestUrlError={requestUrlError}
|
||||
requestUrlPreview={requestUrlPreview}
|
||||
requestQueryParams={requestQueryParams}
|
||||
requestUrlOnChange={requestUrlOnChange}
|
||||
requestQueryParamsOnChange={requestQueryParamsOnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showRequestHeaders ? (
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Configure Headers"
|
||||
description="Transform your request header into the required specification."
|
||||
number="3"
|
||||
url="https://hasura.io/docs/latest/graphql/core/actions/transforms.html#request-headers"
|
||||
/>
|
||||
<div className="grid gap-3 grid-cols-3">
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Add or Transform Header Key
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Value
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<KeyValueInput
|
||||
pairs={requestAddHeaders}
|
||||
setPairs={requestAddHeadersOnChange}
|
||||
testId="add-headers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestOptionsTransforms;
|
@ -0,0 +1,128 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
|
||||
import { KeyValuePair, TransformationType } from './stateDefaults';
|
||||
import KeyValueInput from './CustomEditors/KeyValueInput';
|
||||
import NumberedSidebar from './CustomEditors/NumberedSidebar';
|
||||
import { editorDebounceTime, setEnvVarsToLS } from './utils';
|
||||
|
||||
type SampleContextTransformsProps = {
|
||||
transformationType: TransformationType;
|
||||
envVars: KeyValuePair[];
|
||||
sessionVars: KeyValuePair[];
|
||||
envVarsOnChange: (envVars: KeyValuePair[]) => void;
|
||||
sessionVarsOnChange: (sessionVars: KeyValuePair[]) => void;
|
||||
};
|
||||
|
||||
const SampleContextTransforms: React.FC<SampleContextTransformsProps> = ({
|
||||
transformationType,
|
||||
envVars,
|
||||
sessionVars,
|
||||
envVarsOnChange,
|
||||
sessionVarsOnChange,
|
||||
}) => {
|
||||
const [localEnvVars, setLocalEnvVars] = useState<KeyValuePair[]>(envVars);
|
||||
const [localSessionVars, setLocalSessionVars] =
|
||||
useState<KeyValuePair[]>(sessionVars);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalEnvVars(envVars);
|
||||
}, [envVars]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSessionVars(sessionVars);
|
||||
}, [sessionVars]);
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
envVarsOnChange(localEnvVars);
|
||||
setEnvVarsToLS(localEnvVars);
|
||||
},
|
||||
editorDebounceTime,
|
||||
[localEnvVars]
|
||||
);
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
sessionVarsOnChange(localSessionVars);
|
||||
},
|
||||
editorDebounceTime,
|
||||
[localSessionVars]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="m-md pl-lg pr-sm border-l border-l-gray-400">
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Sample Env Variables"
|
||||
description={
|
||||
<span>
|
||||
Enter a sample input for your provided env variables.
|
||||
<br />
|
||||
e.g. the sample value for {transformationType.toUpperCase()}
|
||||
_BASE_URL
|
||||
</span>
|
||||
}
|
||||
number="1"
|
||||
/>
|
||||
<div className="grid gap-3 grid-cols-3">
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Env Variables
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Value
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<KeyValueInput
|
||||
pairs={localEnvVars}
|
||||
setPairs={ev => {
|
||||
setLocalEnvVars(ev);
|
||||
}}
|
||||
testId="env-vars"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Sample Session Variables"
|
||||
description={
|
||||
<span>
|
||||
Enter a sample input for your provided session variables.
|
||||
<br />
|
||||
e.g. the sample value for x-hasura-user-id
|
||||
</span>
|
||||
}
|
||||
number="2"
|
||||
/>
|
||||
<div className="grid gap-3 grid-cols-3">
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Session Variables
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Value
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<KeyValueInput
|
||||
pairs={localSessionVars}
|
||||
setPairs={sv => {
|
||||
setLocalSessionVars(sv);
|
||||
}}
|
||||
testId="session-vars"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SampleContextTransforms;
|
@ -0,0 +1,313 @@
|
||||
import {
|
||||
RequestTransformMethod,
|
||||
RequestTransformContentType,
|
||||
RequestTransformBodyActions,
|
||||
} from '../../../metadata/types';
|
||||
import {
|
||||
SET_ENV_VARS,
|
||||
SET_SESSION_VARS,
|
||||
SET_REQUEST_METHOD,
|
||||
SET_REQUEST_URL,
|
||||
SET_REQUEST_URL_ERROR,
|
||||
SET_REQUEST_URL_PREVIEW,
|
||||
SET_REQUEST_QUERY_PARAMS,
|
||||
SET_REQUEST_ADD_HEADERS,
|
||||
SET_REQUEST_BODY,
|
||||
SET_REQUEST_BODY_ERROR,
|
||||
SET_REQUEST_SAMPLE_INPUT,
|
||||
SET_REQUEST_TRANSFORMED_BODY,
|
||||
SET_REQUEST_CONTENT_TYPE,
|
||||
SET_REQUEST_URL_TRANSFORM,
|
||||
SET_REQUEST_PAYLOAD_TRANSFORM,
|
||||
SET_REQUEST_TRANSFORM_STATE,
|
||||
SetEnvVars,
|
||||
SetSessionVars,
|
||||
SetRequestMethod,
|
||||
SetRequestUrl,
|
||||
SetRequestUrlError,
|
||||
SetRequestUrlPreview,
|
||||
SetRequestQueryParams,
|
||||
SetRequestAddHeaders,
|
||||
SetRequestBody,
|
||||
SetRequestBodyError,
|
||||
SetRequestSampleInput,
|
||||
SetRequestTransformedBody,
|
||||
SetRequestTransformState,
|
||||
SetRequestContentType,
|
||||
SetRequestUrlTransform,
|
||||
SetRequestPayloadTransform,
|
||||
RequestTransformEvents,
|
||||
defaultActionRequestSampleInput,
|
||||
defaultActionRequestBody,
|
||||
defaultActionRequestSamplePayload,
|
||||
defaultRequestContentType,
|
||||
RequestTransformState,
|
||||
KeyValuePair,
|
||||
defaultEventRequestBody,
|
||||
defaultEventRequestSampleInput,
|
||||
RequestTransformStateBody,
|
||||
} from './stateDefaults';
|
||||
import { getSessionVarsFromLS, getEnvVarsFromLS } from './utils';
|
||||
|
||||
export const setEnvVars = (envVars: KeyValuePair[]): SetEnvVars => ({
|
||||
type: SET_ENV_VARS,
|
||||
envVars,
|
||||
});
|
||||
|
||||
export const setSessionVars = (
|
||||
sessionVars: KeyValuePair[]
|
||||
): SetSessionVars => ({
|
||||
type: SET_SESSION_VARS,
|
||||
sessionVars,
|
||||
});
|
||||
|
||||
export const setRequestMethod = (
|
||||
requestMethod: RequestTransformMethod
|
||||
): SetRequestMethod => ({
|
||||
type: SET_REQUEST_METHOD,
|
||||
requestMethod,
|
||||
});
|
||||
|
||||
export const setRequestUrl = (requestUrl: string): SetRequestUrl => ({
|
||||
type: SET_REQUEST_URL,
|
||||
requestUrl,
|
||||
});
|
||||
|
||||
export const setRequestUrlError = (
|
||||
requestUrlError: string
|
||||
): SetRequestUrlError => ({
|
||||
type: SET_REQUEST_URL_ERROR,
|
||||
requestUrlError,
|
||||
});
|
||||
|
||||
export const setRequestUrlPreview = (
|
||||
requestUrlPreview: string
|
||||
): SetRequestUrlPreview => ({
|
||||
type: SET_REQUEST_URL_PREVIEW,
|
||||
requestUrlPreview,
|
||||
});
|
||||
|
||||
export const setRequestQueryParams = (
|
||||
requestQueryParams: KeyValuePair[]
|
||||
): SetRequestQueryParams => ({
|
||||
type: SET_REQUEST_QUERY_PARAMS,
|
||||
requestQueryParams,
|
||||
});
|
||||
|
||||
export const setRequestAddHeaders = (
|
||||
requestAddHeaders: KeyValuePair[]
|
||||
): SetRequestAddHeaders => ({
|
||||
type: SET_REQUEST_ADD_HEADERS,
|
||||
requestAddHeaders,
|
||||
});
|
||||
|
||||
export const setRequestBody = (
|
||||
requestBody: RequestTransformStateBody
|
||||
): SetRequestBody => ({
|
||||
type: SET_REQUEST_BODY,
|
||||
requestBody,
|
||||
});
|
||||
|
||||
export const setRequestBodyError = (
|
||||
requestBodyError: string
|
||||
): SetRequestBodyError => ({
|
||||
type: SET_REQUEST_BODY_ERROR,
|
||||
requestBodyError,
|
||||
});
|
||||
|
||||
export const setRequestSampleInput = (
|
||||
requestSampleInput: string
|
||||
): SetRequestSampleInput => ({
|
||||
type: SET_REQUEST_SAMPLE_INPUT,
|
||||
requestSampleInput,
|
||||
});
|
||||
|
||||
export const setRequestTransformedBody = (
|
||||
requestTransformedBody: string
|
||||
): SetRequestTransformedBody => ({
|
||||
type: SET_REQUEST_TRANSFORMED_BODY,
|
||||
requestTransformedBody,
|
||||
});
|
||||
|
||||
export const setRequestContentType = (
|
||||
requestContentType: RequestTransformContentType
|
||||
): SetRequestContentType => ({
|
||||
type: SET_REQUEST_CONTENT_TYPE,
|
||||
requestContentType,
|
||||
});
|
||||
|
||||
export const setRequestUrlTransform = (
|
||||
isRequestUrlTransform: boolean
|
||||
): SetRequestUrlTransform => ({
|
||||
type: SET_REQUEST_URL_TRANSFORM,
|
||||
isRequestUrlTransform,
|
||||
});
|
||||
|
||||
export const setRequestPayloadTransform = (
|
||||
isRequestPayloadTransform: boolean
|
||||
): SetRequestPayloadTransform => ({
|
||||
type: SET_REQUEST_PAYLOAD_TRANSFORM,
|
||||
isRequestPayloadTransform,
|
||||
});
|
||||
|
||||
export const setRequestTransformState = (
|
||||
newState: RequestTransformState
|
||||
): SetRequestTransformState => ({
|
||||
type: SET_REQUEST_TRANSFORM_STATE,
|
||||
newState,
|
||||
});
|
||||
|
||||
const currentVersion = 2;
|
||||
|
||||
export const requestBodyActionState = {
|
||||
remove: 'remove' as RequestTransformBodyActions,
|
||||
transformApplicationJson: 'transform' as RequestTransformBodyActions,
|
||||
transformFormUrlEncoded:
|
||||
'x_www_form_urlencoded' as RequestTransformBodyActions,
|
||||
};
|
||||
|
||||
export const requestTransformState: RequestTransformState = {
|
||||
version: currentVersion,
|
||||
envVars: [],
|
||||
sessionVars: [],
|
||||
requestMethod: null,
|
||||
requestUrl: '',
|
||||
requestUrlError: '',
|
||||
requestUrlPreview: '',
|
||||
requestQueryParams: [],
|
||||
requestAddHeaders: [],
|
||||
requestBody: { action: requestBodyActionState.transformApplicationJson },
|
||||
requestBodyError: '',
|
||||
requestSampleInput: '',
|
||||
requestTransformedBody: '',
|
||||
requestContentType: defaultRequestContentType,
|
||||
isRequestUrlTransform: false,
|
||||
isRequestPayloadTransform: false,
|
||||
templatingEngine: 'Kriti',
|
||||
};
|
||||
|
||||
export const getActionRequestTransformDefaultState =
|
||||
(): RequestTransformState => {
|
||||
return {
|
||||
...requestTransformState,
|
||||
envVars: getEnvVarsFromLS(),
|
||||
sessionVars: getSessionVarsFromLS(),
|
||||
requestQueryParams: [{ name: '', value: '' }],
|
||||
requestAddHeaders: [{ name: '', value: '' }],
|
||||
requestBody: {
|
||||
action: requestBodyActionState.transformApplicationJson,
|
||||
template: defaultActionRequestBody,
|
||||
form_template: [{ name: 'name', value: '{{$body.action.name}}' }],
|
||||
},
|
||||
requestSampleInput: defaultActionRequestSampleInput,
|
||||
};
|
||||
};
|
||||
|
||||
export const getEventRequestTransformDefaultState =
|
||||
(): RequestTransformState => {
|
||||
return {
|
||||
...requestTransformState,
|
||||
envVars: getEnvVarsFromLS(),
|
||||
sessionVars: getSessionVarsFromLS(),
|
||||
requestQueryParams: [{ name: '', value: '' }],
|
||||
requestAddHeaders: [{ name: '', value: '' }],
|
||||
requestBody: {
|
||||
action: requestBodyActionState.transformApplicationJson,
|
||||
template: defaultEventRequestBody,
|
||||
form_template: [{ name: 'name', value: '{{$body.table.name}}' }],
|
||||
},
|
||||
requestSampleInput: defaultEventRequestSampleInput,
|
||||
};
|
||||
};
|
||||
|
||||
export const requestTransformReducer = (
|
||||
state = requestTransformState,
|
||||
action: RequestTransformEvents
|
||||
): RequestTransformState => {
|
||||
switch (action.type) {
|
||||
case SET_ENV_VARS:
|
||||
return {
|
||||
...state,
|
||||
envVars: action.envVars,
|
||||
};
|
||||
case SET_SESSION_VARS:
|
||||
return {
|
||||
...state,
|
||||
sessionVars: action.sessionVars,
|
||||
};
|
||||
case SET_REQUEST_METHOD:
|
||||
return {
|
||||
...state,
|
||||
requestMethod: action.requestMethod,
|
||||
};
|
||||
case SET_REQUEST_URL:
|
||||
return {
|
||||
...state,
|
||||
requestUrl: action.requestUrl,
|
||||
};
|
||||
case SET_REQUEST_URL_ERROR:
|
||||
return {
|
||||
...state,
|
||||
requestUrlError: action.requestUrlError,
|
||||
};
|
||||
case SET_REQUEST_URL_PREVIEW:
|
||||
return {
|
||||
...state,
|
||||
requestUrlPreview: action.requestUrlPreview,
|
||||
};
|
||||
case SET_REQUEST_QUERY_PARAMS:
|
||||
return {
|
||||
...state,
|
||||
requestQueryParams: action.requestQueryParams,
|
||||
};
|
||||
case SET_REQUEST_ADD_HEADERS:
|
||||
return {
|
||||
...state,
|
||||
requestAddHeaders: action.requestAddHeaders,
|
||||
};
|
||||
case SET_REQUEST_BODY:
|
||||
return {
|
||||
...state,
|
||||
requestBody: action.requestBody,
|
||||
};
|
||||
case SET_REQUEST_BODY_ERROR:
|
||||
return {
|
||||
...state,
|
||||
requestBodyError: action.requestBodyError,
|
||||
};
|
||||
case SET_REQUEST_SAMPLE_INPUT:
|
||||
return {
|
||||
...state,
|
||||
requestSampleInput: action.requestSampleInput,
|
||||
};
|
||||
case SET_REQUEST_TRANSFORMED_BODY:
|
||||
return {
|
||||
...state,
|
||||
requestTransformedBody: action.requestTransformedBody,
|
||||
};
|
||||
case SET_REQUEST_CONTENT_TYPE:
|
||||
return {
|
||||
...state,
|
||||
requestContentType: action.requestContentType,
|
||||
requestTransformedBody: defaultActionRequestSamplePayload(
|
||||
action.requestContentType
|
||||
),
|
||||
};
|
||||
case SET_REQUEST_URL_TRANSFORM:
|
||||
return {
|
||||
...state,
|
||||
isRequestUrlTransform: action.isRequestUrlTransform,
|
||||
};
|
||||
case SET_REQUEST_PAYLOAD_TRANSFORM:
|
||||
return {
|
||||
...state,
|
||||
isRequestPayloadTransform: action.isRequestPayloadTransform,
|
||||
};
|
||||
case SET_REQUEST_TRANSFORM_STATE:
|
||||
return {
|
||||
...action.newState,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
@ -0,0 +1,223 @@
|
||||
import { Action as ReduxAction } from 'redux';
|
||||
import { getEventRequestSampleInput } from '@/components/Services/Events/EventTriggers/utils';
|
||||
import { getActionRequestSampleInput } from '../../Services/Actions/Add/utils';
|
||||
import {
|
||||
defaultActionDefSdl,
|
||||
defaultTypesDefSdl,
|
||||
} from '../../Services/Actions/Common/stateDefaults';
|
||||
import {
|
||||
RequestTransformMethod,
|
||||
RequestTransformContentType,
|
||||
RequestTransformTemplateEngine,
|
||||
RequestTransformBody,
|
||||
} from '../../../metadata/types';
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
|
||||
export const SET_ENV_VARS = 'RequestTransform/SET_ENV_VARS';
|
||||
export const SET_SESSION_VARS = 'RequestTransform/SET_SESSION_VARS';
|
||||
export const SET_REQUEST_METHOD = 'RequestTransform/SET_REQUEST_METHOD';
|
||||
export const SET_REQUEST_URL = 'RequestTransform/SET_REQUEST_URL';
|
||||
export const SET_REQUEST_URL_ERROR = 'RequestTransform/SET_REQUEST_URL_ERROR';
|
||||
export const SET_REQUEST_URL_PREVIEW =
|
||||
'RequestTransform/SET_REQUEST_URL_PREVIEW';
|
||||
export const SET_REQUEST_QUERY_PARAMS =
|
||||
'RequestTransform/SET_REQUEST_QUERY_PARAMS';
|
||||
export const SET_REQUEST_ADD_HEADERS =
|
||||
'RequestTransform/SET_REQUEST_ADD_HEADERS';
|
||||
export const SET_REQUEST_BODY = 'RequestTransform/SET_REQUEST_BODY';
|
||||
export const SET_REQUEST_BODY_ERROR = 'RequestTransform/SET_REQUEST_BODY_ERROR';
|
||||
export const SET_REQUEST_SAMPLE_INPUT =
|
||||
'RequestTransform/SET_REQUEST_SAMPLE_INPUT';
|
||||
export const SET_REQUEST_TRANSFORMED_BODY =
|
||||
'RequestTransform/SET_REQUEST_TRANSFORMED_BODY';
|
||||
export const SET_ENABLE_REQUEST_BODY =
|
||||
'RequestTransform/SET_ENABLE_REQUEST_BODY';
|
||||
export const SET_REQUEST_CONTENT_TYPE =
|
||||
'RequestTransform/SET_REQUEST_CONTENT_TYPE';
|
||||
export const SET_REQUEST_URL_TRANSFORM =
|
||||
'RequestTransform/SET_REQUEST_URL_TRANSFORM';
|
||||
export const SET_REQUEST_PAYLOAD_TRANSFORM =
|
||||
'RequestTransform/SET_REQUEST_PAYLOAD_TRANSFORM';
|
||||
export const SET_REQUEST_TRANSFORM_STATE =
|
||||
'RequestTransform/SET_REQUEST_TRANSFORM_STATE';
|
||||
|
||||
export interface SetEnvVars extends ReduxAction {
|
||||
type: typeof SET_ENV_VARS;
|
||||
envVars: KeyValuePair[];
|
||||
}
|
||||
|
||||
export interface SetSessionVars extends ReduxAction {
|
||||
type: typeof SET_SESSION_VARS;
|
||||
sessionVars: KeyValuePair[];
|
||||
}
|
||||
|
||||
export interface SetRequestMethod extends ReduxAction {
|
||||
type: typeof SET_REQUEST_METHOD;
|
||||
requestMethod: RequestTransformMethod;
|
||||
}
|
||||
|
||||
export interface SetRequestUrl extends ReduxAction {
|
||||
type: typeof SET_REQUEST_URL;
|
||||
requestUrl: string;
|
||||
}
|
||||
|
||||
export interface SetRequestUrlError extends ReduxAction {
|
||||
type: typeof SET_REQUEST_URL_ERROR;
|
||||
requestUrlError: string;
|
||||
}
|
||||
|
||||
export interface SetRequestUrlPreview extends ReduxAction {
|
||||
type: typeof SET_REQUEST_URL_PREVIEW;
|
||||
requestUrlPreview: string;
|
||||
}
|
||||
|
||||
export interface SetRequestQueryParams extends ReduxAction {
|
||||
type: typeof SET_REQUEST_QUERY_PARAMS;
|
||||
requestQueryParams: KeyValuePair[];
|
||||
}
|
||||
|
||||
export interface SetRequestAddHeaders extends ReduxAction {
|
||||
type: typeof SET_REQUEST_ADD_HEADERS;
|
||||
requestAddHeaders: KeyValuePair[];
|
||||
}
|
||||
|
||||
export interface SetRequestBody extends ReduxAction {
|
||||
type: typeof SET_REQUEST_BODY;
|
||||
requestBody: RequestTransformStateBody;
|
||||
}
|
||||
|
||||
export interface SetRequestBodyError extends ReduxAction {
|
||||
type: typeof SET_REQUEST_BODY_ERROR;
|
||||
requestBodyError: string;
|
||||
}
|
||||
|
||||
export interface SetRequestSampleInput extends ReduxAction {
|
||||
type: typeof SET_REQUEST_SAMPLE_INPUT;
|
||||
requestSampleInput: string;
|
||||
}
|
||||
|
||||
export interface SetRequestTransformedBody extends ReduxAction {
|
||||
type: typeof SET_REQUEST_TRANSFORMED_BODY;
|
||||
requestTransformedBody: string;
|
||||
}
|
||||
|
||||
export interface SetRequestContentType extends ReduxAction {
|
||||
type: typeof SET_REQUEST_CONTENT_TYPE;
|
||||
requestContentType: RequestTransformContentType;
|
||||
}
|
||||
|
||||
export interface SetRequestUrlTransform extends ReduxAction {
|
||||
type: typeof SET_REQUEST_URL_TRANSFORM;
|
||||
isRequestUrlTransform: boolean;
|
||||
}
|
||||
|
||||
export interface SetRequestPayloadTransform extends ReduxAction {
|
||||
type: typeof SET_REQUEST_PAYLOAD_TRANSFORM;
|
||||
isRequestPayloadTransform: boolean;
|
||||
}
|
||||
|
||||
export interface SetRequestTransformState extends ReduxAction {
|
||||
type: typeof SET_REQUEST_TRANSFORM_STATE;
|
||||
newState: RequestTransformState;
|
||||
}
|
||||
|
||||
export type RequestTransformEvents =
|
||||
| SetEnvVars
|
||||
| SetSessionVars
|
||||
| SetRequestMethod
|
||||
| SetRequestUrl
|
||||
| SetRequestUrlError
|
||||
| SetRequestUrlPreview
|
||||
| SetRequestQueryParams
|
||||
| SetRequestAddHeaders
|
||||
| SetRequestBody
|
||||
| SetRequestBodyError
|
||||
| SetRequestSampleInput
|
||||
| SetRequestTransformedBody
|
||||
| SetRequestContentType
|
||||
| SetRequestUrlTransform
|
||||
| SetRequestPayloadTransform
|
||||
| SetRequestTransformState;
|
||||
|
||||
export type RequestTransformState = {
|
||||
version: 1 | 2;
|
||||
envVars: KeyValuePair[];
|
||||
sessionVars: KeyValuePair[];
|
||||
requestMethod: Nullable<RequestTransformMethod>;
|
||||
requestUrl: string;
|
||||
requestUrlError: string;
|
||||
requestUrlPreview: string;
|
||||
requestQueryParams: KeyValuePair[];
|
||||
requestAddHeaders: KeyValuePair[];
|
||||
requestBody: RequestTransformStateBody;
|
||||
requestBodyError: string;
|
||||
requestSampleInput: string;
|
||||
requestTransformedBody: string;
|
||||
requestContentType: RequestTransformContentType;
|
||||
isRequestUrlTransform: boolean;
|
||||
isRequestPayloadTransform: boolean;
|
||||
templatingEngine: RequestTransformTemplateEngine;
|
||||
};
|
||||
|
||||
export type RequestTransformStateBody = Omit<
|
||||
RequestTransformBody,
|
||||
'form_template'
|
||||
> & { form_template?: KeyValuePair[] };
|
||||
|
||||
export type KeyValuePair = {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type GraphiQlHeader = {
|
||||
key: string;
|
||||
value: string;
|
||||
isActive: boolean;
|
||||
isNewHeader: boolean;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
|
||||
export type TransformationType = 'action' | 'event';
|
||||
|
||||
export const defaultActionRequestSampleInput = getActionRequestSampleInput(
|
||||
defaultActionDefSdl,
|
||||
defaultTypesDefSdl
|
||||
);
|
||||
|
||||
export const defaultEventRequestSampleInput = getEventRequestSampleInput();
|
||||
|
||||
export const defaultActionRequestBody = `{
|
||||
"users": {
|
||||
"name": {{$body.input.arg1.username}},
|
||||
"password": {{$body.input.arg1.password}}
|
||||
}
|
||||
}`;
|
||||
|
||||
export const defaultEventRequestBody = `{
|
||||
"table": {
|
||||
"name": {{$body.table.name}},
|
||||
"schema": {{$body.table.schema}}
|
||||
}
|
||||
}`;
|
||||
|
||||
const defaultActionRequestJsonPayload = `{
|
||||
"users": {
|
||||
"name": "username",
|
||||
"password": "password",
|
||||
}
|
||||
}`;
|
||||
|
||||
export const defaultActionRequestSamplePayload = (
|
||||
requestContentType: RequestTransformContentType
|
||||
) => {
|
||||
if (requestContentType === 'application/json')
|
||||
return defaultActionRequestJsonPayload;
|
||||
|
||||
if (requestContentType === 'application/x-www-form-urlencoded')
|
||||
return `retrieveSensitive=userId:1,username,password,role`;
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const defaultRequestContentType: RequestTransformContentType =
|
||||
'application/json';
|
@ -0,0 +1,480 @@
|
||||
import {
|
||||
RequestTransform,
|
||||
RequestTransformBody,
|
||||
RequestTransformMethod,
|
||||
} from '@/metadata/types';
|
||||
import { getLSItem, setLSItem, LS_KEYS } from '@/utils/localStorage';
|
||||
import {
|
||||
defaultRequestContentType,
|
||||
GraphiQlHeader,
|
||||
KeyValuePair,
|
||||
RequestTransformState,
|
||||
RequestTransformStateBody,
|
||||
} from './stateDefaults';
|
||||
import { isEmpty, isJsonString } from '../utils/jsUtils';
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
import { requestBodyActionState } from './requestTransformState';
|
||||
|
||||
export const getPairsObjFromArray = (
|
||||
pairs: KeyValuePair[]
|
||||
): Record<string, string> => {
|
||||
let obj = {};
|
||||
|
||||
pairs.forEach(({ name, value }) => {
|
||||
if (!!name && !!value) {
|
||||
const pair = { [name]: value };
|
||||
obj = { ...obj, ...pair };
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
export const addPlaceholderValue = (pairs: KeyValuePair[]) => {
|
||||
if (pairs.length) {
|
||||
const lastVal = pairs[pairs.length - 1];
|
||||
if (lastVal.name && lastVal.value) {
|
||||
pairs.push({ name: '', value: '' });
|
||||
}
|
||||
} else {
|
||||
pairs.push({ name: '', value: '' });
|
||||
}
|
||||
return pairs;
|
||||
};
|
||||
|
||||
const getSessionVarsArrayFromGraphiQL = () => {
|
||||
const lsHeadersString =
|
||||
getLSItem(LS_KEYS.apiExplorerConsoleGraphQLHeaders) ?? '';
|
||||
const headers: GraphiQlHeader[] = isJsonString(lsHeadersString)
|
||||
? JSON.parse(lsHeadersString)
|
||||
: [];
|
||||
let sessionVars: KeyValuePair[] = [];
|
||||
if (Array.isArray(headers)) {
|
||||
sessionVars = headers
|
||||
.filter(
|
||||
(header: GraphiQlHeader) =>
|
||||
header.isActive && header.key?.toLowerCase().startsWith('x-hasura')
|
||||
)
|
||||
.map((header: GraphiQlHeader) => ({
|
||||
name: header.key?.toLowerCase(),
|
||||
value: header.value,
|
||||
}));
|
||||
}
|
||||
return sessionVars;
|
||||
};
|
||||
|
||||
const getEnvVarsArrayFromLS = () => {
|
||||
const lsEnvString = getLSItem(LS_KEYS.webhookTransformEnvVars) ?? '';
|
||||
const envVars: KeyValuePair[] = isJsonString(lsEnvString)
|
||||
? JSON.parse(lsEnvString)
|
||||
: [];
|
||||
return envVars;
|
||||
};
|
||||
|
||||
export const getSessionVarsFromLS = () =>
|
||||
isEmpty(getSessionVarsArrayFromGraphiQL())
|
||||
? [{ name: '', value: '' }]
|
||||
: [...getSessionVarsArrayFromGraphiQL(), { name: '', value: '' }];
|
||||
|
||||
export const getEnvVarsFromLS = () =>
|
||||
isEmpty(getEnvVarsArrayFromLS())
|
||||
? [{ name: '', value: '' }]
|
||||
: [...getEnvVarsArrayFromLS(), { name: '', value: '' }];
|
||||
|
||||
export const setEnvVarsToLS = (envVars: KeyValuePair[]) => {
|
||||
const validEnvVars = envVars.filter(
|
||||
e => !isEmpty(e.name) && !isEmpty(e.value)
|
||||
);
|
||||
setLSItem(`${LS_KEYS.webhookTransformEnvVars}`, JSON.stringify(validEnvVars));
|
||||
};
|
||||
|
||||
export const getArrayFromServerPairObject = (
|
||||
pairs: Nullable<Record<string, string>>
|
||||
): KeyValuePair[] => {
|
||||
const transformArray: KeyValuePair[] = [];
|
||||
if (pairs && Object.keys(pairs).length !== 0) {
|
||||
Object.entries(pairs).forEach(([key, value]) => {
|
||||
transformArray.push({ name: key, value });
|
||||
});
|
||||
}
|
||||
transformArray.push({ name: '', value: '' });
|
||||
return transformArray;
|
||||
};
|
||||
|
||||
const checkEmptyString = (val?: string) => {
|
||||
return val && val !== '' ? val : undefined;
|
||||
};
|
||||
|
||||
const getUrlWithBasePrefix = (val?: string) => {
|
||||
return val ? `{{$base_url}}${val}` : undefined;
|
||||
};
|
||||
|
||||
const getTransformBodyServer = (reqBody: RequestTransformStateBody) => {
|
||||
if (reqBody.action === requestBodyActionState.remove)
|
||||
return { action: reqBody.action };
|
||||
else if (reqBody.action === requestBodyActionState.transformApplicationJson)
|
||||
return { action: reqBody.action, template: reqBody.template };
|
||||
return {
|
||||
action: reqBody.action,
|
||||
form_template: getPairsObjFromArray(reqBody.form_template ?? []),
|
||||
};
|
||||
};
|
||||
|
||||
export const getRequestTransformObject = (
|
||||
transformState: RequestTransformState
|
||||
) => {
|
||||
const isRequestUrlTransform = transformState.isRequestUrlTransform;
|
||||
const isRequestPayloadTransform = transformState.isRequestPayloadTransform;
|
||||
|
||||
if (!isRequestUrlTransform && !isRequestPayloadTransform) return null;
|
||||
|
||||
let obj: RequestTransform = {
|
||||
version: 2,
|
||||
template_engine: transformState.templatingEngine,
|
||||
};
|
||||
|
||||
if (isRequestUrlTransform) {
|
||||
obj = {
|
||||
...obj,
|
||||
method: transformState.requestMethod,
|
||||
url: getUrlWithBasePrefix(transformState.requestUrl),
|
||||
query_params: getPairsObjFromArray(transformState.requestQueryParams),
|
||||
};
|
||||
if (transformState.requestMethod === 'GET') {
|
||||
obj = {
|
||||
...obj,
|
||||
request_headers: {
|
||||
remove_headers: ['content-type'],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isRequestPayloadTransform) {
|
||||
obj = {
|
||||
...obj,
|
||||
body: getTransformBodyServer(transformState.requestBody),
|
||||
};
|
||||
if (
|
||||
transformState.requestBody.action ===
|
||||
requestBodyActionState.transformFormUrlEncoded
|
||||
) {
|
||||
obj = {
|
||||
...obj,
|
||||
request_headers: {
|
||||
remove_headers: ['content-type'],
|
||||
add_headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
const getErrorFromCode = (data: Record<string, any>) => {
|
||||
const errorCode = data.code ? data.code : '';
|
||||
const errorMsg = data.error ? data.error : '';
|
||||
return `${errorCode}: ${errorMsg}`;
|
||||
};
|
||||
|
||||
const getErrorFromBody = (errorObj: Record<string, any>) => {
|
||||
const errorCode = errorObj?.error_code;
|
||||
const errorMsg = errorObj?.message;
|
||||
const stPos = errorObj?.source_position?.start_line
|
||||
? `, starts line ${errorObj?.source_position?.start_line}, column ${errorObj?.source_position?.start_column}`
|
||||
: ``;
|
||||
const endPos = errorObj?.source_position?.end_line
|
||||
? `, ends line ${errorObj?.source_position?.end_line}, column ${errorObj?.source_position?.end_column}`
|
||||
: ``;
|
||||
return `${errorCode}: ${errorMsg} ${stPos} ${endPos}`;
|
||||
};
|
||||
|
||||
export const parseValidateApiData = (
|
||||
requestData: Record<string, any> | Record<string, any>[],
|
||||
setError: (error: string) => void,
|
||||
setUrl?: (data: string) => void,
|
||||
setBody?: (data: string) => void
|
||||
) => {
|
||||
if (Array.isArray(requestData)) {
|
||||
const errorMessage = getErrorFromBody(requestData[0]);
|
||||
setError(errorMessage);
|
||||
} else if (requestData?.code) {
|
||||
const errorMessage = getErrorFromCode(requestData);
|
||||
setError(errorMessage);
|
||||
} else if (requestData?.webhook_url || requestData?.body) {
|
||||
setError('');
|
||||
if (setUrl && requestData?.webhook_url) {
|
||||
setUrl(requestData?.webhook_url);
|
||||
}
|
||||
if (setBody && requestData?.body) {
|
||||
setBody(JSON.stringify(requestData?.body, null, 2));
|
||||
}
|
||||
} else {
|
||||
const errorMessage = `Error during validation: ${requestData}`;
|
||||
setError(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
// fields for `request_transform` key in `test_webhook_transform` api
|
||||
type RequestTransformerFields = {
|
||||
url?: string;
|
||||
method?: Nullable<RequestTransformMethod>;
|
||||
query_params?: Record<string, string>;
|
||||
template_engine?: string;
|
||||
};
|
||||
|
||||
type RequestTransformerV1 = RequestTransformerFields & {
|
||||
version: 1;
|
||||
body?: string;
|
||||
};
|
||||
|
||||
type RequestTransformerV2 = RequestTransformerFields & {
|
||||
version: 2;
|
||||
body?: RequestTransformBody;
|
||||
};
|
||||
|
||||
type RequestTransformer = RequestTransformerV1 | RequestTransformerV2;
|
||||
|
||||
const getTransformer = (
|
||||
version: 1 | 2,
|
||||
transformerBody?: RequestTransformStateBody,
|
||||
transformerUrl?: string,
|
||||
requestMethod?: Nullable<RequestTransformMethod>,
|
||||
queryParams?: KeyValuePair[]
|
||||
): RequestTransformer => {
|
||||
return version === 1
|
||||
? {
|
||||
version,
|
||||
body: checkEmptyString(transformerBody?.template ?? ''),
|
||||
url: checkEmptyString(transformerUrl),
|
||||
method: requestMethod,
|
||||
query_params: queryParams
|
||||
? getPairsObjFromArray(queryParams)
|
||||
: undefined,
|
||||
template_engine: 'Kriti',
|
||||
}
|
||||
: {
|
||||
version,
|
||||
body: transformerBody
|
||||
? getTransformBodyServer(transformerBody)
|
||||
: undefined,
|
||||
url: checkEmptyString(transformerUrl),
|
||||
method: requestMethod,
|
||||
query_params: queryParams
|
||||
? getPairsObjFromArray(queryParams)
|
||||
: undefined,
|
||||
template_engine: 'Kriti',
|
||||
};
|
||||
};
|
||||
|
||||
const generateValidateTransformQuery = (
|
||||
transformer: RequestTransformer,
|
||||
requestPayload: Nullable<Record<string, any>> = null,
|
||||
webhookUrl: string,
|
||||
sessionVars?: KeyValuePair[],
|
||||
isEnvVar?: boolean,
|
||||
envVars?: KeyValuePair[]
|
||||
) => {
|
||||
return {
|
||||
type: 'test_webhook_transform',
|
||||
args: {
|
||||
webhook_url: isEnvVar ? { from_env: webhookUrl } : webhookUrl,
|
||||
body: requestPayload,
|
||||
env: envVars ? getPairsObjFromArray(envVars) : undefined,
|
||||
session_variables: sessionVars
|
||||
? getPairsObjFromArray(sessionVars)
|
||||
: undefined,
|
||||
request_transform: transformer,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type ValidateTransformOptionsArgsType = {
|
||||
version: 1 | 2;
|
||||
inputPayloadString: string;
|
||||
webhookUrl: string;
|
||||
envVarsFromContext?: KeyValuePair[];
|
||||
sessionVarsFromContext?: KeyValuePair[];
|
||||
transformerBody?: RequestTransformStateBody;
|
||||
requestUrl?: string;
|
||||
queryParams?: KeyValuePair[];
|
||||
isEnvVar?: boolean;
|
||||
requestMethod?: Nullable<RequestTransformMethod>;
|
||||
};
|
||||
|
||||
export const getValidateTransformOptions = ({
|
||||
version,
|
||||
inputPayloadString,
|
||||
webhookUrl,
|
||||
envVarsFromContext,
|
||||
sessionVarsFromContext,
|
||||
transformerBody,
|
||||
requestUrl,
|
||||
queryParams,
|
||||
isEnvVar,
|
||||
requestMethod,
|
||||
}: ValidateTransformOptionsArgsType) => {
|
||||
const requestPayload = isJsonString(inputPayloadString)
|
||||
? JSON.parse(inputPayloadString)
|
||||
: null;
|
||||
const transformerUrl = requestUrl
|
||||
? `{{$base_url}}${requestUrl}`
|
||||
: `{{$base_url}}`;
|
||||
|
||||
const finalReqBody = generateValidateTransformQuery(
|
||||
getTransformer(
|
||||
version,
|
||||
transformerBody,
|
||||
transformerUrl,
|
||||
requestMethod,
|
||||
queryParams
|
||||
),
|
||||
requestPayload,
|
||||
webhookUrl,
|
||||
sessionVarsFromContext,
|
||||
isEnvVar,
|
||||
envVarsFromContext
|
||||
);
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(finalReqBody),
|
||||
};
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const getWordListArray = (mainObj: Record<string, any>) => {
|
||||
const uniqueWords = new Set<string>();
|
||||
const recursivelyWalkObj = (obj: Record<string, any>) => {
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if (typeof key === 'string') {
|
||||
uniqueWords.add(key);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
uniqueWords.add(value);
|
||||
}
|
||||
if (typeof value === 'object' && value != null) {
|
||||
recursivelyWalkObj(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
recursivelyWalkObj(mainObj);
|
||||
return Array.from(uniqueWords);
|
||||
};
|
||||
|
||||
export const getAceCompleterFromString = (jsonString: string) => {
|
||||
const jsonObject = isJsonString(jsonString) ? JSON.parse(jsonString) : {};
|
||||
const wordListArray = getWordListArray(jsonObject);
|
||||
|
||||
const wordCompleter = {
|
||||
getCompletions: (
|
||||
editor: any,
|
||||
session: any,
|
||||
pos: any,
|
||||
prefix: string,
|
||||
callback: (
|
||||
arg1: Nullable<string>,
|
||||
arg2: { caption: string; value: string; meta: string }[]
|
||||
) => void
|
||||
) => {
|
||||
if (prefix.length === 0) {
|
||||
callback(null, []);
|
||||
return;
|
||||
}
|
||||
callback(
|
||||
null,
|
||||
wordListArray.map(word => {
|
||||
return {
|
||||
caption: word,
|
||||
value: word,
|
||||
meta: 'Sample Input',
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
return wordCompleter;
|
||||
};
|
||||
|
||||
const getTrimmedRequestUrl = (val: string) => {
|
||||
const prefix = `{{$base_url}}`;
|
||||
return val.startsWith(prefix) ? val.slice(prefix.length) : val;
|
||||
};
|
||||
|
||||
const getRequestTransformBody = (
|
||||
transform: RequestTransform
|
||||
): RequestTransformStateBody => {
|
||||
if (transform.body) {
|
||||
return transform.version === 1
|
||||
? {
|
||||
action: requestBodyActionState.transformApplicationJson,
|
||||
template: transform?.body ?? '',
|
||||
}
|
||||
: {
|
||||
...transform.body,
|
||||
form_template: getArrayFromServerPairObject(
|
||||
transform.body?.form_template
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
action: requestBodyActionState.transformApplicationJson,
|
||||
template: '',
|
||||
};
|
||||
};
|
||||
|
||||
export const getTransformState = (
|
||||
transform: RequestTransform,
|
||||
sampleInput: string
|
||||
): RequestTransformState => ({
|
||||
version: transform?.version,
|
||||
envVars: getEnvVarsFromLS(),
|
||||
sessionVars: getSessionVarsFromLS(),
|
||||
requestMethod: transform?.method ?? null,
|
||||
requestUrl: transform?.url ? getTrimmedRequestUrl(transform?.url) : '',
|
||||
requestUrlError: '',
|
||||
requestUrlPreview: '',
|
||||
requestQueryParams: getArrayFromServerPairObject(transform?.query_params) ?? [
|
||||
{ name: '', value: '' },
|
||||
],
|
||||
requestAddHeaders: getArrayFromServerPairObject(
|
||||
transform?.request_headers?.add_headers
|
||||
) ?? [{ name: '', value: '' }],
|
||||
requestBody: getRequestTransformBody(transform),
|
||||
requestBodyError: '',
|
||||
requestSampleInput: sampleInput,
|
||||
requestTransformedBody: '',
|
||||
requestContentType: transform?.content_type ?? defaultRequestContentType,
|
||||
isRequestUrlTransform:
|
||||
!!transform?.method ||
|
||||
!!transform?.url ||
|
||||
!isEmpty(transform?.query_params),
|
||||
isRequestPayloadTransform: !!transform?.body,
|
||||
templatingEngine: transform?.template_engine ?? 'Kriti',
|
||||
});
|
||||
|
||||
export const capitaliseFirstLetter = (val: string) =>
|
||||
`${val[0].toUpperCase()}${val.slice(1)}`;
|
||||
|
||||
export const sidebarNumberStyles =
|
||||
'-mb-9 -ml-14 bg-gray-50 text-sm font-medium border border-gray-400 rounded-full flex items-center justify-center h-lg w-lg';
|
||||
|
||||
export const inputStyles =
|
||||
'block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400';
|
||||
|
||||
export const fixedInputStyles =
|
||||
'inline-flex items-center h-onput rounded-l text-gray-600 font-semibold px-sm border border-r-0 border-gray-300 bg-gray-50';
|
||||
|
||||
export const buttonShadow =
|
||||
'bg-gray-50 bg-gradient-to-t from-transparent to-white border border-gray-300 rounded shadow-xs hover:border-gray-400';
|
||||
|
||||
export const focusYellowRing =
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-400';
|
||||
|
||||
export const editorDebounceTime = 1000;
|
@ -0,0 +1,98 @@
|
||||
import React, { useState } from 'react';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import type {
|
||||
GetSectionSuggestions,
|
||||
GetSuggestionValue,
|
||||
InputProps,
|
||||
RenderSectionTitle,
|
||||
RenderSuggestion,
|
||||
SuggestionsFetchRequested,
|
||||
} from 'react-autosuggest';
|
||||
import styles from './Theme.module.scss';
|
||||
|
||||
export interface AutoSuggestOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface AutoSuggestSection {
|
||||
title: string;
|
||||
suggestions: AutoSuggestOption[];
|
||||
}
|
||||
|
||||
interface CustomInputAutoSuggestProps extends InputProps<AutoSuggestOption> {
|
||||
options: AutoSuggestSection[];
|
||||
theme?: Record<string, string>;
|
||||
}
|
||||
|
||||
const CustomInputAutoSuggest: React.FC<CustomInputAutoSuggestProps> = ({
|
||||
options,
|
||||
theme = styles,
|
||||
...inputProps
|
||||
}) => {
|
||||
const [suggestions, setSuggestions] = useState<AutoSuggestSection[]>([]);
|
||||
|
||||
const getSuggestions = (value: string) => {
|
||||
const inputValue = value.trim().toLowerCase();
|
||||
|
||||
if (inputValue === '') {
|
||||
return options;
|
||||
}
|
||||
|
||||
return options.map(option => ({
|
||||
title: option.title,
|
||||
suggestions: option.suggestions.filter(
|
||||
suggestion =>
|
||||
suggestion.value.toLowerCase().slice(0, inputValue.length) ===
|
||||
inputValue
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const onSuggestionsFetchRequested: SuggestionsFetchRequested = ({
|
||||
value,
|
||||
}) => {
|
||||
setSuggestions(getSuggestions(value));
|
||||
};
|
||||
|
||||
const getSuggestionValue: GetSuggestionValue<AutoSuggestOption> =
|
||||
suggestion => suggestion.value;
|
||||
|
||||
const onSuggestionsClearRequested = () => {
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
const renderSuggestion: RenderSuggestion<AutoSuggestOption> = suggestion => (
|
||||
<div>{suggestion.value}</div>
|
||||
);
|
||||
|
||||
/* Don't render the section when there are no suggestions in it */
|
||||
const renderSectionTitle: RenderSectionTitle = (
|
||||
section: AutoSuggestSection
|
||||
) => {
|
||||
return section.suggestions.length > 0 ? section.title : null;
|
||||
};
|
||||
|
||||
const getSectionSuggestions: GetSectionSuggestions<
|
||||
AutoSuggestOption,
|
||||
AutoSuggestSection
|
||||
> = suggestionSection => suggestionSection.suggestions;
|
||||
|
||||
return (
|
||||
<Autosuggest<AutoSuggestOption, AutoSuggestSection>
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||
getSuggestionValue={getSuggestionValue}
|
||||
renderSuggestion={renderSuggestion}
|
||||
inputProps={{ ...inputProps }}
|
||||
theme={theme}
|
||||
multiSection
|
||||
renderSectionTitle={renderSectionTitle}
|
||||
shouldRenderSuggestions={() => true}
|
||||
getSectionSuggestions={getSectionSuggestions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomInputAutoSuggest;
|
@ -0,0 +1,11 @@
|
||||
@import '../../CustomInputAutoSuggest/Theme.module';
|
||||
|
||||
.suggestionsContainerOpen {
|
||||
top: 30px;
|
||||
width: 280px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
padding: 6px 12px;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
@import '../../CustomInputAutoSuggest/Theme.module';
|
||||
|
||||
.suggestionsContainerOpen {
|
||||
top: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
padding: 6px 12px;
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
$suggestion-width: 280px;
|
||||
$set-top: 34px;
|
||||
$suggestion-padding: 6px 12px;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
.input {
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.inputFocussed {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inputOpen {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.suggestionsContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.suggestionsContainerOpen {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: $set-top;
|
||||
min-width: 100%;
|
||||
width: $suggestion-width;
|
||||
border: 1px solid #aaa;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.suggestionsList {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
cursor: pointer;
|
||||
word-break: break-word;
|
||||
// padding: $suggestion-padding;
|
||||
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
background-color: #deebff;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestionHighlighted {
|
||||
background-color: #deebff;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
color: #999;
|
||||
cursor: default;
|
||||
display: block;
|
||||
font-size: 75%;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25em;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
text-transform: uppercase;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sectionContainer {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.sectionContainerFirst {
|
||||
margin-top: 10px;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
.normalInput {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.modeToggleButton {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 100;
|
||||
opacity: 0.3;
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
.modeType {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 28px;
|
||||
z-index: 100;
|
||||
font-style: italic;
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
import React, { useState } from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import { FaCompressAlt, FaExpandAlt } from 'react-icons/fa';
|
||||
|
||||
import 'brace/mode/markdown';
|
||||
import 'brace/theme/github';
|
||||
|
||||
const styles = require('./CustomInput.module.scss');
|
||||
|
||||
const NORMALKEY = 'normal';
|
||||
const JSONKEY = 'json';
|
||||
|
||||
const parseJSONData = (data, editorType) => {
|
||||
try {
|
||||
const dataObject = typeof data === 'object' ? data : JSON.parse(data);
|
||||
|
||||
return JSON.stringify(dataObject, null, editorType === JSONKEY ? 4 : 0);
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const createInitialState = data => {
|
||||
const initialState = {
|
||||
editorType: NORMALKEY,
|
||||
data: parseJSONData(data, NORMALKEY),
|
||||
};
|
||||
return initialState;
|
||||
};
|
||||
|
||||
const JsonInput = props => {
|
||||
const { standardProps, placeholderProp } = props;
|
||||
const { defaultValue, onChange } = standardProps;
|
||||
const allProps = { ...standardProps };
|
||||
delete allProps.defaultValue;
|
||||
const [state, updateState] = useState(createInitialState(defaultValue));
|
||||
const { editorType, data } = state;
|
||||
|
||||
const updateData = (newData, currentState) => {
|
||||
return {
|
||||
...currentState,
|
||||
data: newData,
|
||||
};
|
||||
};
|
||||
|
||||
const toggleEditorType = currentState => {
|
||||
const nextEditorType =
|
||||
currentState.editorType === JSONKEY ? NORMALKEY : JSONKEY;
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
data: parseJSONData(currentState.data, nextEditorType),
|
||||
editorType: nextEditorType,
|
||||
};
|
||||
};
|
||||
|
||||
const handleKeyUpEvent = e => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.which === 32) {
|
||||
updateState(toggleEditorType);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditorExec = () => {
|
||||
updateState(toggleEditorType);
|
||||
};
|
||||
|
||||
const handleInputChangeAndPropagate = e => {
|
||||
const val = e.target.value;
|
||||
updateState(currentState => updateData(val, currentState));
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextAreaChangeAndPropagate = (value, e) => {
|
||||
const val = value;
|
||||
updateState(currentState => updateData(val, currentState));
|
||||
if (onChange) {
|
||||
onChange(e, value);
|
||||
}
|
||||
};
|
||||
|
||||
const getJsonEditor = () => {
|
||||
return (
|
||||
<AceEditor
|
||||
key="ace_json_editor"
|
||||
{...allProps}
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="jsontoggler"
|
||||
minLines={10}
|
||||
maxLines={30}
|
||||
width="100%"
|
||||
value={data}
|
||||
showPrintMargin={false}
|
||||
onChange={handleTextAreaChangeAndPropagate}
|
||||
showGutter={false}
|
||||
focus
|
||||
commands={[
|
||||
{
|
||||
name: 'toggleEditor',
|
||||
bindKey: { win: 'Ctrl-Space', mac: 'Command-Space' },
|
||||
exec: handleEditorExec,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getNormalEditor = () => {
|
||||
return (
|
||||
<input
|
||||
key="input_json_editor"
|
||||
{...allProps}
|
||||
placeholder={placeholderProp}
|
||||
value={data}
|
||||
onChange={handleInputChangeAndPropagate}
|
||||
onKeyUp={handleKeyUpEvent}
|
||||
className={allProps.className + ' ' + styles.normalInput}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const editor = editorType === JSONKEY ? getJsonEditor() : getNormalEditor();
|
||||
|
||||
const toggleIcon =
|
||||
editorType === JSONKEY ? (
|
||||
<FaCompressAlt
|
||||
key="icon_json_editor"
|
||||
className={styles.modeToggleButton}
|
||||
onClick={() => updateState(toggleEditorType)}
|
||||
title={
|
||||
(editorType === JSONKEY ? 'Collapse' : 'Expand') + ' (Ctrl + Space)'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FaExpandAlt
|
||||
key="icon_json_editor"
|
||||
className={styles.modeToggleButton}
|
||||
onClick={() => updateState(toggleEditorType)}
|
||||
title={
|
||||
(editorType === JSONKEY ? 'Collapse' : 'Expand') + ' (Ctrl + Space)'
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className="json_input_editor">
|
||||
<label>{editor}</label>
|
||||
{toggleIcon}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
export default JsonInput;
|
@ -0,0 +1,197 @@
|
||||
import React, { useState } from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import { FaCompressAlt, FaExpandAlt } from 'react-icons/fa';
|
||||
|
||||
import 'brace/mode/html';
|
||||
import 'brace/mode/markdown';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/theme/chrome';
|
||||
|
||||
const styles = require('./CustomInput.module.scss');
|
||||
|
||||
// editorType is what sort of editor. All are ACE Editor
|
||||
// modes except 0, which is text input
|
||||
|
||||
// ACE editor mode names
|
||||
const EDITORTYPES = [
|
||||
'normal', // must be first
|
||||
'text',
|
||||
'markdown',
|
||||
'html',
|
||||
];
|
||||
|
||||
// human readable editor names
|
||||
// const EDITORTYPENAMES = [
|
||||
// 'single line input',
|
||||
// 'multi-line text input',
|
||||
// 'markdown',
|
||||
// 'html',
|
||||
// ];
|
||||
|
||||
// short human readable editor names
|
||||
// for the visible label
|
||||
// const SHORT_EDITOR_TYPE_NAMES = ['', 'multi-line', 'markdown', 'html'];
|
||||
|
||||
const NORMAL_KEY = 0;
|
||||
const MULTILINE_KEY = 1;
|
||||
|
||||
const createInitialState = data => {
|
||||
const initialState = {
|
||||
editorType: NORMAL_KEY,
|
||||
data: data,
|
||||
};
|
||||
return initialState;
|
||||
};
|
||||
|
||||
const TextInput = props => {
|
||||
const { standardProps, placeholderProp } = props;
|
||||
const { defaultValue, onChange } = standardProps;
|
||||
const allProps = { ...standardProps };
|
||||
delete allProps.defaultValue;
|
||||
const [state, updateState] = useState(createInitialState(defaultValue));
|
||||
const { editorType, data } = state;
|
||||
|
||||
const updateData = (newData, currentState) => {
|
||||
return {
|
||||
...currentState,
|
||||
data: newData,
|
||||
};
|
||||
};
|
||||
|
||||
const cycleEditorType = currentState => {
|
||||
// const nextEditorType = (currentState.editorType + 1) % EDITORTYPES.length;
|
||||
const nextEditorType =
|
||||
currentState.editorType === NORMAL_KEY ? MULTILINE_KEY : NORMAL_KEY;
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
editorType: nextEditorType,
|
||||
};
|
||||
};
|
||||
|
||||
const handleKeyUpEvent = e => {
|
||||
if ((e.ctrlKey || event.metaKey) && e.which === 32) {
|
||||
updateState(cycleEditorType);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditorExec = () => {
|
||||
updateState(cycleEditorType);
|
||||
};
|
||||
|
||||
const handleInputChangeAndPropagate = e => {
|
||||
const val = e.target.value;
|
||||
updateState(currentState => updateData(val, currentState));
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextAreaChangeAndPropagate = (value, e) => {
|
||||
const val = value;
|
||||
updateState(currentState => updateData(val, currentState));
|
||||
if (onChange) {
|
||||
onChange(e, value);
|
||||
}
|
||||
};
|
||||
|
||||
const getAceEditor = curmode => {
|
||||
return (
|
||||
<AceEditor
|
||||
key="ace_text_editor"
|
||||
{...allProps}
|
||||
mode={curmode}
|
||||
theme="chrome"
|
||||
name="texttoggler"
|
||||
minLines={10}
|
||||
maxLines={30}
|
||||
width="100%"
|
||||
value={data}
|
||||
showPrintMargin={false}
|
||||
onChange={handleTextAreaChangeAndPropagate}
|
||||
showGutter={false}
|
||||
focus
|
||||
commands={[
|
||||
{
|
||||
name: 'toggleEditor',
|
||||
bindKey: { win: 'Ctrl-Space', mac: 'Command-Space' },
|
||||
exec: handleEditorExec,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getNormalEditor = () => {
|
||||
return (
|
||||
<input
|
||||
key="input_text_editor"
|
||||
{...allProps}
|
||||
placeholder={placeholderProp}
|
||||
value={data}
|
||||
onChange={handleInputChangeAndPropagate}
|
||||
onKeyUp={handleKeyUpEvent}
|
||||
className={allProps.className + ' ' + styles.normalInput}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const editor =
|
||||
editorType === NORMAL_KEY
|
||||
? getNormalEditor()
|
||||
: getAceEditor(EDITORTYPES[editorType]);
|
||||
|
||||
// const cycleIcon = (
|
||||
// <span
|
||||
// onClick={() => updateState(cycleEditorType)}
|
||||
// title={
|
||||
// 'Change to ' +
|
||||
// EDITORTYPENAMES[(editorType + 1) % EDITORTYPES.length] +
|
||||
// ' (Ctrl + Space)'
|
||||
// }
|
||||
// >
|
||||
// <span className={styles.modeType}>
|
||||
// {SHORT_EDITOR_TYPE_NAMES[editorType]}
|
||||
// </span>
|
||||
// <i
|
||||
// key="icon_text_editor"
|
||||
// className={
|
||||
// 'fa ' +
|
||||
// styles.modeToggleButton +
|
||||
// (editorType === NORMAL_KEY ? ' fa-expand' : ' fa-chevron-right')
|
||||
// }
|
||||
// />
|
||||
// </span>
|
||||
// );
|
||||
|
||||
const cycleIcon =
|
||||
editorType === MULTILINE_KEY ? (
|
||||
<FaCompressAlt
|
||||
key="icon_text_editor"
|
||||
className={styles.modeToggleButton}
|
||||
onClick={() => updateState(cycleEditorType)}
|
||||
title={
|
||||
(editorType === MULTILINE_KEY ? 'Collapse' : 'Expand') +
|
||||
' (Ctrl + Space)'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FaExpandAlt
|
||||
key="icon_text_editor"
|
||||
className={styles.modeToggleButton}
|
||||
onClick={() => updateState(cycleEditorType)}
|
||||
title={
|
||||
(editorType === MULTILINE_KEY ? 'Collapse' : 'Expand') +
|
||||
' (Ctrl + Space)'
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className="text_input_editor">
|
||||
<label>{editor}</label>
|
||||
{cycleIcon}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
export default TextInput;
|
@ -0,0 +1,75 @@
|
||||
@import "../Common.module";
|
||||
|
||||
.data_dropdown_wrapper {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.dataDropdown {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.dropdown_wrapper {
|
||||
border-radius: 5px;
|
||||
width: auto;
|
||||
position: absolute;
|
||||
padding-inline-start: 0px;
|
||||
box-shadow: 7px 7px 20px 0 rgba(0, 0, 0, 0.32);
|
||||
background-color: #ffffff;
|
||||
color: yellow;
|
||||
-webkit-padding-start: 0px;
|
||||
z-index: 10;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style-type: none;
|
||||
color: #303030;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
|
||||
button {
|
||||
text-align: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
// &:hover {
|
||||
// background-color: #eee;
|
||||
// }
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownRight {
|
||||
top: -12px;
|
||||
left: 35px;
|
||||
}
|
||||
|
||||
.dropdownBottom {
|
||||
top: 32px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dropdownRight:before,
|
||||
.dropdownBottom:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border: solid 5px transparent;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.dropdownRight:before {
|
||||
border-right-color: white;
|
||||
left: -10px;
|
||||
top: 17px;
|
||||
}
|
||||
|
||||
.dropdownBottom:before {
|
||||
border-bottom-color: white;
|
||||
top: -10px;
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { getParentNodeByAttribute } from '../../../utils/domFunctions';
|
||||
import styles from './Dropdown.module.scss';
|
||||
|
||||
export type DropdownPosition = 'bottom' | 'right';
|
||||
export type DropdownOption = {
|
||||
/** html inside each row */
|
||||
content: React.ReactNode;
|
||||
/** An onClick handler for each row. If this is undefined, the row is not clickable */
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
interface DropdownListProps {
|
||||
options: DropdownOption[];
|
||||
dismiss: () => void;
|
||||
position: DropdownPosition;
|
||||
}
|
||||
|
||||
const DropdownList: React.VFC<DropdownListProps> = ({
|
||||
options,
|
||||
dismiss,
|
||||
position,
|
||||
}) => {
|
||||
const dropdownPositionStyle =
|
||||
position === 'bottom' ? styles.dropdownBottom : styles.dropdownRight;
|
||||
|
||||
return (
|
||||
<ul className={`${styles.dropdown_wrapper} ${dropdownPositionStyle}`}>
|
||||
{options.map((option, i) => (
|
||||
<li key={i}>
|
||||
{option.onClick ? (
|
||||
<button
|
||||
className={styles.cursorPointer}
|
||||
onClick={() => {
|
||||
option?.onClick?.();
|
||||
dismiss();
|
||||
}}
|
||||
>
|
||||
{option.content}
|
||||
</button>
|
||||
) : (
|
||||
option.content
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
interface DropdownProps {
|
||||
/** Tag the component with this keyPrefix. This can be consumed in tests */
|
||||
testId: string;
|
||||
/** Dropdown button */
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((args: { onClick: () => void }) => React.ReactNode);
|
||||
/** Line items */
|
||||
options: any[];
|
||||
/** Prefixes keys with the value */
|
||||
keyPrefix?: string;
|
||||
/** bottom, right (default: right) */
|
||||
position?: DropdownPosition;
|
||||
}
|
||||
|
||||
const Dropdown: React.VFC<DropdownProps> = ({
|
||||
keyPrefix,
|
||||
testId,
|
||||
children,
|
||||
options,
|
||||
position = 'right',
|
||||
}) => {
|
||||
const nodeId = `data-dropdown-element_${testId}`;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setIsOpen(_isOpen => !_isOpen);
|
||||
}, []);
|
||||
|
||||
const dismissDropdown = useCallback(() => setIsOpen(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
const dataElement = getParentNodeByAttribute(e.target, 'data-element');
|
||||
if (!dataElement || dataElement.getAttribute('data-element') !== nodeId) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
if (isOpen) {
|
||||
document.addEventListener('click', handler);
|
||||
}
|
||||
return () => {
|
||||
if (isOpen) {
|
||||
document.removeEventListener('click', handler);
|
||||
}
|
||||
};
|
||||
}, [isOpen, nodeId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${keyPrefix}_wrapper`}
|
||||
data-test={testId}
|
||||
className={styles.data_dropdown_wrapper}
|
||||
data-element={nodeId}
|
||||
>
|
||||
<div
|
||||
className={styles.dataDropdown}
|
||||
key={`${keyPrefix}_children_wrapper`}
|
||||
>
|
||||
<div key={`${keyPrefix}_children`}>
|
||||
{typeof children === 'function' ? (
|
||||
children({ onClick: toggle })
|
||||
) : (
|
||||
<button onClick={toggle}>{children}</button>
|
||||
)}
|
||||
</div>
|
||||
{isOpen && (
|
||||
<DropdownList
|
||||
position={position}
|
||||
options={options}
|
||||
dismiss={dismissDropdown}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import InputGroup from 'react-bootstrap/lib/InputGroup';
|
||||
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
|
||||
import MenuItem from 'react-bootstrap/lib/MenuItem';
|
||||
|
||||
type DropDownButtonProps = {
|
||||
title: string;
|
||||
dropdownOptions: {
|
||||
display_text: string;
|
||||
value: string;
|
||||
}[];
|
||||
dataKey: string;
|
||||
dataIndex?: string;
|
||||
onButtonChange: (e: React.MouseEvent<unknown>) => void;
|
||||
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
value?: string;
|
||||
inputVal: string;
|
||||
required: boolean;
|
||||
id?: string;
|
||||
testId?: string;
|
||||
disabled?: boolean;
|
||||
bsClass?: string;
|
||||
inputPlaceHolder: string;
|
||||
inputStyle?: Record<string, string>;
|
||||
};
|
||||
|
||||
const DDButton: React.FC<DropDownButtonProps> = props => {
|
||||
const {
|
||||
title,
|
||||
dropdownOptions,
|
||||
value,
|
||||
required,
|
||||
onInputChange,
|
||||
onButtonChange,
|
||||
dataKey,
|
||||
dataIndex,
|
||||
bsClass,
|
||||
disabled,
|
||||
inputVal,
|
||||
inputPlaceHolder,
|
||||
id,
|
||||
testId,
|
||||
} = props;
|
||||
return (
|
||||
<InputGroup className={bsClass}>
|
||||
<DropdownButton
|
||||
title={value || title}
|
||||
componentClass={InputGroup.Button}
|
||||
disabled={disabled}
|
||||
id={id || ''}
|
||||
data-test={`${testId}-dropdown-button`}
|
||||
>
|
||||
{dropdownOptions.map((d, i) => (
|
||||
<MenuItem
|
||||
data-index-id={dataIndex}
|
||||
value={d.value}
|
||||
onClick={onButtonChange}
|
||||
eventKey={i + 1}
|
||||
key={i}
|
||||
data-test={`${testId}-dropdown-item-${i + 1}`}
|
||||
>
|
||||
{d.display_text}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
<input
|
||||
style={props.inputStyle}
|
||||
type="text"
|
||||
data-key={dataKey}
|
||||
data-index-id={dataIndex}
|
||||
className="form-control"
|
||||
required={required}
|
||||
onChange={onInputChange}
|
||||
disabled={disabled}
|
||||
value={inputVal || ''}
|
||||
placeholder={inputPlaceHolder}
|
||||
data-test={`${testId}-input`}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default DDButton;
|
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { FaEdit } from 'react-icons/fa';
|
||||
import styles from '../Common.module.scss';
|
||||
|
||||
class Heading extends React.Component {
|
||||
state = {
|
||||
text: this.props.currentValue,
|
||||
isEditting: false,
|
||||
};
|
||||
|
||||
handleTextChange = e => {
|
||||
this.setState({ text: e.target.value });
|
||||
};
|
||||
|
||||
toggleEditting = () => {
|
||||
this.setState({ isEditting: !this.state.isEditting });
|
||||
};
|
||||
|
||||
handleKeyPress = e => {
|
||||
if (this.state.isEditting) {
|
||||
if (e.charCode === 13) {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
save = () => {
|
||||
if (this.props.loading) {
|
||||
return;
|
||||
}
|
||||
this.props.save(this.state.text);
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { editable, currentValue, save, loading, property } = this.props;
|
||||
|
||||
const { text, isEditting } = this.state;
|
||||
|
||||
if (!editable) {
|
||||
return <h2 className={styles.heading_text}>{currentValue}</h2>;
|
||||
}
|
||||
|
||||
if (!save) {
|
||||
console.warn('In EditableHeading, please provide a prop save');
|
||||
}
|
||||
|
||||
if (!isEditting) {
|
||||
return (
|
||||
<div className={styles.editable_heading_text}>
|
||||
<h2>{currentValue}</h2>
|
||||
<div
|
||||
onClick={this.toggleEditting}
|
||||
className={styles.editable_heading_action}
|
||||
data-test={`heading-edit-${property}`}
|
||||
>
|
||||
<FaEdit />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.editable_heading_textbox}>
|
||||
<input
|
||||
onChange={this.handleTextChange}
|
||||
className={`${styles.add_pad_left} form-control`}
|
||||
type="text"
|
||||
onKeyPress={this.handleKeyPress}
|
||||
value={text}
|
||||
data-test={`heading-edit-${property}-input`}
|
||||
/>
|
||||
<div className={styles.editable_heading_action}>
|
||||
<div
|
||||
className={styles.editable_heading_action_item}
|
||||
onClick={this.save}
|
||||
data-test={`heading-edit-${property}-save`}
|
||||
>
|
||||
{loading ? 'Saving...' : 'Save'}
|
||||
</div>
|
||||
<div
|
||||
className={styles.editable_heading_action_item}
|
||||
onClick={this.toggleEditting}
|
||||
data-test={`heading-edit-${property}-cancel`}
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Heading;
|
@ -0,0 +1,35 @@
|
||||
@import "../Common.module";
|
||||
|
||||
.queryBox {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
min-height: 30px;
|
||||
|
||||
.inputRow {
|
||||
margin: 20px 0;
|
||||
|
||||
div[class^=col-xs-] {
|
||||
padding-left: 0;
|
||||
padding-right: 2.5px;
|
||||
}
|
||||
|
||||
:global(.form-control) {
|
||||
height: 35px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
:global(.form-control):focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
|
||||
:global(.fa) {
|
||||
padding-top: 8px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
|
||||
import { OrderBy } from '../utils/v1QueryUtils';
|
||||
import { useFilterQuery, TriggerOperation } from './state';
|
||||
import { Filter, FilterRenderProp } from './types';
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
import styles from './FilterQuery.module.scss';
|
||||
import { BaseTable } from '../../../dataSources/types';
|
||||
import { generateTableDef } from '../../../dataSources';
|
||||
import { Dispatch } from '../../../types';
|
||||
import { EventKind } from '../../Services/Events/types';
|
||||
|
||||
interface Props {
|
||||
table: BaseTable;
|
||||
relationships: Nullable<string[]>;
|
||||
render: FilterRenderProp;
|
||||
presets: {
|
||||
filters: Filter[];
|
||||
sorts: OrderBy[];
|
||||
};
|
||||
dispatch: Dispatch;
|
||||
triggerOp: TriggerOperation;
|
||||
triggerType: EventKind;
|
||||
triggerName?: string;
|
||||
currentSource?: string; // mainly needed by data triggers
|
||||
}
|
||||
|
||||
/*
|
||||
* Where clause and sorts builder
|
||||
* Accepts a render prop to render the results of filter/sort query
|
||||
*/
|
||||
|
||||
const FilterQuery: React.FC<Props> = props => {
|
||||
const {
|
||||
table,
|
||||
dispatch,
|
||||
presets,
|
||||
render,
|
||||
relationships,
|
||||
triggerName,
|
||||
currentSource,
|
||||
triggerOp,
|
||||
triggerType,
|
||||
} = props;
|
||||
|
||||
const { rows, count, runQuery, state, setState } = useFilterQuery(
|
||||
generateTableDef(table.table_name, table.table_schema),
|
||||
dispatch,
|
||||
presets,
|
||||
relationships,
|
||||
triggerOp,
|
||||
triggerType,
|
||||
triggerName,
|
||||
currentSource
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.add_mar_top}>
|
||||
{render(rows, count, state, setState, runQuery)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterQuery;
|
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { OrderBy } from '../utils/v1QueryUtils';
|
||||
|
||||
import styles from './FilterQuery.module.scss';
|
||||
import { BaseTable } from '../../../dataSources/types';
|
||||
|
||||
type Props = {
|
||||
sorts: OrderBy[];
|
||||
setSorts: (o: OrderBy[]) => void;
|
||||
table: BaseTable;
|
||||
};
|
||||
|
||||
const Sorts: React.FC<Props> = props => {
|
||||
const { sorts, setSorts, table } = props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{sorts.map((sort, i) => {
|
||||
const removeSort = () => {
|
||||
setSorts([...sorts.slice(0, i), ...sorts.slice(i + 1)]);
|
||||
};
|
||||
|
||||
const setColumn = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const col = e.target.value;
|
||||
setSorts([
|
||||
...sorts.slice(0, i),
|
||||
{ ...sorts[i], column: col },
|
||||
...sorts.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
const setType = (e: React.BaseSyntheticEvent) => {
|
||||
const type = e.target.value;
|
||||
setSorts([
|
||||
...sorts.slice(0, i),
|
||||
{ ...sorts[i], type },
|
||||
...sorts.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i} // eslint-disable-line react/no-array-index-key
|
||||
className={`${styles.inputRow} row`}
|
||||
>
|
||||
<div className="col-xs-4">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={setColumn}
|
||||
value={sort.column}
|
||||
data-test={`filter-column-${i}`}
|
||||
>
|
||||
{sort.column === '' ? (
|
||||
<option disabled value="">
|
||||
-- column --
|
||||
</option>
|
||||
) : null}
|
||||
{table.columns.map(c => (
|
||||
<option key={c.column_name} value={c.column_name}>
|
||||
{c.column_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-3">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={setType}
|
||||
value={sort.type}
|
||||
data-test={`filter-op-${i}`}
|
||||
>
|
||||
<option key="asc" value="asc">
|
||||
asc
|
||||
</option>
|
||||
<option key="desc" value="desc">
|
||||
desc
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="text-center col-xs-1">
|
||||
{sorts.length === i + 1 ? null : (
|
||||
<FaTimes onClick={removeSort} data-test={`clear-filter-${i}`} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorts;
|
@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { ValueFilter, Operator } from './types';
|
||||
import { allOperators } from './utils';
|
||||
|
||||
import { isNotDefined } from '../utils/jsUtils';
|
||||
import styles from './FilterQuery.module.scss';
|
||||
import { BaseTable } from '../../../dataSources/types';
|
||||
|
||||
type Props = {
|
||||
filters: ValueFilter[];
|
||||
setFilters: (f: ValueFilter[]) => void;
|
||||
table: BaseTable;
|
||||
};
|
||||
|
||||
const Where: React.FC<Props> = props => {
|
||||
const { filters, setFilters, table } = props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{filters.map((filter, i) => {
|
||||
const removeFilter = () => {
|
||||
setFilters([...filters.slice(0, i), ...filters.slice(i + 1)]);
|
||||
};
|
||||
|
||||
const setKey = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const col = e.target.value;
|
||||
setFilters([
|
||||
...filters.slice(0, i),
|
||||
{ ...filters[i], key: col },
|
||||
...filters.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
const setOperator = (e: React.BaseSyntheticEvent) => {
|
||||
// TODO synthetic event with enums
|
||||
const op: Operator = e.target.value;
|
||||
setFilters([
|
||||
...filters.slice(0, i),
|
||||
{ ...filters[i], operator: op, value: '' },
|
||||
...filters.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
const setValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setFilters([
|
||||
...filters.slice(0, i),
|
||||
{ ...filters[i], value },
|
||||
...filters.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i} // eslint-disable-line react/no-array-index-key
|
||||
className={`${styles.inputRow} row`}
|
||||
>
|
||||
<div className="col-xs-4">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={setKey}
|
||||
value={filter.key}
|
||||
data-test={`filter-column-${i}`}
|
||||
>
|
||||
{filter.key === '' ? (
|
||||
<option disabled value="">
|
||||
-- column --
|
||||
</option>
|
||||
) : null}
|
||||
{table.columns.map(c => (
|
||||
<option key={c.column_name} value={c.column_name}>
|
||||
{c.column_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-3">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={setOperator}
|
||||
value={filter.operator || ''}
|
||||
data-test={`filter-op-${i}`}
|
||||
>
|
||||
{isNotDefined(filter.operator) ? (
|
||||
<option disabled value="">
|
||||
-- op --
|
||||
</option>
|
||||
) : null}
|
||||
{allOperators.map(o => (
|
||||
<option key={o.operator} value={o.operator}>
|
||||
{`[${o.alias}] ${o.name}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-4">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="-- value --"
|
||||
value={filter.value}
|
||||
onChange={setValue}
|
||||
data-test={`filter-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center col-xs-1">
|
||||
{filters.length === i + 1 ? null : (
|
||||
<FaTimes
|
||||
onClick={removeFilter}
|
||||
data-test={`clear-filter-${i}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Where;
|
@ -0,0 +1,229 @@
|
||||
import React from 'react';
|
||||
import { dataSource, currentDriver } from '@/dataSources';
|
||||
import { OrderBy, makeOrderBy, getRunSqlQuery } from '../utils/v1QueryUtils';
|
||||
import requestAction from '../../../utils/requestAction';
|
||||
import { Dispatch } from '../../../types';
|
||||
import endpoints from '../../../Endpoints';
|
||||
import {
|
||||
makeFilterState,
|
||||
SetFilterState,
|
||||
ValueFilter,
|
||||
makeValueFilter,
|
||||
Filter,
|
||||
RunQuery,
|
||||
} from './types';
|
||||
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
import { QualifiedTable } from '../../../metadata/types';
|
||||
import {
|
||||
getScheduledEvents,
|
||||
getEventInvocations,
|
||||
} from '../../../metadata/queryUtils';
|
||||
import { EventKind } from '../../Services/Events/types';
|
||||
import { isNotDefined } from '../utils/jsUtils';
|
||||
import { parseEventsSQLResp } from '../../Services/Events/utils';
|
||||
|
||||
const defaultFilter = makeValueFilter('', null, '');
|
||||
const defaultSort = makeOrderBy('', 'asc');
|
||||
const defaultState = makeFilterState([defaultFilter], [defaultSort], 10, 0);
|
||||
|
||||
export type TriggerOperation = 'pending' | 'processed' | 'invocation';
|
||||
|
||||
export const useFilterQuery = (
|
||||
table: QualifiedTable,
|
||||
dispatch: Dispatch,
|
||||
presets: {
|
||||
filters: Filter[];
|
||||
sorts: OrderBy[];
|
||||
},
|
||||
relationships: Nullable<string[]>,
|
||||
triggerOp: TriggerOperation,
|
||||
triggerType: EventKind,
|
||||
triggerName?: string,
|
||||
currentSource?: string
|
||||
) => {
|
||||
const [state, setState] = React.useState(defaultState);
|
||||
const [rows, setRows] = React.useState<any[]>([]);
|
||||
const [count, setCount] = React.useState<number>();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState(false);
|
||||
|
||||
const runQuery: RunQuery = (runQueryOpts = {}) => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
|
||||
const { offset, limit, sorts: newSorts } = runQueryOpts;
|
||||
|
||||
const offsetValue = isNotDefined(offset) ? state.offset : offset;
|
||||
const limitValue = isNotDefined(limit) ? state.limit : limit;
|
||||
|
||||
let query = {};
|
||||
let endpoint = endpoints.metadata;
|
||||
|
||||
if (triggerType === 'scheduled') {
|
||||
if (triggerOp !== 'invocation') {
|
||||
query = getScheduledEvents(
|
||||
'one_off',
|
||||
limitValue ?? 10,
|
||||
offsetValue ?? 0,
|
||||
triggerOp
|
||||
);
|
||||
} else {
|
||||
query = getEventInvocations(
|
||||
'one_off',
|
||||
limitValue ?? 10,
|
||||
offsetValue ?? 0
|
||||
);
|
||||
}
|
||||
} else if (triggerType === 'cron') {
|
||||
if (triggerOp !== 'invocation') {
|
||||
query = getScheduledEvents(
|
||||
'cron',
|
||||
limitValue ?? 10,
|
||||
offsetValue ?? 0,
|
||||
triggerOp,
|
||||
triggerName
|
||||
);
|
||||
} else {
|
||||
query = getEventInvocations(
|
||||
'cron',
|
||||
limitValue ?? 10,
|
||||
offsetValue ?? 0,
|
||||
triggerName
|
||||
);
|
||||
}
|
||||
} else if (triggerType === 'data') {
|
||||
endpoint = endpoints.query;
|
||||
if (triggerName) {
|
||||
query = {
|
||||
args: [
|
||||
getRunSqlQuery(
|
||||
dataSource?.getDataTriggerLogsCountQuery?.(
|
||||
triggerName,
|
||||
triggerOp
|
||||
) ?? '',
|
||||
currentSource ?? 'default',
|
||||
false,
|
||||
false,
|
||||
currentDriver
|
||||
),
|
||||
getRunSqlQuery(
|
||||
dataSource?.getDataTriggerLogsQuery?.(
|
||||
triggerOp,
|
||||
triggerName,
|
||||
limitValue,
|
||||
offsetValue
|
||||
) ?? '',
|
||||
currentSource ?? 'default',
|
||||
false,
|
||||
false,
|
||||
currentDriver
|
||||
),
|
||||
],
|
||||
source: currentSource ?? 'default',
|
||||
type: 'bulk',
|
||||
};
|
||||
} else {
|
||||
return; // fixme: this should just be an error saying that there's no trigger name provided
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(query),
|
||||
};
|
||||
|
||||
dispatch(
|
||||
requestAction(endpoint, options, undefined, undefined, true, true)
|
||||
).then(
|
||||
(data: any) => {
|
||||
if (triggerType === 'data') {
|
||||
setCount(Number(data?.[0].result?.[1]?.[0]));
|
||||
// formatting of the data
|
||||
const formattedData: Record<string, any>[] = parseEventsSQLResp(
|
||||
data?.[1]?.result ?? []
|
||||
);
|
||||
setRows(formattedData);
|
||||
} else if (triggerOp !== 'invocation') {
|
||||
setRows(data?.events ?? []);
|
||||
} else {
|
||||
setRows(data?.invocations ?? []);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
if (offset !== undefined) {
|
||||
setState(s => ({ ...s, offset }));
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
setState(s => ({ ...s, limit }));
|
||||
}
|
||||
if (newSorts) {
|
||||
setState(s => ({
|
||||
...s,
|
||||
sorts: newSorts,
|
||||
}));
|
||||
}
|
||||
if (triggerType !== 'data') {
|
||||
setCount(data?.count ?? 10);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
runQuery();
|
||||
}, []);
|
||||
|
||||
const setter: SetFilterState = {
|
||||
sorts: (sorts: OrderBy[]) => {
|
||||
const newSorts = [...sorts];
|
||||
if (!sorts.length || sorts[sorts.length - 1].column) {
|
||||
newSorts.push(defaultSort);
|
||||
}
|
||||
setState(s => ({
|
||||
...s,
|
||||
sorts: newSorts,
|
||||
}));
|
||||
},
|
||||
filters: (filters: ValueFilter[]) => {
|
||||
const newFilters = [...filters];
|
||||
if (
|
||||
!filters.length ||
|
||||
filters[filters.length - 1].value ||
|
||||
filters[filters.length - 1].key
|
||||
) {
|
||||
newFilters.push(defaultFilter);
|
||||
}
|
||||
setState(s => ({
|
||||
...s,
|
||||
filters: newFilters,
|
||||
}));
|
||||
},
|
||||
offset: (o: number) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
offset: o,
|
||||
}));
|
||||
},
|
||||
limit: (l: number) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
limit: l,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
rows,
|
||||
loading,
|
||||
error,
|
||||
runQuery,
|
||||
state,
|
||||
count,
|
||||
setState: setter,
|
||||
};
|
||||
};
|
@ -0,0 +1,146 @@
|
||||
import { OrderBy } from '../utils/v1QueryUtils';
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
|
||||
// Types for sorts
|
||||
export type SetSorts = (s: OrderBy[]) => void;
|
||||
|
||||
// All supported postgres operators
|
||||
export type Operator =
|
||||
| '$eq'
|
||||
| '$ne'
|
||||
| '$in'
|
||||
| '$nin'
|
||||
| '$gt'
|
||||
| '$lt'
|
||||
| '$gte'
|
||||
| '$lte'
|
||||
| '$like'
|
||||
| '$nlike'
|
||||
| '$ilike'
|
||||
| '$nilike'
|
||||
| '$similar'
|
||||
| '$nsimilar'
|
||||
| '$regex'
|
||||
| '$iregex'
|
||||
| '$nregex'
|
||||
| '$niregex';
|
||||
|
||||
// Operator with names and aliases
|
||||
export type OperatorDef = {
|
||||
alias: string;
|
||||
operator: Operator;
|
||||
name: string;
|
||||
default?: string;
|
||||
};
|
||||
|
||||
/*
|
||||
* Value filter. Eg: { name: { $eq: "jondoe" } }
|
||||
*/
|
||||
|
||||
export type ValueFilter = {
|
||||
kind: 'value';
|
||||
key: string;
|
||||
operator: Nullable<Operator>;
|
||||
value: string;
|
||||
};
|
||||
/*
|
||||
* Constructor for value filter
|
||||
*/
|
||||
export const makeValueFilter = (
|
||||
key: string,
|
||||
operator: Nullable<Operator>,
|
||||
value: string
|
||||
): ValueFilter => ({ kind: 'value', key, operator, value });
|
||||
|
||||
/*
|
||||
* Relationship filter filter. Eg: { user { name: { $eq: "jondoe" } } }
|
||||
*/
|
||||
|
||||
export type RelationshipFilter = {
|
||||
kind: 'relationship';
|
||||
key: string;
|
||||
value: Filter;
|
||||
};
|
||||
/*
|
||||
* Constructor for relationship filter
|
||||
*/
|
||||
export const makeRelationshipFilter = (
|
||||
key: string,
|
||||
value: Filter
|
||||
): RelationshipFilter => ({ kind: 'relationship', key, value });
|
||||
|
||||
/*
|
||||
* Filter with logical gates
|
||||
* Eg: { $and: [ { title: { $eq: "My Title" } }, { author: { name: { $eq: "jon" }}} ]}
|
||||
*/
|
||||
|
||||
type LogicGate = '$or' | '$and' | '$not';
|
||||
export type OperatorFilter = {
|
||||
kind: 'operator';
|
||||
key: LogicGate;
|
||||
value: Filter[];
|
||||
};
|
||||
|
||||
/*
|
||||
* Constructor for operation filter
|
||||
*/
|
||||
export const makeOperationFilter = (
|
||||
key: LogicGate,
|
||||
value: Filter[]
|
||||
): OperatorFilter => ({ kind: 'operator', key, value });
|
||||
|
||||
/*
|
||||
* Filter for building the where clause
|
||||
* Filter could be a value filter, relationship filter, or a combination of filters
|
||||
*/
|
||||
export type Filter = ValueFilter | RelationshipFilter | OperatorFilter;
|
||||
|
||||
/*
|
||||
* Setter function for Filters
|
||||
*/
|
||||
export type SetValueFilters = (s: ValueFilter[]) => void;
|
||||
|
||||
/*
|
||||
* Local state for the filter query component
|
||||
*/
|
||||
export type FilterState = {
|
||||
filters: ValueFilter[];
|
||||
sorts: OrderBy[];
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
/*
|
||||
* Constructor for FilterState
|
||||
*/
|
||||
export const makeFilterState = (
|
||||
filters: ValueFilter[],
|
||||
sorts: OrderBy[],
|
||||
limit: number,
|
||||
offset: number
|
||||
): FilterState => ({ filters, sorts, limit, offset });
|
||||
|
||||
/*
|
||||
* Local state setter for the filter query component
|
||||
*/
|
||||
export type SetFilterState = {
|
||||
sorts: SetSorts;
|
||||
filters: SetValueFilters;
|
||||
offset: (o: number) => void;
|
||||
limit: (l: number) => void;
|
||||
};
|
||||
|
||||
export type RunQueryOptions = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
sorts?: OrderBy[];
|
||||
};
|
||||
|
||||
export type RunQuery = (options?: RunQueryOptions) => void;
|
||||
|
||||
export type FilterRenderProp = (
|
||||
rows: any[],
|
||||
count: number | undefined,
|
||||
state: FilterState,
|
||||
setState: SetFilterState,
|
||||
runQuery: RunQuery
|
||||
) => React.ReactNode;
|
@ -0,0 +1,82 @@
|
||||
import { Operator, OperatorDef, Filter } from './types';
|
||||
|
||||
export const allOperators: OperatorDef[] = [
|
||||
{ name: 'equals', operator: '$eq', alias: '_eq' },
|
||||
{ name: 'not equals', operator: '$ne', alias: '_neq' },
|
||||
{ name: 'in', operator: '$in', alias: '_in', default: '[]' },
|
||||
{ name: 'not in', operator: '$nin', alias: '_nin', default: '[]' },
|
||||
{ name: '>', operator: '$gt', alias: '_gt' },
|
||||
{ name: '<', operator: '$lt', alias: '_lt' },
|
||||
{ name: '>=', operator: '$gte', alias: '_gte' },
|
||||
{ name: '<=', operator: '$lte', alias: '_lte' },
|
||||
{ name: 'like', operator: '$like', alias: '_like', default: '%%' },
|
||||
{
|
||||
name: 'not like',
|
||||
operator: '$nlike',
|
||||
alias: '_nlike',
|
||||
default: '%%',
|
||||
},
|
||||
{
|
||||
name: 'like (case-insensitive)',
|
||||
operator: '$ilike',
|
||||
alias: '_ilike',
|
||||
default: '%%',
|
||||
},
|
||||
{
|
||||
name: 'not like (case-insensitive)',
|
||||
operator: '$nilike',
|
||||
alias: '_nilike',
|
||||
default: '%%',
|
||||
},
|
||||
{ name: 'similar', operator: '$similar', alias: '_similar' },
|
||||
{ name: 'not similar', operator: '$nsimilar', alias: '_nsimilar' },
|
||||
{ name: '~', operator: '$regex', alias: '_regex' },
|
||||
{
|
||||
name: '~*',
|
||||
operator: '$iregex',
|
||||
alias: '_iregex',
|
||||
},
|
||||
{
|
||||
name: '!~',
|
||||
operator: '$nregex',
|
||||
alias: '_nregex',
|
||||
},
|
||||
{
|
||||
name: '!~*',
|
||||
operator: '$niregex',
|
||||
alias: '_niregex',
|
||||
},
|
||||
];
|
||||
|
||||
export const getOperatorDefaultValue = (op: Operator) => {
|
||||
const operator = allOperators.find(o => o.operator === op);
|
||||
return operator ? operator.default : '';
|
||||
};
|
||||
|
||||
export const parseFilter = (f: Filter): any => {
|
||||
switch (f.kind) {
|
||||
case 'value':
|
||||
return f.operator
|
||||
? {
|
||||
[f.key]: {
|
||||
[f.operator]: f.value,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
break;
|
||||
|
||||
case 'relationship':
|
||||
return {
|
||||
[f.key]: parseFilter(f.value),
|
||||
};
|
||||
break;
|
||||
case 'operator':
|
||||
return {
|
||||
[f.key]: f.value.map(opFilter => parseFilter(opFilter)),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
return parseFilter(f);
|
||||
break;
|
||||
}
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
|
||||
import WarningSymbol from '../WarningSymbol/WarningSymbol';
|
||||
|
||||
const gqlPattern = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
||||
|
||||
export interface GqlCompatibilityWarningProps {
|
||||
identifier: string;
|
||||
className?: string;
|
||||
ifWarningCanBeFixed?: boolean;
|
||||
}
|
||||
|
||||
const GqlCompatibilityWarning: React.FC<GqlCompatibilityWarningProps> = ({
|
||||
identifier,
|
||||
className = '',
|
||||
ifWarningCanBeFixed = false,
|
||||
}) => {
|
||||
const isGraphQLCompatible = gqlPattern.test(identifier);
|
||||
|
||||
if (isGraphQLCompatible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gqlCompatibilityTip =
|
||||
'This identifier name does not conform to the GraphQL naming standard. ' +
|
||||
'Names in GraphQL should be limited to this ASCII subset: /[_A-Za-z][_0-9A-Za-z]*/. ' +
|
||||
'All GraphQL types depending on this identifier will not be exposed over the GraphQL API';
|
||||
|
||||
const gqlCompatibilityTipWithFix =
|
||||
'This identifier name does not conform to the GraphQL naming standard. ' +
|
||||
'Names in GraphQL should be limited to this ASCII subset: /[_A-Za-z][_0-9A-Za-z]*/. ' +
|
||||
'Invalid characters in this identifier will be replaced with underscores. ' +
|
||||
'If a table or column name starts with number(s), ' +
|
||||
'the number(s) will be replaced by "table_" or "column_" respectively';
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
<WarningSymbol
|
||||
tooltipText={
|
||||
!ifWarningCanBeFixed
|
||||
? gqlCompatibilityTip
|
||||
: gqlCompatibilityTipWithFix
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default GqlCompatibilityWarning;
|
@ -0,0 +1,5 @@
|
||||
@import "../Common.module";
|
||||
|
||||
.headerInputWidth {
|
||||
width: 300px
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import styles from './Headers.module.scss';
|
||||
import DropdownButton from '../DropdownButton/DropdownButton';
|
||||
import { addPlaceholderHeader } from './utils';
|
||||
|
||||
export type Header = {
|
||||
type: 'static' | 'env';
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const defaultHeader: Header = {
|
||||
name: '',
|
||||
type: 'static',
|
||||
value: '',
|
||||
};
|
||||
|
||||
interface HeadersListProps extends React.ComponentProps<'div'> {
|
||||
headers: Header[];
|
||||
disabled?: boolean;
|
||||
setHeaders: (h: Header[]) => void;
|
||||
}
|
||||
|
||||
const Headers: React.FC<HeadersListProps> = ({
|
||||
headers,
|
||||
setHeaders,
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{headers.map(({ name, value, type }, i) => {
|
||||
const setHeaderType = (e: React.BaseSyntheticEvent) => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].type = e.target.getAttribute('value');
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const setHeaderKey = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].name = e.target.value;
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const setHeaderValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].value = e.target.value;
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const removeHeader = () => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
setHeaders([...newHeaders.slice(0, i), ...newHeaders.slice(i + 1)]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.display_flex} ${styles.add_mar_bottom_mid}`}
|
||||
key={i.toString()}
|
||||
>
|
||||
<input
|
||||
value={name}
|
||||
onChange={setHeaderKey}
|
||||
placeholder="key"
|
||||
className={`form-control ${styles.add_mar_right} ${styles.headerInputWidth}`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={styles.headerInputWidth}>
|
||||
<DropdownButton
|
||||
dropdownOptions={[
|
||||
{ display_text: 'Value', value: 'static' },
|
||||
{ display_text: 'From env var', value: 'env' },
|
||||
]}
|
||||
title={type === 'env' ? 'From env var' : 'Value'}
|
||||
dataKey={type === 'env' ? 'env' : 'static'}
|
||||
onButtonChange={setHeaderType}
|
||||
onInputChange={setHeaderValue}
|
||||
required={false}
|
||||
bsClass={styles.dropdown_button}
|
||||
inputVal={value}
|
||||
id={`header-value-${i}`}
|
||||
inputPlaceHolder={type === 'env' ? 'HEADER_FROM_ENV' : 'value'}
|
||||
testId={`header-value-${i}`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
{i < headers.length - 1 ? (
|
||||
<FaTimes
|
||||
className={`${styles.fontAwosomeClose} text-lg`}
|
||||
onClick={removeHeader}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Headers;
|
@ -0,0 +1,48 @@
|
||||
import { Header as HeaderClient, defaultHeader } from './Headers';
|
||||
import { ServerHeader } from '../../../metadata/types';
|
||||
|
||||
export const transformHeaders = (headers_?: HeaderClient[]) => {
|
||||
const headers = headers_ || [];
|
||||
return headers
|
||||
.map(h => {
|
||||
const transformedHeader: ServerHeader = {
|
||||
name: h.name,
|
||||
};
|
||||
if (h.type === 'static') {
|
||||
transformedHeader.value = h.value;
|
||||
} else {
|
||||
transformedHeader.value_from_env = h.value;
|
||||
}
|
||||
return transformedHeader;
|
||||
})
|
||||
.filter(h => !!h.name && (!!h.value || !!h.value_from_env));
|
||||
};
|
||||
|
||||
export const addPlaceholderHeader = (newHeaders: HeaderClient[]) => {
|
||||
if (newHeaders.length) {
|
||||
const lastHeader = newHeaders[newHeaders.length - 1];
|
||||
if (lastHeader.name && lastHeader.value) {
|
||||
newHeaders.push(defaultHeader);
|
||||
}
|
||||
} else {
|
||||
newHeaders.push(defaultHeader);
|
||||
}
|
||||
return newHeaders;
|
||||
};
|
||||
|
||||
export const parseServerHeaders = (headers: ServerHeader[] = []) => {
|
||||
return addPlaceholderHeader(
|
||||
headers.map(h => {
|
||||
const parsedHeader: HeaderClient = {
|
||||
name: h.name,
|
||||
value: h.value || '',
|
||||
type: 'static',
|
||||
};
|
||||
if (h.value_from_env) {
|
||||
parsedHeader.value = h.value_from_env;
|
||||
parsedHeader.type = 'env';
|
||||
}
|
||||
return parsedHeader;
|
||||
})
|
||||
);
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
const ActionDef: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 mr-xs h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 9.134a1 1 0 010 1.732l-3.354 1.935-1.18 4.455a1 1 0 01-1.933 0L9.854 12.8 6.5 10.866a1 1 0 010-1.732l3.354-1.935 1.18-4.455A1 1 0 0112 2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionDef;
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
const AddIcon: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-5 mr-xs"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddIcon;
|
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { FaCheck } from 'react-icons/fa';
|
||||
import styles from '../Common.module.scss';
|
||||
|
||||
const Check = ({ className = '', title = '' }) => {
|
||||
return (
|
||||
<FaCheck
|
||||
className={`${styles.iconCheck} ${className}`}
|
||||
aria-hidden="true"
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Check;
|
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { FaClock } from 'react-icons/fa';
|
||||
|
||||
const Clock = ({ className = '', title = '' }) => {
|
||||
return (
|
||||
<FaClock
|
||||
className={`${className || ''}`}
|
||||
aria-hidden="true"
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Clock;
|
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { FaCopy } from 'react-icons/fa';
|
||||
|
||||
const Copy = ({ className = '' }) => {
|
||||
return <FaCopy className={`${className || ''}`} aria-hidden="true" />;
|
||||
};
|
||||
|
||||
export default Copy;
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
|
||||
import styles from '../Common.module.scss';
|
||||
|
||||
const Cross = ({ className = '', title = '' }) => {
|
||||
return (
|
||||
<FaTimes
|
||||
className={` ${styles.iconCross} ${className}`}
|
||||
aria-hidden="true"
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cross;
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
const GlobalTypesDefIcon: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 mr-xs h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 011.523-1.943A5.977 5.977 0 0116 10c0 .34-.028.675-.083 1H15a2 2 0 00-2 2v2.197A5.973 5.973 0 0110 16v-2a2 2 0 00-2-2 2 2 0 01-2-2 2 2 0 00-1.668-1.973z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalTypesDefIcon;
|
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
|
||||
const Invalid = ({ className = '', title = '' }) => {
|
||||
return (
|
||||
<FaExclamation
|
||||
className={`${className || ''}`}
|
||||
aria-hidden="true"
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Invalid;
|
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { FaRedoAlt } from 'react-icons/fa';
|
||||
|
||||
const Reload = ({ className = '' }) => {
|
||||
return <FaRedoAlt className={` ${className || ''}`} aria-hidden="true" />;
|
||||
};
|
||||
|
||||
export default Reload;
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
const ResetIcon: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 mr-xs h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetIcon;
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
const TypesDefIcon: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 mr-xs"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypesDefIcon;
|
@ -0,0 +1,72 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { inputChecker } from './utils';
|
||||
|
||||
class InputChecker extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
isError: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
}
|
||||
onBlur(e) {
|
||||
const val = e.target.value;
|
||||
if (!val) {
|
||||
this.setState({
|
||||
isError: false,
|
||||
errorMessage: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
inputChecker(this.props.type, val)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
isError: false,
|
||||
errorMessage: '',
|
||||
});
|
||||
})
|
||||
.catch(r => {
|
||||
this.setState({
|
||||
isError: true,
|
||||
errorMessage: r.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
render() {
|
||||
const { value, onChange, placeholder, disabled, title } = this.props;
|
||||
|
||||
const style = {
|
||||
border: '1px solid red',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
return (
|
||||
<input
|
||||
{...this.props}
|
||||
className={'input-sm form-control'}
|
||||
style={this.state.isError ? style : {}}
|
||||
placeholder={placeholder || 'new input'}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={this.onBlur}
|
||||
disabled={disabled}
|
||||
title={this.state.errorMessage || title}
|
||||
data-test={this.props['data-test']}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InputChecker.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default InputChecker;
|
@ -0,0 +1,59 @@
|
||||
const intChecker = val => {
|
||||
const rVal = parseInt(val, 10);
|
||||
|
||||
if (!rVal) {
|
||||
throw new Error('Invalid input for type integer');
|
||||
}
|
||||
return rVal;
|
||||
};
|
||||
const boolChecker = val => {
|
||||
let rVal = '';
|
||||
if (val === 'true') {
|
||||
rVal = true;
|
||||
} else if (rVal === 'false') {
|
||||
rVal = false;
|
||||
} else {
|
||||
rVal = null;
|
||||
}
|
||||
|
||||
if (rVal === null) {
|
||||
throw new Error('Invalid input for type bool');
|
||||
}
|
||||
return rVal;
|
||||
};
|
||||
|
||||
const jsonChecker = val => {
|
||||
try {
|
||||
JSON.parse(val);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
const typeChecker = {
|
||||
integer: intChecker,
|
||||
numeric: intChecker,
|
||||
bigint: intChecker,
|
||||
boolean: boolChecker,
|
||||
json: jsonChecker,
|
||||
jsonb: jsonChecker,
|
||||
};
|
||||
|
||||
const inputChecker = (type, value) => {
|
||||
// Checks the input against the intended type
|
||||
// and returns the value or error
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (type in typeChecker) {
|
||||
resolve(typeChecker[type](value));
|
||||
return;
|
||||
}
|
||||
resolve(value);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { inputChecker };
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
type KnowMoreProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
const KnowMore: React.FC<KnowMoreProps> = props => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-xs font-normal italic text-secondary text-sm flex items-center"
|
||||
>
|
||||
Know More
|
||||
<svg
|
||||
className="ml-xs w-4 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
|
||||
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowMore;
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
interface KnowMoreLinkProps {
|
||||
href: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const KnowMoreLink: React.VFC<KnowMoreLinkProps> = ({
|
||||
href,
|
||||
text = 'Know more',
|
||||
}) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
className="ml-sm font-normal text-secondary text-italic text-sm"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
({text})
|
||||
</a>
|
||||
);
|
||||
|
||||
export default KnowMoreLink;
|
@ -0,0 +1,27 @@
|
||||
import React, { InputHTMLAttributes } from 'react';
|
||||
import { IconTooltip } from '@/new-components/Tooltip';
|
||||
|
||||
interface LabeledInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
boldlabel?: boolean;
|
||||
tooltipText?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export const LabeledInput: React.FC<LabeledInputProps> = props => (
|
||||
<>
|
||||
<label
|
||||
className={`flex items-center gap-1 ${
|
||||
props.boldlabel ? '' : 'inline-block pb-2.5 font-bold'
|
||||
}`}
|
||||
>
|
||||
{props?.boldlabel ? <b>{props.label}</b> : props.label}
|
||||
{props.tooltipText && <IconTooltip message={props.tooltipText} />}
|
||||
</label>
|
||||
<input
|
||||
type={props?.type ?? 'text'}
|
||||
className="form-control font-normal mb-4"
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
@ -0,0 +1,55 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { FaAngleRight } from 'react-icons/fa';
|
||||
import { Link } from 'react-router';
|
||||
import styles from '../../TableCommon/Table.module.scss';
|
||||
|
||||
export type BreadCrumb = {
|
||||
url: string;
|
||||
title: string;
|
||||
prefix?: ReactElement;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
breadCrumbs: BreadCrumb[];
|
||||
};
|
||||
|
||||
const BreadCrumb: React.FC<Props> = ({ breadCrumbs }) => {
|
||||
let bC = null;
|
||||
|
||||
if (breadCrumbs && breadCrumbs.length > 0) {
|
||||
bC = breadCrumbs.map((b: BreadCrumb, i: number) => {
|
||||
let bCElem;
|
||||
|
||||
const addArrow = () => (
|
||||
<React.Fragment key={i}>
|
||||
|
||||
<FaAngleRight key={`${b.title}-arrow`} aria-hidden="true" />
|
||||
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const isLastElem = i === breadCrumbs.length - 1;
|
||||
|
||||
if (!isLastElem) {
|
||||
bCElem = [
|
||||
<Link key={`bc-title-${b.title}`} to={`${b.url}`}>
|
||||
{b.prefix} {b.title}
|
||||
</Link>,
|
||||
addArrow(),
|
||||
];
|
||||
} else {
|
||||
bCElem = (
|
||||
<span>
|
||||
{b.prefix} {b.title}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span key={i}>{bCElem}</span>;
|
||||
});
|
||||
}
|
||||
|
||||
return <div className={styles.dataBreadCrumb}>You are here: {bC}</div>;
|
||||
};
|
||||
|
||||
export default BreadCrumb;
|
@ -0,0 +1 @@
|
||||
@import '../../Common.module';
|
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
|
||||
import BreadCrumb, {
|
||||
BreadCrumb as BreadCrumbType,
|
||||
} from '../BreadCrumb/BreadCrumb';
|
||||
import Tabs, { Tabs as TabsType } from '../ReusableTabs/ReusableTabs';
|
||||
import styles from './CommonTabLayout.module.scss';
|
||||
|
||||
type Props = {
|
||||
breadCrumbs: BreadCrumbType[];
|
||||
heading: React.ReactNode;
|
||||
appPrefix: string;
|
||||
currentTab: string;
|
||||
tabsInfo: TabsType;
|
||||
baseUrl: string;
|
||||
showLoader: boolean;
|
||||
testPrefix: string;
|
||||
subHeading?: React.ReactNode;
|
||||
};
|
||||
|
||||
const CommonTabLayout: React.FC<Props> = props => {
|
||||
const {
|
||||
breadCrumbs,
|
||||
heading,
|
||||
appPrefix,
|
||||
currentTab,
|
||||
tabsInfo,
|
||||
baseUrl,
|
||||
showLoader,
|
||||
testPrefix,
|
||||
subHeading,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.subHeader}>
|
||||
{breadCrumbs.length ? <BreadCrumb breadCrumbs={breadCrumbs} /> : null}
|
||||
<h2 className={`${styles.heading_text} ${styles.set_line_height}`}>
|
||||
{heading || ''}
|
||||
</h2>
|
||||
{subHeading || null}
|
||||
<Tabs
|
||||
appPrefix={appPrefix}
|
||||
tabName={currentTab}
|
||||
tabsInfo={tabsInfo}
|
||||
baseUrl={baseUrl}
|
||||
showLoader={showLoader}
|
||||
testPrefix={testPrefix}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonTabLayout;
|
@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import Button from '../../Button/Button';
|
||||
|
||||
class Editor extends React.Component {
|
||||
static getDerivedStateFromProps(nextProps, state) {
|
||||
if (nextProps.toggled !== state.isEditing) {
|
||||
if (nextProps.toggled !== undefined) {
|
||||
return {
|
||||
isEditing: nextProps.toggled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isEditing: props.toggled || false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleEditor = () => {
|
||||
if (this.props.expandCallback && !this.state.isEditing) {
|
||||
this.props.expandCallback();
|
||||
} else if (this.props.collapseCallback && this.state.isEditing) {
|
||||
this.props.collapseCallback();
|
||||
}
|
||||
this.setState({
|
||||
isEditing: !this.state.isEditing,
|
||||
});
|
||||
};
|
||||
|
||||
toggleButton = () => {
|
||||
const {
|
||||
readOnlyMode = false,
|
||||
isCollapsable,
|
||||
service,
|
||||
property,
|
||||
collapseButtonText,
|
||||
expandButtonText,
|
||||
} = this.props;
|
||||
|
||||
const { isEditing } = this.state;
|
||||
if (isCollapsable === false && isEditing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="white"
|
||||
size="xs"
|
||||
className="mr-sm"
|
||||
data-test={`${service}-${isEditing ? 'close' : 'edit'}-${property}`}
|
||||
onClick={this.toggleEditor}
|
||||
disabled={readOnlyMode}
|
||||
>
|
||||
{isEditing ? collapseButtonText || 'Close' : expandButtonText || 'Edit'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
saveButton = saveFunc => {
|
||||
const { service, property, ongoingRequest, saveButtonColor } = this.props;
|
||||
const isProcessing = ongoingRequest === property;
|
||||
const saveWithToggle = () => saveFunc(this.toggleEditor);
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
color={saveButtonColor || 'yellow'}
|
||||
size="sm"
|
||||
className="mr-sm"
|
||||
onClick={saveWithToggle}
|
||||
data-test={`${service}-${property}-save`}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{this.props.saveButtonText || 'Save'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
removeButton = removeFunc => {
|
||||
const { service, property, ongoingRequest, removeButtonColor } = this.props;
|
||||
const isProcessing = ongoingRequest === property;
|
||||
const removeWithToggle = () => removeFunc(this.toggleEditor);
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
color={removeButtonColor || 'red'}
|
||||
size="sm"
|
||||
onClick={removeWithToggle}
|
||||
data-test={`${service}-${property}-remove`}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{this.props.removeButtonText || 'Remove'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
actionButtons = () => {
|
||||
const { saveFunc, removeFunc } = this.props;
|
||||
return (
|
||||
<div className="flex items-center mt-sm">
|
||||
{saveFunc && this.saveButton(saveFunc)}
|
||||
{removeFunc && this.removeButton(removeFunc)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isEditing } = this.state;
|
||||
const {
|
||||
editorCollapsed,
|
||||
editorExpanded,
|
||||
collapsedLabel,
|
||||
expandedLabel,
|
||||
readOnlyMode = false,
|
||||
} = this.props;
|
||||
|
||||
let editorClass;
|
||||
let editorLabel;
|
||||
let editorContent;
|
||||
let actionButtons;
|
||||
|
||||
if (isEditing) {
|
||||
editorClass = 'block rounded bg-white border border-gray-300 p-md mb-sm';
|
||||
editorLabel = expandedLabel && expandedLabel();
|
||||
actionButtons = this.actionButtons();
|
||||
|
||||
if (editorExpanded) {
|
||||
editorContent = <div>{editorExpanded()}</div>;
|
||||
}
|
||||
} else {
|
||||
editorClass = 'block';
|
||||
editorLabel = collapsedLabel && collapsedLabel();
|
||||
|
||||
if (editorCollapsed) {
|
||||
editorContent = <div>{editorCollapsed()}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-md ${editorClass}`}>
|
||||
<div className="mb-sm flex items-center">
|
||||
{this.toggleButton()}
|
||||
{editorLabel}
|
||||
</div>
|
||||
{editorContent}
|
||||
{!readOnlyMode && actionButtons}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Editor;
|
@ -0,0 +1,18 @@
|
||||
@import "../../Common.module";
|
||||
|
||||
.editorExpanded {
|
||||
margin: 10px 0;
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.editorCollapsed {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.editorContent {
|
||||
padding: 10px 15px;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import styles from '../../TableCommon/Table.module.scss';
|
||||
|
||||
const LeftContainer: React.FC = ({ children }) => {
|
||||
return (
|
||||
<div className={`${styles.pageSidebar} col-xs-12 ${styles.padd_remove}`}>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftContainer;
|
@ -0,0 +1,98 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import styles from './LeftSubSidebar.module.scss';
|
||||
|
||||
interface LeftSidebarItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface LeftSidebarSectionProps extends React.ComponentProps<'div'> {
|
||||
items: LeftSidebarItem[];
|
||||
currentItem?: LeftSidebarItem;
|
||||
getServiceEntityLink: (s: string) => string;
|
||||
service: string;
|
||||
sidebarIcon?: ReactElement;
|
||||
}
|
||||
|
||||
const LeftSidebarSection = ({
|
||||
items = [],
|
||||
currentItem,
|
||||
service,
|
||||
sidebarIcon,
|
||||
getServiceEntityLink,
|
||||
}: LeftSidebarSectionProps) => {
|
||||
// TODO needs refactor to accomodate other services
|
||||
|
||||
const [searchText, setSearchText] = React.useState('');
|
||||
|
||||
const getSearchInput = () => {
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchText(e.target.value);
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
onChange={handleSearch}
|
||||
className="form-control"
|
||||
placeholder={`search ${service}`}
|
||||
data-test={`search-${service}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
let itemList: LeftSidebarItem[] = [];
|
||||
if (searchText) {
|
||||
const secondaryResults: LeftSidebarItem[] = [];
|
||||
items.forEach(a => {
|
||||
if (a.name.startsWith(searchText)) {
|
||||
itemList.push(a);
|
||||
} else if (a.name.includes(searchText)) {
|
||||
secondaryResults.push(a);
|
||||
}
|
||||
});
|
||||
itemList = [...itemList, ...secondaryResults];
|
||||
} else {
|
||||
itemList = [...items];
|
||||
}
|
||||
|
||||
const getChildList = () => {
|
||||
let childList;
|
||||
if (itemList.length === 0) {
|
||||
childList = (
|
||||
<li className={styles.noChildren} data-test="sidebar-no-services">
|
||||
<i>No {service} available</i>
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
childList = itemList.map(a => {
|
||||
let activeTableClass = '';
|
||||
if (currentItem && currentItem.name === a.name) {
|
||||
activeTableClass = styles.activeLink;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={activeTableClass}
|
||||
key={a.name}
|
||||
data-test={`action-sidebar-links-${a.name}`}
|
||||
>
|
||||
<Link to={getServiceEntityLink(a.name)} data-test={a.name}>
|
||||
{sidebarIcon}
|
||||
{a.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return childList;
|
||||
};
|
||||
|
||||
return {
|
||||
getChildList,
|
||||
getSearchInput,
|
||||
count: itemList.length,
|
||||
items: itemList,
|
||||
};
|
||||
};
|
||||
|
||||
export default LeftSidebarSection;
|
@ -0,0 +1,276 @@
|
||||
|
||||
@import '../../Common.module';
|
||||
|
||||
.displayFlexContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flexRow {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.add_btn {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.account {
|
||||
padding: 20px 0;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
height: calc(100vh - 26px);
|
||||
overflow: auto;
|
||||
// background: #444;
|
||||
// color: $navbar-inverse-color;
|
||||
color: #333;
|
||||
border: 1px solid #e5e5e5;
|
||||
background-color: #f8f8f8;
|
||||
|
||||
/*
|
||||
a,a:visited {
|
||||
color: $navbar-inverse-link-color;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $navbar-inverse-link-hover-color;
|
||||
}
|
||||
*/
|
||||
|
||||
hr {
|
||||
margin: 0;
|
||||
/* taken from bootsrap $navbar-inverse-color variable */
|
||||
border-color: lighten(lighten(#000, 46.7%), 15%);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-top: 10px;
|
||||
padding-left: 7px;
|
||||
|
||||
li {
|
||||
padding: 7px 0;
|
||||
transition: color 0.5s;
|
||||
|
||||
/*
|
||||
a,a:visited {
|
||||
color: $navbar-inverse-link-color;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $navbar-inverse-link-hover-color;
|
||||
}
|
||||
*/
|
||||
|
||||
a {
|
||||
color: #767e93;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover {
|
||||
padding: 7px 0;
|
||||
// color: $navbar-inverse-link-hover-color;
|
||||
transition: color 0.5s;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0;
|
||||
height: $mainContainerHeight;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sidebarSearch {
|
||||
margin-right: 20px;
|
||||
padding: 10px 0px;
|
||||
padding-bottom: 0px;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
margin: 10px 10px 10px 8px;
|
||||
color: #979797;
|
||||
}
|
||||
|
||||
input {
|
||||
padding-left: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarHeadingWrapper {
|
||||
width: 100%;
|
||||
float: left;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.sidebarHeading {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
color: #767e93;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.subSidebarList {
|
||||
// max-height: 300px;
|
||||
// overflow-y: auto;
|
||||
overflow: auto;
|
||||
padding-left: 20px;
|
||||
max-height: calc(100vh - 275px);
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.subSidebarListUL {
|
||||
padding-left: 5px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
li {
|
||||
border-bottom: 0px !important;
|
||||
padding: 4px 0 !important;
|
||||
|
||||
.tableFunctionDivider {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
a {
|
||||
background: transparent !important;
|
||||
padding: 5px 0px !important;
|
||||
font-weight: 400 !important;
|
||||
padding-left: 5px !important;
|
||||
display: initial !important;
|
||||
|
||||
.tableIcon,
|
||||
.functionIcon {
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.icon_mar_left {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noChildren {
|
||||
font-weight: 400 !important;
|
||||
padding-bottom: 10px !important;
|
||||
color: #767e93 !important;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
padding-top: 15px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.reducedChildPadding {
|
||||
li:first-child {
|
||||
padding-top: 5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarTablePadding {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
.activeLink {
|
||||
a {
|
||||
color: #fd9540 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.padLeft4 {
|
||||
margin-left: 8px;
|
||||
top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.floatRight {
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.treeNav {
|
||||
color: #767e93;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
|
||||
i {
|
||||
margin-right: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 14px !important;
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
}
|
||||
.titleClosed::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.inline_display {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.loaderBar {
|
||||
position: relative;
|
||||
padding: 0px 54px;
|
||||
background-color: #f0f0f0;
|
||||
color: #e5e5e5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.loaderBar::after {
|
||||
display: block;
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 108px;
|
||||
height: 18px;
|
||||
margin-top: -18px;
|
||||
z-index: 2;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0.5),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
animation: loading 2s infinite;
|
||||
}
|
||||
@keyframes loading {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.color_green {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.padd_left_sm {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.display_flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.align_items_center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin: 0;
|
||||
height: 17px;
|
||||
width: 17px;
|
||||
margin-left: 5px;
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { FaSearch } from 'react-icons/fa';
|
||||
|
||||
import Button from '../../Button/Button';
|
||||
import styles from './LeftSubSidebar.module.scss';
|
||||
|
||||
interface Props extends React.ComponentProps<'div'> {
|
||||
showAddBtn: boolean;
|
||||
searchInput: React.ReactNode;
|
||||
heading: string;
|
||||
addLink: string;
|
||||
addLabel: string;
|
||||
addTestString: string;
|
||||
childListTestString: string;
|
||||
}
|
||||
|
||||
const LeftSubSidebar: React.FC<Props> = props => {
|
||||
const {
|
||||
showAddBtn,
|
||||
searchInput,
|
||||
heading,
|
||||
addLink,
|
||||
addLabel,
|
||||
addTestString,
|
||||
children,
|
||||
childListTestString,
|
||||
} = props;
|
||||
|
||||
const getAddButton = () => {
|
||||
let addButton = null;
|
||||
|
||||
if (showAddBtn) {
|
||||
addButton = (
|
||||
<div
|
||||
className={`col-xs-4 text-center ${styles.padd_left_remove} ${styles.sidebarCreateTable}`}
|
||||
>
|
||||
<Link className={styles.padd_remove_full} to={addLink}>
|
||||
<Button size="xs" color="white" data-test={addTestString}>
|
||||
{addLabel}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return addButton;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.subSidebarList}>
|
||||
<div className={`${styles.display_flex} ${styles.padd_top_medium}`}>
|
||||
{searchInput && (
|
||||
<div
|
||||
className={`${styles.sidebarSearch} form-group col-xs-12 ${styles.padd_remove}`}
|
||||
>
|
||||
<FaSearch aria-hidden="true" />
|
||||
{searchInput}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.sidebarHeadingWrapper}>
|
||||
<div
|
||||
className={`col-xs-8 ${styles.sidebarHeading} ${styles.padd_left_remove}`}
|
||||
>
|
||||
{heading}
|
||||
</div>
|
||||
{getAddButton()}
|
||||
</div>
|
||||
<ul className={styles.subSidebarListUL} data-test={childListTestString}>
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftSubSidebar;
|
@ -0,0 +1,98 @@
|
||||
import React, { ReactElement, useMemo, useState } from 'react';
|
||||
|
||||
import { Link } from 'react-router';
|
||||
import { FaDatabase } from 'react-icons/fa';
|
||||
|
||||
import styles from './LeftSubSidebar.module.scss';
|
||||
|
||||
type CollapsibleItemsProps = {
|
||||
source: string;
|
||||
currentItem?: { name: string; source: string };
|
||||
getServiceEntityLink: (v: string) => string;
|
||||
items: { name: string; source: string }[];
|
||||
icon: ReactElement;
|
||||
};
|
||||
const CollapsibleItems: React.FC<CollapsibleItemsProps> = ({
|
||||
currentItem,
|
||||
source,
|
||||
getServiceEntityLink,
|
||||
items,
|
||||
icon,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<div className={styles.padd_bottom_small}>
|
||||
<div
|
||||
onClick={() => setIsOpen(prev => !prev)}
|
||||
onKeyDown={() => setIsOpen(prev => !prev)}
|
||||
role="button"
|
||||
className={styles.padd_bottom_small}
|
||||
>
|
||||
<span className={`${styles.title} ${isOpen ? '' : styles.titleClosed}`}>
|
||||
<FaDatabase /> {source}
|
||||
</span>
|
||||
</div>
|
||||
{isOpen
|
||||
? items.map(({ name }) => (
|
||||
<li
|
||||
className={
|
||||
currentItem && currentItem.name === name
|
||||
? styles.activeLink
|
||||
: ''
|
||||
}
|
||||
key={name}
|
||||
data-test={`action-sidebar-links-${name}`}
|
||||
>
|
||||
<Link to={getServiceEntityLink(name)} data-test={name}>
|
||||
{icon}
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type TreeViewProps = {
|
||||
service: string;
|
||||
items: { name: string; source: string }[];
|
||||
currentItem?: { name: string; source: string };
|
||||
getServiceEntityLink: (name: string) => string;
|
||||
icon: ReactElement;
|
||||
};
|
||||
export const TreeView: React.FC<TreeViewProps> = ({
|
||||
items,
|
||||
service,
|
||||
...rest
|
||||
}) => {
|
||||
const itemsBySource = useMemo(() => {
|
||||
return items.reduce((acc, item) => {
|
||||
return {
|
||||
...acc,
|
||||
[item.source]: acc[item.source] ? [...acc[item.source], item] : [item],
|
||||
};
|
||||
}, {} as Record<string, { name: string; source: string }[]>);
|
||||
}, [items]);
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<li className={styles.noChildren} data-test="sidebar-no-services">
|
||||
<i>No {service} available</i>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.treeNav}>
|
||||
{Object.keys(itemsBySource).map(source => (
|
||||
<CollapsibleItems
|
||||
source={source}
|
||||
items={itemsBySource[source]}
|
||||
{...rest}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="142.514px" height="142.514px" viewBox="0 0 142.514 142.514" style="enable-background:new 0 0 142.514 142.514;"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M34.367,142.514c11.645,0,17.827-10.4,19.645-16.544c0.029-0.097,0.056-0.196,0.081-0.297
|
||||
c4.236-17.545,10.984-45.353,15.983-65.58h17.886c3.363,0,6.09-2.726,6.09-6.09c0-3.364-2.727-6.09-6.09-6.09H73.103
|
||||
c1.6-6.373,2.771-10.912,3.232-12.461l0.512-1.734c1.888-6.443,6.309-21.535,13.146-21.535c6.34,0,7.285,9.764,7.328,10.236
|
||||
c0.27,3.343,3.186,5.868,6.537,5.579c3.354-0.256,5.864-3.187,5.605-6.539C108.894,14.036,104.087,0,89.991,0
|
||||
C74.03,0,68.038,20.458,65.159,30.292l-0.49,1.659c-0.585,1.946-2.12,7.942-4.122,15.962H39.239c-3.364,0-6.09,2.726-6.09,6.09
|
||||
c0,3.364,2.726,6.09,6.09,6.09H57.53c-6.253,25.362-14.334,58.815-15.223,62.498c-0.332,0.965-2.829,7.742-7.937,7.742
|
||||
c-7.8,0-11.177-10.948-11.204-11.03c-0.936-3.229-4.305-5.098-7.544-4.156c-3.23,0.937-5.092,4.314-4.156,7.545
|
||||
C13.597,130.053,20.816,142.514,34.367,142.514z" fill="#767E93"/>
|
||||
<path d="M124.685,126.809c3.589,0,6.605-2.549,6.605-6.607c0-1.885-0.754-3.586-2.359-5.474l-12.646-14.534l12.271-14.346
|
||||
c1.132-1.416,1.98-2.926,1.98-4.908c0-3.59-2.927-6.231-6.703-6.231c-2.547,0-4.527,1.604-6.229,3.684l-9.531,12.454L98.73,78.391
|
||||
c-1.89-2.357-3.869-3.682-6.7-3.682c-3.59,0-6.607,2.551-6.607,6.609c0,1.885,0.756,3.586,2.357,5.471l11.799,13.592
|
||||
L86.647,115.67c-1.227,1.416-1.98,2.926-1.98,4.908c0,3.589,2.926,6.229,6.699,6.229c2.549,0,4.53-1.604,6.229-3.682l10.19-13.4
|
||||
l10.193,13.4C119.872,125.488,121.854,126.809,124.685,126.809z" fill="#767E93"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="142.514px" height="142.514px" viewBox="0 0 142.514 142.514" style="enable-background:new 0 0 142.514 142.514;"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M34.367,142.514c11.645,0,17.827-10.4,19.645-16.544c0.029-0.097,0.056-0.196,0.081-0.297
|
||||
c4.236-17.545,10.984-45.353,15.983-65.58h17.886c3.363,0,6.09-2.726,6.09-6.09c0-3.364-2.727-6.09-6.09-6.09H73.103
|
||||
c1.6-6.373,2.771-10.912,3.232-12.461l0.512-1.734c1.888-6.443,6.309-21.535,13.146-21.535c6.34,0,7.285,9.764,7.328,10.236
|
||||
c0.27,3.343,3.186,5.868,6.537,5.579c3.354-0.256,5.864-3.187,5.605-6.539C108.894,14.036,104.087,0,89.991,0
|
||||
C74.03,0,68.038,20.458,65.159,30.292l-0.49,1.659c-0.585,1.946-2.12,7.942-4.122,15.962H39.239c-3.364,0-6.09,2.726-6.09,6.09
|
||||
c0,3.364,2.726,6.09,6.09,6.09H57.53c-6.253,25.362-14.334,58.815-15.223,62.498c-0.332,0.965-2.829,7.742-7.937,7.742
|
||||
c-7.8,0-11.177-10.948-11.204-11.03c-0.936-3.229-4.305-5.098-7.544-4.156c-3.23,0.937-5.092,4.314-4.156,7.545
|
||||
C13.597,130.053,20.816,142.514,34.367,142.514z" fill="#fd9540"/>
|
||||
<path d="M124.685,126.809c3.589,0,6.605-2.549,6.605-6.607c0-1.885-0.754-3.586-2.359-5.474l-12.646-14.534l12.271-14.346
|
||||
c1.132-1.416,1.98-2.926,1.98-4.908c0-3.59-2.927-6.231-6.703-6.231c-2.547,0-4.527,1.604-6.229,3.684l-9.531,12.454L98.73,78.391
|
||||
c-1.89-2.357-3.869-3.682-6.7-3.682c-3.59,0-6.607,2.551-6.607,6.609c0,1.885,0.756,3.586,2.357,5.471l11.799,13.592
|
||||
L86.647,115.67c-1.227,1.416-1.98,2.926-1.98,4.908c0,3.589,2.926,6.229,6.699,6.229c2.549,0,4.53-1.604,6.229-3.682l10.19-13.4
|
||||
l10.193,13.4C119.872,125.488,121.854,126.809,124.685,126.809z" fill="#fd9540"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import styles from '../../Common.module.scss';
|
||||
|
||||
interface PageContainerProps extends React.ComponentProps<'div'> {
|
||||
helmet: string;
|
||||
leftContainer: React.ReactNode;
|
||||
}
|
||||
|
||||
const PageContainer: React.FC<PageContainerProps> = ({
|
||||
helmet,
|
||||
leftContainer,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Helmet title={helmet} />
|
||||
<div
|
||||
className={`${styles.wd20} ${styles.align_left} ${styles.height100}`}
|
||||
>
|
||||
{leftContainer}
|
||||
</div>
|
||||
<div className={styles.wd80}>{children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageContainer;
|
@ -0,0 +1,212 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
|
||||
import { generateHeaderSyms } from './HeaderReducer';
|
||||
import DropdownButton from '../../DropdownButton/DropdownButton';
|
||||
|
||||
class Header extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...generateHeaderSyms(props.eventPrefix),
|
||||
};
|
||||
}
|
||||
componentWillUnmount() {
|
||||
// Reset the header whenever it is unmounted
|
||||
this.props.dispatch({
|
||||
type: this.state.RESET_HEADER,
|
||||
});
|
||||
}
|
||||
getIndex(e) {
|
||||
const indexId = e.target.getAttribute('data-index-id');
|
||||
return parseInt(indexId, 10);
|
||||
}
|
||||
getTitle(val, k) {
|
||||
return val.filter(v => v.value === k);
|
||||
}
|
||||
headerKeyChange(e) {
|
||||
const indexId = this.getIndex(e);
|
||||
if (indexId < 0) {
|
||||
console.error('Unable to handle event');
|
||||
return;
|
||||
}
|
||||
Promise.all([
|
||||
this.props.dispatch({
|
||||
type: this.state.HEADER_KEY_CHANGE,
|
||||
data: {
|
||||
name: e.target.value,
|
||||
index: indexId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
checkAndAddNew(e) {
|
||||
const indexId = this.getIndex(e);
|
||||
if (indexId < 0) {
|
||||
console.error('Unable to handle event');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.props.headers[indexId].name &&
|
||||
this.props.headers[indexId].name.length > 0 &&
|
||||
indexId === this.props.headers.length - 1
|
||||
) {
|
||||
Promise.all([this.props.dispatch({ type: this.state.ADD_NEW_HEADER })]);
|
||||
}
|
||||
}
|
||||
headerValueChange(e) {
|
||||
const indexId = this.getIndex(e);
|
||||
if (indexId < 0) {
|
||||
console.error('Unable to handle event');
|
||||
return;
|
||||
}
|
||||
this.props.dispatch({
|
||||
type: this.state.HEADER_VALUE_CHANGE,
|
||||
data: {
|
||||
value: e.target.value,
|
||||
index: indexId,
|
||||
},
|
||||
});
|
||||
}
|
||||
headerTypeChange(e) {
|
||||
const indexId = this.getIndex(e);
|
||||
const typeValue = e.target.getAttribute('value');
|
||||
if (indexId < 0) {
|
||||
console.error('Unable to handle event');
|
||||
return;
|
||||
}
|
||||
this.props.dispatch({
|
||||
type: this.state.HEADER_VALUE_TYPE_CHANGE,
|
||||
data: {
|
||||
type: typeValue,
|
||||
index: indexId,
|
||||
},
|
||||
});
|
||||
}
|
||||
deleteHeader(e) {
|
||||
const indexId = this.getIndex(e);
|
||||
if (indexId < 0) {
|
||||
console.error('Unable to handle event');
|
||||
return;
|
||||
}
|
||||
this.props.dispatch({
|
||||
type: this.state.DELETE_HEADER,
|
||||
data: {
|
||||
type: e.target.value,
|
||||
index: indexId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const styles = require('./Header.module.scss');
|
||||
const { isDisabled } = this.props;
|
||||
const generateHeaderHtml = this.props.headers.map((h, i) => {
|
||||
const getTitle = this.getTitle(this.props.typeOptions, h.type);
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
styles.common_header_wrapper +
|
||||
' ' +
|
||||
styles.display_flex +
|
||||
' form-group'
|
||||
}
|
||||
key={i}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
className={
|
||||
styles.input +
|
||||
' form-control ' +
|
||||
styles.add_mar_right +
|
||||
' ' +
|
||||
styles.defaultWidth
|
||||
}
|
||||
data-index-id={i}
|
||||
value={h.name}
|
||||
onChange={this.headerKeyChange.bind(this)}
|
||||
onBlur={this.checkAndAddNew.bind(this)}
|
||||
placeholder={this.props.keyInputPlaceholder}
|
||||
disabled={isDisabled}
|
||||
data-test={`remote-schema-header-test${i + 1}-key`}
|
||||
/>
|
||||
<span className={styles.header_colon}>:</span>
|
||||
<span className={styles.value_wd}>
|
||||
<DropdownButton
|
||||
dropdownOptions={this.props.typeOptions}
|
||||
title={getTitle.length > 0 ? getTitle[0].display_text : 'Value'}
|
||||
dataKey={h.type}
|
||||
dataIndex={i}
|
||||
onButtonChange={this.headerTypeChange.bind(this)}
|
||||
onInputChange={this.headerValueChange.bind(this)}
|
||||
inputVal={h.value}
|
||||
disabled={isDisabled}
|
||||
id={'common-header-' + (i + 1)}
|
||||
inputPlaceHolder={this.props.placeHolderText(h.type)}
|
||||
testId={`remote-schema-header-test${i + 1}`}
|
||||
/>
|
||||
</span>
|
||||
{/*
|
||||
<select
|
||||
className={
|
||||
'form-control ' +
|
||||
styles.add_pad_left +
|
||||
' ' +
|
||||
styles.add_mar_right +
|
||||
' ' +
|
||||
styles.defaultWidth
|
||||
}
|
||||
value={h.type}
|
||||
onChange={this.headerTypeChange.bind(this)}
|
||||
data-index-id={i}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<option disabled value="">
|
||||
-- value type --
|
||||
</option>
|
||||
{this.props.typeOptions.map((o, k) => (
|
||||
<option key={k} value={o.value} data-index-id={i}>
|
||||
{o.display}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
className={
|
||||
styles.inputDefault +
|
||||
' form-control ' +
|
||||
styles.defaultWidth +
|
||||
' ' +
|
||||
styles.add_pad_left
|
||||
}
|
||||
placeholder="value"
|
||||
value={h.value}
|
||||
onChange={this.headerValueChange.bind(this)}
|
||||
data-index-id={i}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
*/}
|
||||
{i !== this.props.headers.length - 1 && !isDisabled ? (
|
||||
<FaTimes
|
||||
className={styles.fontAwosomeClose + ' h-lg w-lg'}
|
||||
onClick={this.deleteHeader.bind(this)}
|
||||
data-index-id={i}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return <div className={this.props.wrapper_class}>{generateHeaderHtml}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
headers: PropTypes.array,
|
||||
isDisabled: PropTypes.bool,
|
||||
typeOptions: PropTypes.array,
|
||||
placeHolderText: PropTypes.string,
|
||||
keyInputPlaceholder: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Header;
|
@ -0,0 +1,21 @@
|
||||
@import "../../Common.module";
|
||||
|
||||
.common_header_wrapper {
|
||||
.defaultWidth {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.add_mar_right {
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
||||
.header_colon {
|
||||
margin-right: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.value_wd {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
/* Default state */
|
||||
const defaultState = {
|
||||
headers: [
|
||||
{
|
||||
name: '',
|
||||
type: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/* Action constants */
|
||||
const generateHeaderSyms = (prefix = 'API_HEADER') => {
|
||||
// TODO: change this anti-pattern
|
||||
// There is no way to guarantee if the derived constants actually exists. The whole point of using constants is lost
|
||||
return {
|
||||
HEADER_KEY_CHANGE: `${prefix}/HEADER_KEY_CHANGE`,
|
||||
HEADER_VALUE_TYPE_CHANGE: `${prefix}/HEADER_VALUE_TYPE_CHANGE`,
|
||||
HEADER_VALUE_CHANGE: `${prefix}/HEADER_VALUE_CHANGE`,
|
||||
UPDATE_HEADERS: `${prefix}/UPDATE_HEADERS`,
|
||||
RESET_HEADER: `${prefix}/RESET_HEADER`,
|
||||
ADD_NEW_HEADER: `${prefix}/ADD_NEW_HEADER`,
|
||||
DELETE_HEADER: `${prefix}/DELETE_HEADER`,
|
||||
};
|
||||
};
|
||||
|
||||
const generateReducer = (eventPrefix, defaultHeaders) => {
|
||||
/* Action constants */
|
||||
if (defaultHeaders && defaultHeaders.length > 0) {
|
||||
defaultState.headers = [...defaultHeaders];
|
||||
}
|
||||
const {
|
||||
HEADER_KEY_CHANGE,
|
||||
HEADER_VALUE_CHANGE,
|
||||
HEADER_VALUE_TYPE_CHANGE,
|
||||
RESET_HEADER,
|
||||
DELETE_HEADER,
|
||||
ADD_NEW_HEADER,
|
||||
UPDATE_HEADERS,
|
||||
} = generateHeaderSyms(eventPrefix);
|
||||
|
||||
/* Reducer */
|
||||
const headerReducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case HEADER_KEY_CHANGE:
|
||||
return {
|
||||
...state,
|
||||
headers: [
|
||||
...state.headers.slice(0, action.data.index),
|
||||
{
|
||||
...state.headers[action.data.index],
|
||||
name: action.data.name,
|
||||
},
|
||||
...state.headers.slice(action.data.index + 1, state.headers.length),
|
||||
],
|
||||
};
|
||||
case HEADER_VALUE_TYPE_CHANGE:
|
||||
return {
|
||||
...state,
|
||||
headers: [
|
||||
...state.headers.slice(0, action.data.index),
|
||||
{
|
||||
...state.headers[action.data.index],
|
||||
type: action.data.type,
|
||||
},
|
||||
...state.headers.slice(action.data.index + 1, state.headers.length),
|
||||
],
|
||||
};
|
||||
case HEADER_VALUE_CHANGE:
|
||||
return {
|
||||
...state,
|
||||
headers: [
|
||||
...state.headers.slice(0, action.data.index),
|
||||
{
|
||||
...state.headers[action.data.index],
|
||||
value: action.data.value,
|
||||
},
|
||||
...state.headers.slice(action.data.index + 1, state.headers.length),
|
||||
],
|
||||
};
|
||||
case ADD_NEW_HEADER:
|
||||
return {
|
||||
...state,
|
||||
headers: [...state.headers, { ...defaultState.headers[0] }],
|
||||
};
|
||||
|
||||
case DELETE_HEADER:
|
||||
return {
|
||||
...state,
|
||||
headers: [
|
||||
...state.headers.slice(0, action.data.index),
|
||||
...state.headers.slice(action.data.index + 1, state.headers.length),
|
||||
],
|
||||
};
|
||||
case RESET_HEADER:
|
||||
return {
|
||||
...defaultState,
|
||||
};
|
||||
case UPDATE_HEADERS:
|
||||
return {
|
||||
...state,
|
||||
headers: [...action.data],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
};
|
||||
return headerReducer;
|
||||
};
|
||||
|
||||
export { generateHeaderSyms };
|
||||
export default generateReducer;
|
@ -0,0 +1,5 @@
|
||||
@import "../../Common.module";
|
||||
|
||||
.loader_ml {
|
||||
margin-left: 3px;
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { FaSpinner } from 'react-icons/fa';
|
||||
import styles from './ReusableTabs.module.scss';
|
||||
|
||||
export type Tabs = Record<
|
||||
string,
|
||||
{ display_text: string; display?: React.ReactNode }
|
||||
>;
|
||||
|
||||
type Props = {
|
||||
appPrefix: string;
|
||||
tabsInfo: Tabs;
|
||||
tabName: string;
|
||||
count?: number;
|
||||
baseUrl: string;
|
||||
showLoader: boolean;
|
||||
testPrefix: string;
|
||||
};
|
||||
|
||||
const Tabs: React.FC<Props> = ({
|
||||
appPrefix,
|
||||
tabsInfo,
|
||||
tabName,
|
||||
count,
|
||||
baseUrl,
|
||||
showLoader,
|
||||
testPrefix,
|
||||
}) => {
|
||||
let showCount = '';
|
||||
if (!(count === null || count === undefined)) {
|
||||
showCount = `(${count})`;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={styles.common_nav} key="reusable-tabs-1">
|
||||
<ul className="nav nav-pills">
|
||||
{Object.keys(tabsInfo).map((t: string) => (
|
||||
<li
|
||||
role="presentation"
|
||||
className={tabName === t ? styles.active : ''}
|
||||
key={t}
|
||||
>
|
||||
<Link
|
||||
to={`${baseUrl}/${t}`}
|
||||
data-test={`${
|
||||
testPrefix ? `${testPrefix}-` : ''
|
||||
}${appPrefix.slice(1)}-${t}`}
|
||||
>
|
||||
{tabsInfo[t].display || tabsInfo[t].display_text}{' '}
|
||||
{tabName === t ? showCount : null}
|
||||
{tabName === t && showLoader ? (
|
||||
<span className={styles.loader_ml}>
|
||||
<FaSpinner className="animate-spin" />
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="clearfix" key="reusable-tabs-2" />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
@ -0,0 +1,77 @@
|
||||
@import "../../Common.module";
|
||||
|
||||
.container {
|
||||
}
|
||||
|
||||
.flexRow {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.padd_left_remove {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.add_btn {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.account {
|
||||
padding: 20px 0;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
height: $mainContainerHeight;
|
||||
overflow: auto;
|
||||
background: #444;
|
||||
/* Taken from boostrap navbar-inverse-color variables */
|
||||
color: lighten(lighten(#000, 46.7%), 15%);
|
||||
|
||||
hr {
|
||||
margin: 0;
|
||||
/* taken from bootsrap $navbar-inverse-color variable */
|
||||
border-color: lighten(lighten(#000, 46.7%), 15%);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-top: 10px;
|
||||
padding-left: 7px;
|
||||
|
||||
li {
|
||||
padding: 7px 0;
|
||||
transition: color 0.5s;
|
||||
|
||||
a,a:visited {
|
||||
/* Taken from boostrap navbar-inverse-link-color variables */
|
||||
color: lighten(lighten(#000, 46.7%), 15%);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
/* taken from bootsrap $navbar-inverse-link-hover-color variable */
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover {
|
||||
padding: 7px 0;
|
||||
/* taken from bootsrap $navbar-inverse-link-hover-color variable */
|
||||
color: #fff;
|
||||
transition: color 0.5s;
|
||||
pointer: cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0;
|
||||
padding-left: 15px;
|
||||
height: $mainContainerHeight;
|
||||
overflow: auto;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.rightBar {
|
||||
padding-left: 15px;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import styles from './RightContainer.module.scss';
|
||||
|
||||
const RightContainer: React.FC = ({ children }) => {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div
|
||||
className={`${styles.main} ${styles.padd_left_remove} ${styles.padd_top}`}
|
||||
>
|
||||
<div className={`${styles.rightBar}`}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RightContainer;
|
@ -0,0 +1,3 @@
|
||||
import RightContainer from './RightContainer';
|
||||
|
||||
export { RightContainer };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user