From 25f1ede3e1eccee288e60d2afc5eed1bc2f94d84 Mon Sep 17 00:00:00 2001
From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com>
Date: Thu, 11 May 2023 12:00:27 -0700
Subject: [PATCH] Create UI component generator (#11644)
## `generate`: a script to automatically generate UI component
boilerplate
* Putting in this PR now since @fleetdm/frontend folks seemed keen to
use this immediately
* TODO:
- create Makefile command for using this functionality from the project
root
- improve documentation
---------
Co-authored-by: Jacob Shandling
---
docs/Contributing/adding-new-UI-components.md | 12 +++
frontend/components/generate | 87 +++++++++++++++++++
2 files changed, 99 insertions(+)
create mode 100644 docs/Contributing/adding-new-UI-components.md
create mode 100755 frontend/components/generate
diff --git a/docs/Contributing/adding-new-UI-components.md b/docs/Contributing/adding-new-UI-components.md
new file mode 100644
index 0000000000..c5155a172f
--- /dev/null
+++ b/docs/Contributing/adding-new-UI-components.md
@@ -0,0 +1,12 @@
+# Using `generate` to create boilerplate for a new Fleet UI component
+
+## Basic steps to create a new UI component
+
+1. `cd` into `fleet/frontend/components`
+2. Run `./generate -n NewComponentName`, replacing `NewComponentName` with the desired name for your
+ new component.
+3. A directory and all boilerplate for the various files of the component will be generated in
+ `fleet/frontend/components`
+
+You can also run `./generate -h` for information about the other options available (overwrite and
+specifying destination)
diff --git a/frontend/components/generate b/frontend/components/generate
new file mode 100755
index 0000000000..14f6c68b3c
--- /dev/null
+++ b/frontend/components/generate
@@ -0,0 +1,87 @@
+#!/usr/bin/python3
+
+import argparse, os, re
+
+def validate_name(name: str):
+ return len(name) > 1 and name != name.lower() and name != name.upper() and name[0].isupper()
+
+def get_component_classname(name):
+ split_by_caps = re.findall('[A-Z][^A-Z]*', name)
+ return '-'.join([word.lower() for word in split_by_caps])
+
+def createFleetTSXComponent(name: str, output_path: str, overwrite: bool):
+ target = os.path.join(output_path, name)
+
+ # handle existing directory of the same name
+ try:
+ os.mkdir(target)
+ except FileExistsError:
+ if overwrite:
+ print("A directory with that name already exists, overwriting it.")
+ os.system(f"rm -r {target}")
+ os.mkdir(target)
+ else:
+ print("A directory with that name already exists, aborting.\nTo recursively overwrite existing directories of the same name, pass the '-o' flag.")
+ return 1
+
+ print (f"Creating component \"{name}\" in {os.path.abspath(output_path)}")
+
+ base_class_name = get_component_classname(name)
+ react_import_ts = "import React from \"react\";"
+ component_import_ts = f"import {name} from \"./{name}\";"
+
+ # write index file
+ os.system(
+ f"echo 'export {{ default }} from \"./{name}\";' > {target}/index.ts"
+ )
+
+ # write the component
+ base_class_ts = f"const baseClass = \"{base_class_name}\";"
+ interface_name_ts = f"I{name}"
+ interface_definition_ts = f"interface {interface_name_ts} {{\n\n}}"
+ component_ts = f"const {name} = ({{}}: {interface_name_ts}) => {{\n return (\n \n\n
\n );\n}};"
+ export_ts = f"export default {name};"
+ component_ts = f"{react_import_ts}\n\n{base_class_ts}\n\n{interface_definition_ts}\n\n{component_ts}\n\n{export_ts}"
+ os.system(
+ f"echo '{component_ts}' > {target}/{name}.tsx"
+ )
+
+ # write stylesheet
+ os.system(
+ f"echo '.{base_class_name} {{\n\n}}' > {target}/_styles.scss"
+ )
+
+ # write_tests(target, name)
+ library_imports = "import { render, screen } from \"@testing-library/react\";"
+ describe_ts = f"describe(\"{name} component\", () => {{\n it(\"\", () => {{\n\n }});\n}});"
+ tests_ts = f"{react_import_ts}\n\n{library_imports}\n\n{component_import_ts}\n\n{describe_ts}"
+ os.system(f"echo '{tests_ts}' > {target}/{name}.tests.tsx")
+
+ # write storybook
+ storybook_imports_ts = f"import {{ Meta, StoryObj }} from \"@storybook/react\";\n\n{component_import_ts}"
+ storybook_meta_ts = f"const meta: Meta = {{\n title: \"Components/{name}\",\n component: {name},\n}};"
+ default_export_ts = "export default meta;"
+ type_ts = f"type Story = StoryObj;"
+ storybook_export_ts = "export const Basic: Story = {};"
+ storybook_ts = f"{storybook_imports_ts}\n\n{storybook_meta_ts}\n\n{default_export_ts}\n\n{type_ts}\n\n{storybook_export_ts}"
+ os.system(f"echo '{storybook_ts}' > {target}/{name}.stories.tsx")
+
+ return 0
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Generate all boilerplate for a new Fleet UI component")
+ parser.add_argument("-n", "--name", type=str, required=True,
+ help="The PascalCase name for the new component")
+ parser.add_argument("-p", "--output_path", type=str, required=False, help="Absolute or relative path at which to create the component's containing directory. Defaults to the current directory.")
+ parser.add_argument("-o", "--overwrite_directory", required=False, action="store_true", help="Flag to overwrite an existing component directory with the same name if it already exists. Defaults to False.")
+ args = parser.parse_args()
+
+ name, output_path, overwrite = args.name, args.output_path if args.output_path else ".", args.overwrite_directory
+
+ if validate_name(name):
+ createFleetTSXComponent(name, output_path, overwrite)
+ else:
+ print("Enter a PascalCase name")
+
+