platform(nx): initial oss migration

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5429
GitOrigin-RevId: 3df08906d9c3cd6a9f75b933469bce4782c4a8d5
This commit is contained in:
Nicolas Beaussart 2022-08-18 21:36:39 +02:00 committed by hasura-bot
parent dea80bfac7
commit 38ffe84ce3
1408 changed files with 215982 additions and 1893 deletions

1
frontend/.nvmrc Normal file
View File

@ -0,0 +1 @@
v16.15.1

View 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;
};

View File

@ -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": {

View File

@ -1 +0,0 @@
/* Your styles goes here. */

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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&apos;re up and running</span>
</h2>
<a href="#commands"> What&apos;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&apos;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;

View File

@ -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>

View File

@ -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",

View File

@ -8,7 +8,7 @@
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noPropertyAccessFromIndexSignature": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},

View File

@ -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"
]
}

View File

@ -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"
}
}
]
}

View File

@ -1 +1,2 @@
export * from './lib/console-legacy-oss';
export const add = (a: number, b: number): number => a + b;
export { App } from './lib/client';

View 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 };

View 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;

View 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>
);

View File

@ -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,
};

View 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));

View File

@ -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;
}
}

View File

@ -0,0 +1,10 @@
const defaultState = {
percent: 0,
intervalTime: 200,
ongoingRequest: false,
requestSuccess: null,
requestError: null,
error: null,
};
export default defaultState;

View File

@ -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,
};

View File

@ -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;

View File

@ -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';
}
};

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,3 @@
import Button from './Button';
export default Button;

View File

@ -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

View File

@ -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&apos;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&apos;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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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}>
&#123;&#123;$base_url&#125;&#125;
</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;

View File

@ -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;

View File

@ -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">&#123;&#123;$body&#125;&#125;</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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
};

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,11 @@
@import '../../CustomInputAutoSuggest/Theme.module';
.suggestionsContainerOpen {
top: 30px;
width: 280px;
left: 5px;
}
.suggestion {
padding: 6px 12px;
}

View File

@ -0,0 +1,10 @@
@import '../../CustomInputAutoSuggest/Theme.module';
.suggestionsContainerOpen {
top: 30px;
width: 100%;
}
.suggestion {
padding: 6px 12px;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,
};
};

View File

@ -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;

View File

@ -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;
}
};

View File

@ -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;

View File

@ -0,0 +1,5 @@
@import "../Common.module";
.headerInputWidth {
width: 300px
}

View File

@ -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;

View File

@ -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;
})
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -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;

View File

@ -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}
/>
</>
);

View File

@ -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}>
&nbsp;
<FaAngleRight key={`${b.title}-arrow`} aria-hidden="true" />
&nbsp;
</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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -0,0 +1,5 @@
@import "../../Common.module";
.loader_ml {
margin-left: 3px;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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