Skip to content

Commit 845b8ae

Browse files
feat: Enhance scaffold cloud UI (#18909)
1 parent c05da5a commit 845b8ae

17 files changed

Lines changed: 350 additions & 117 deletions

scaffold/cmd/templates/cloud-config-ui/.eslintrc.json.tpl

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
{
22
"root": true,
3+
"plugins": ["import", "custom-rules"],
34
"extends": [
45
"eslint:recommended",
56
"plugin:react/recommended",
67
"plugin:react-hooks/recommended",
78
"plugin:@typescript-eslint/recommended",
89
"plugin:jsx-a11y/recommended",
910
"plugin:prettier/recommended",
10-
"plugin:unicorn/recommended"
11+
"plugin:unicorn/recommended",
12+
"plugin:import/typescript"
1113
],
1214
"globals": {
1315
"JSX": true
@@ -18,6 +20,7 @@
1820
"node": true
1921
},
2022
"rules": {
23+
"custom-rules/mui-material-imports": "error",
2124
"@typescript-eslint/explicit-module-boundary-types": 0,
2225
"@typescript-eslint/no-empty-interface": "error",
2326
"@typescript-eslint/no-explicit-any": 0,
@@ -44,6 +47,37 @@
4447
"depth": 5
4548
}
4649
],
50+
"import/newline-after-import": "error",
51+
"import/no-duplicates": ["error", { "prefer-inline": false }],
52+
"import/no-unresolved": "error",
53+
"import/order": [
54+
"error",
55+
{
56+
"alphabetize": {
57+
"caseInsensitive": true,
58+
"order": "asc"
59+
},
60+
"groups": [
61+
"builtin",
62+
"external",
63+
"internal",
64+
["sibling", "parent"],
65+
"index",
66+
"object",
67+
"type",
68+
"unknown"
69+
],
70+
"newlines-between": "always-and-inside-groups",
71+
"pathGroups": [
72+
{
73+
"group": "external",
74+
"pattern": "react",
75+
"position": "before"
76+
}
77+
],
78+
"pathGroupsExcludedImportTypes": ["react"]
79+
}
80+
],
4781
"jsx-a11y/media-has-caption": "off",
4882
"jsx-a11y/no-autofocus": "off",
4983
"jsx-a11y/no-onchange": "off",
@@ -56,7 +90,7 @@
5690
"no-prototype-builtins": 0,
5791
"no-undef": "error",
5892
"no-unexpected-multiline": "error",
59-
"no-unused-vars": "off",
93+
"no-unused-vars": "error",
6094
"object-curly-newline": [
6195
"error",
6296
{
@@ -89,13 +123,19 @@
89123
"react/jsx-boolean-value": ["error", "always"],
90124
"react/jsx-first-prop-new-line": ["error", "multiline"],
91125
"react/jsx-fragments": ["error", "syntax"],
92-
"react/jsx-uses-react": "off",
126+
"react/jsx-uses-react": "error",
127+
"react/jsx-uses-vars": "error",
93128
"react/react-in-jsx-scope": "off",
94129
"unicorn/filename-case": "off",
95130
"unicorn/no-nested-ternary": "off",
96131
"unicorn/no-null": "off",
97132
"unicorn/no-useless-undefined": "off",
98133
"unicorn/prevent-abbreviations": "off"
99134
},
100-
"ignorePatterns": ["node_modules/", "dist/", "*.js", "*.cjs"]
135+
"ignorePatterns": ["node_modules/", "dist/", "*.js", "*.cjs"],
136+
"settings": {
137+
"react": {
138+
"version": "detect"
139+
}
140+
}
101141
}

scaffold/cmd/templates/cloud-config-ui/README.md.tpl

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@ Make sure to copy `.env.example.json` to `.env.json` inside `src` folder before
1010

1111
## Plugin tables
1212

13-
In case your plugin is a source plugin and you want to use the list of tables in your plugin UI, then make sure to build the plugin first to get a generated list of tables. Inside the plugin root directory run `go build`. This will generate a plugin file and then you should run `./{generated-plugin-file-name} doc --format=json docs`. The generated file with table should be located inside `docs/__tables.json`. Then navigate back to the `cloud-config-ui` directory inside your plugin and run `npm install`. After that you can import the list of tables inside the application like this:
13+
In case your plugin is a source plugin and you want to use the list of tables in your plugin UI, then make sure to build the plugin first to get a generated list of tables. Inside the plugin root directory run `go build`. This will generate a plugin file and then you should run `./{plugin-name} doc --format=json docs`. The generated file with table should be located inside `docs/__tables.json`. Then navigate back to the `cloud-config-ui` directory inside your plugin and run `npm start` or `npm run build`, they both will copy the generated `__tables.json` file to the `src/data` folder. After that you can import the list of tables inside the application like this:
1414

1515
```ts
16-
import pluginTables from '@cloudquery-plugin/tables';
16+
import pluginTables from 'data/tables.json';
1717
```
1818

19-
If you change your plugin configuration that should update the list of plugins, you only need to regenerate `__tables.json`, the frontend app will automatically detect changes there.
20-
21-
There is no need to commit `__tables.json` file as its content will be automatically included during the build of the plugin UI.
22-
2319
## Available Scripts
2420

2521
In the project directory, you can run:
Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
1-
import test, { expect } from "@playwright/test";
2-
import fs from "node:fs";
3-
import YAML from "yaml";
1+
import fs from 'node:fs';
42

5-
test("Submit the form", async ({ page }) => {
6-
await page.goto("/");
3+
import test, { expect } from '@playwright/test';
4+
import YAML from 'yaml';
5+
6+
test('Submit the form', async ({ page }) => {
7+
await page.goto('/');
78
89
// fill the form
910
10-
await page.getByRole("button", { name: "Submit" }).click();
11+
await page.getByRole('button', { name: 'Submit' }).click();
1112
const valuesText = await page
12-
.locator("text=Values:")
13-
.locator("xpath=following-sibling::*[1]")
13+
.locator('text=Values:')
14+
.locator('xpath=following-sibling::*[1]')
1415
.textContent();
1516

1617
expect(valuesText).toBeTruthy();
1718

18-
if (process.env.E2E_TESTS_GENERATE_CONFIG === "true") {
19+
if (process.env.E2E_TESTS_GENERATE_CONFIG === 'true') {
1920
const spec = JSON.parse(valuesText as string);
2021
const localConfig = YAML.stringify({
21-
kind: "{plugin_kind}",
22+
kind: '{plugin_kind}',
2223
spec: {
23-
name: "{plugin_name}",
24-
registry: "local",
25-
path: "../{plugin_name}",
26-
destinations: ["{plugin_name}"],
24+
name: '{plugin_name}',
25+
registry: 'local',
26+
path: '../{plugin_name}',
27+
destinations: ['{plugin_name}'],
2728
spec: spec.spec,
2829
2930
// use for destination
@@ -38,14 +39,14 @@ test("Submit the form", async ({ page }) => {
3839
});
3940

4041
const anotherConfig = YAML.stringify({
41-
kind: "{source | destination}", // should be opposite to localConfig
42+
kind: '{source | destination}', // should be opposite to localConfig
4243
spec: {
43-
name: "postgresql",
44-
path: "cloudquery/postgresql",
45-
registry: "cloudquery",
46-
version: "{v6.2.5 | v8.2.7}", // use v6.2.5 for source or v8.2.7 for destination
44+
name: 'postgresql',
45+
path: 'cloudquery/postgresql',
46+
registry: 'cloudquery',
47+
version: '{v6.2.5 | v8.2.7}', // use v6.2.5 for source or v8.2.7 for destination
4748
spec: {
48-
connection_string: "test",
49+
connection_string: 'test',
4950
},
5051

5152
// use for source
@@ -54,22 +55,17 @@ test("Submit the form", async ({ page }) => {
5455
},
5556
});
5657

57-
if (!fs.existsSync("temp")) {
58-
fs.mkdirSync("temp");
58+
if (!fs.existsSync('temp')) {
59+
fs.mkdirSync('temp');
5960
}
6061

61-
fs.writeFileSync(
62-
"./temp/config.yml",
63-
`${localConfig}---\n${anotherConfig}`
64-
);
62+
fs.writeFileSync('./temp/config.yml', `${localConfig}---\n${anotherConfig}`);
6563

6664
fs.writeFileSync(
67-
"./temp/.env",
65+
'./temp/.env',
6866
`${spec.envs
69-
.map(
70-
(env: { name: string; value: string }) => `${env.name}=${env.value}`
71-
)
72-
.join("\n")}`
67+
.map((env: { name: string; value: string }) => `${env.name}=${env.value}`)
68+
.join('\n')}`,
7369
);
7470
}
7571
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
rules: {
3+
'mui-material-imports': require('./mui-material-imports.cjs'),
4+
},
5+
};
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
module.exports = {
2+
meta: {
3+
type: 'suggestion',
4+
docs: {
5+
description: 'disallow named imports from @mui/material',
6+
category: 'Best Practices',
7+
recommended: false,
8+
},
9+
fixable: 'code',
10+
schema: [], // no options
11+
},
12+
create(context) {
13+
return {
14+
ImportDeclaration(node) {
15+
if (node.source.value === '@mui/material' && node.specifiers.length > 0) {
16+
const namedImports = node.specifiers.filter(
17+
(specifier) => specifier.type === 'ImportSpecifier',
18+
);
19+
20+
if (namedImports.length > 0) {
21+
context.report({
22+
node,
23+
message: 'Use default import for each module from @mui/material',
24+
fix(fixer) {
25+
const fixes = namedImports.map((specifier) => {
26+
const importName = specifier.local.name;
27+
let newImportStatement = `import ${importName} from '@mui/material/${importName}';`;
28+
switch (importName) {
29+
case 'useTheme': {
30+
newImportStatement = `import useTheme from '@mui/material/styles/useTheme';`;
31+
break;
32+
}
33+
case 'useMediaQuery': {
34+
newImportStatement = `import useMediaQuery from '@mui/material/useMediaQuery';`;
35+
break;
36+
}
37+
case 'ThemeProvider': {
38+
newImportStatement = `import ThemeProvider from '@mui/material/styles/ThemeProvider';`;
39+
break;
40+
}
41+
case 'createTheme': {
42+
newImportStatement = `import createTheme from '@mui/material/styles/createTheme';`;
43+
break;
44+
}
45+
case 'createTypography': {
46+
newImportStatement = `import createTypography from '@mui/material/styles/createTypography';`;
47+
break;
48+
}
49+
case 'styled': {
50+
newImportStatement = `import styled from '@mui/material/styles/styled';`;
51+
break;
52+
}
53+
case 'alpha': {
54+
newImportStatement = `import { alpha } from '@mui/material/styles';`;
55+
break;
56+
}
57+
case 'Theme': {
58+
newImportStatement = `import { Theme } from '@mui/material/styles';`;
59+
break;
60+
}
61+
case 'Palette': {
62+
newImportStatement = `import { Palette } from '@mui/material/styles/createPalette';`;
63+
break;
64+
}
65+
case 'PaletteColor': {
66+
newImportStatement = `import { PaletteColor } from '@mui/material/styles/createPalette';`;
67+
break;
68+
}
69+
case 'PaletteOptions': {
70+
newImportStatement = `import { PaletteOptions } from '@mui/material/styles/createPalette';`;
71+
break;
72+
}
73+
case 'Components': {
74+
newImportStatement = `import { Components } from '@mui/material/styles';`;
75+
break;
76+
}
77+
case 'Shadows': {
78+
newImportStatement = `import { Shadows } from '@mui/material/styles';`;
79+
break;
80+
}
81+
case 'Breakpoint': {
82+
newImportStatement = `import { Breakpoint } from '@mui/material/styles';`;
83+
break;
84+
}
85+
case 'TypographyOptions': {
86+
newImportStatement = `import { TypographyOptions } from '@mui/material/styles/createTypography';`;
87+
break;
88+
}
89+
}
90+
return fixer.insertTextBefore(node, newImportStatement + '\n');
91+
});
92+
93+
const removeNamedImports = fixer.remove(node);
94+
95+
return fixes.concat(removeNamedImports);
96+
},
97+
});
98+
}
99+
} else if (node.source.value === '@mui/icons-material' && node.specifiers.length > 0) {
100+
const namedImports = node.specifiers.filter(
101+
(specifier) => specifier.type === 'ImportSpecifier',
102+
);
103+
104+
if (namedImports.length > 0) {
105+
context.report({
106+
node,
107+
message: 'Use default import for each module from @mui/icons-material',
108+
fix(fixer) {
109+
const fixes = namedImports.map((specifier) => {
110+
const importName = specifier.local.name;
111+
const newImportStatement = `import ${importName}Icon from '@mui/icons-material/${importName}';`;
112+
113+
return fixer.insertTextBefore(node, newImportStatement + '\n');
114+
});
115+
116+
const removeNamedImports = fixer.remove(node);
117+
118+
return fixes.concat(removeNamedImports);
119+
},
120+
});
121+
}
122+
} else if (node.source.value === '@mui/lab' && node.specifiers.length > 0) {
123+
const namedImports = node.specifiers.filter(
124+
(specifier) => specifier.type === 'ImportSpecifier',
125+
);
126+
127+
if (namedImports.length > 0) {
128+
context.report({
129+
node,
130+
message: 'Use default import for each module from @mui/lab',
131+
fix(fixer) {
132+
const fixes = namedImports.map((specifier) => {
133+
const importName = specifier.local.name;
134+
const newImportStatement = `import ${importName} from '@mui/lab/${importName}';`;
135+
136+
return fixer.insertTextBefore(node, newImportStatement + '\n');
137+
});
138+
139+
const removeNamedImports = fixer.remove(node);
140+
141+
return fixes.concat(removeNamedImports);
142+
},
143+
});
144+
}
145+
}
146+
},
147+
};
148+
},
149+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "eslint-plugin-custom-rules",
3+
"main": "index.cjs"
4+
}

0 commit comments

Comments
 (0)