Skip to content

Commit

Permalink
feat(ui): fixed raw data modal and added copy to clipboard button
Browse files Browse the repository at this point in the history
  • Loading branch information
pehlicd committed May 21, 2024
1 parent 6f1c98d commit 39840bb
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 32 deletions.
32 changes: 30 additions & 2 deletions internal/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package internal
import (
"context"
"fmt"
"gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/client-go/kubernetes/scheme"
)

func (app App) GetBindings() (*Bindings, error) {
Expand Down Expand Up @@ -37,7 +41,7 @@ func GenerateData(bindings *Bindings) []Data {
Kind: "ClusterRoleBinding",
Subjects: crb.Subjects,
RoleRef: crb.RoleRef,
Raw: crb.String(),
Raw: yamlParser(&crb),
})
}

Expand All @@ -48,9 +52,33 @@ func GenerateData(bindings *Bindings) []Data {
Kind: "RoleBinding",
Subjects: rb.Subjects,
RoleRef: rb.RoleRef,
Raw: rb.String(),
Raw: yamlParser(&rb),
})
}

return data
}

func yamlParser(obj runtime.Object) string {
// Convert the object to YAML
s := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, json.SerializerOptions{Yaml: true, Pretty: true})
o, err := runtime.Encode(s, obj)
if err != nil {
return ""
}

// Unmarshal the JSON into a generic map
var yamlObj map[string]interface{}
err = yaml.Unmarshal(o, &yamlObj)
if err != nil {
return err.Error()
}

// Marshal the map back into YAML
yamlData, err := yaml.Marshal(yamlObj)
if err != nil {
return err.Error()
}

return string(yamlData)
}
2 changes: 1 addition & 1 deletion internal/statik/statik.go

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions ui/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,22 @@ export const PlusIcon = ({size = 24, width, height, ...props}: IconSvgProps) =>
</g>
</svg>
);

export const CopyIcon = ({ size, height, width, ...props }: IconSvgProps) => {
return (
<svg
fill="none"
height={height || 20}
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width={width || 20}
{...props}
>
<path d="M6 17C4.89543 17 4 16.1046 4 15V5C4 3.89543 4.89543 3 6 3H13C13.7403 3 14.3866 3.4022 14.7324 4M11 21H18C19.1046 21 20 20.1046 20 19V9C20 7.89543 19.1046 7 18 7H11C9.89543 7, 9 7.89543 9 9V19C9 20.1046 9.89543 21 11 21Z" />
</svg>
);
};
87 changes: 58 additions & 29 deletions ui/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import {
ChipProps,
SortDescriptor
} from "@nextui-org/react";
import {SearchIcon, VerticalDotsIcon, ChevronDownIcon, RefreshIcon} from "@/components/icons";
import {Modal, ModalBody, ModalContent, ModalHeader} from "@nextui-org/modal";
import {Card, CardBody, CardHeader} from "@nextui-org/card";
import { stringify } from 'yaml';
import { SearchIcon, VerticalDotsIcon, ChevronDownIcon, RefreshIcon, CopyIcon } from "@/components/icons";
import { Modal, ModalBody, ModalContent } from "@nextui-org/modal";
import { Card, CardBody, CardHeader } from "@nextui-org/card";
import axios from "axios";
import { toast } from "react-toastify";

function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
Expand All @@ -35,16 +35,16 @@ const kindColorMap: Record<string, ChipProps["color"]> = {
};

const columns = [
{name: "NAME", uid: "name", sortable: true},
{name: "KIND", uid: "kind"},
{name: "SUBJECTS", uid: "subjects"},
{name: "ROLE REF", uid: "role_ref"},
{name: "DETAILS", uid: "details"},
{ name: "NAME", uid: "name", sortable: true },
{ name: "KIND", uid: "kind" },
{ name: "SUBJECTS", uid: "subjects" },
{ name: "ROLE REF", uid: "role_ref" },
{ name: "DETAILS", uid: "details" },
];

const kindOptions = [
{name: "ClusterRoleBinding", uid: "ClusterRoleBinding"},
{name: "RoleBinding", uid: "RoleBinding"},
{ name: "ClusterRoleBinding", uid: "ClusterRoleBinding" },
{ name: "RoleBinding", uid: "RoleBinding" },
];

type Subject = {
Expand Down Expand Up @@ -90,6 +90,20 @@ export default function MainTable() {
.catch(error => console.error('Error fetching data:', error));
}, []);

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsModalOpen(false);
}
};

document.addEventListener("keydown", handleKeyDown);

return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);

const headerColumns = React.useMemo(() => {
if (visibleColumns === "all") return columns;

Expand Down Expand Up @@ -167,12 +181,12 @@ export default function MainTable() {
return (
<div className="relative flex justify-center items-center gap-2">
<Dropdown>
<DropdownTrigger>
<DropdownTrigger aria-label="More options">
<Button isIconOnly size="sm" variant="light">
<VerticalDotsIcon className="text-default-300"/>
<VerticalDotsIcon className="text-default-300" />
</Button>
</DropdownTrigger>
<DropdownMenu>
<DropdownMenu aria-label="Details options">
<DropdownItem onClick={
() => {
setModalData(data); // Set the binding data to the modalData state
Expand Down Expand Up @@ -214,10 +228,10 @@ export default function MainTable() {
}
}, []);

const onClear = React.useCallback(()=>{
const onClear = React.useCallback(() => {
setFilterValue("")
setPage(1)
},[])
}, [])

const topContent = React.useMemo(() => {
return (
Expand All @@ -234,14 +248,14 @@ export default function MainTable() {
/>
<div className="flex gap-3">
<Dropdown>
<DropdownTrigger className="hidden sm:flex">
<DropdownTrigger className="hidden sm:flex" aria-label="Filter by kind">
<Button endContent={<ChevronDownIcon className="text-small" />} variant="flat">
Kind
</Button>
</DropdownTrigger>
<DropdownMenu
disallowEmptySelection
aria-label="Table Columns"
aria-label="Kind filter options"
closeOnSelect={false}
selectedKeys={kindFilter}
selectionMode="multiple"
Expand All @@ -255,14 +269,14 @@ export default function MainTable() {
</DropdownMenu>
</Dropdown>
<Dropdown>
<DropdownTrigger className="hidden sm:flex">
<DropdownTrigger className="hidden sm:flex" aria-label="Select columns">
<Button endContent={<ChevronDownIcon className="text-small" />} variant="flat">
Columns
</Button>
</DropdownTrigger>
<DropdownMenu
disallowEmptySelection
aria-label="Table Columns"
aria-label="Column selection options"
closeOnSelect={false}
selectedKeys={visibleColumns}
selectionMode="multiple"
Expand Down Expand Up @@ -322,6 +336,18 @@ export default function MainTable() {
);
}, [page, pages, onPreviousPage, onNextPage]);

const copyToClipboard = async () => {
if (modalData && modalData.raw) {
try {
await navigator.clipboard.writeText(modalData.raw);
toast.success("Successfully copied to the clipboard!");
} catch (err) {
toast.error("Failed to copy to the clipboard!");
console.error("Failed to copy to the clipboard:", err)
}
}
};

return (
<>
<Card
Expand Down Expand Up @@ -369,7 +395,7 @@ export default function MainTable() {
</Table>
{/* Modal to display the data */}
<Modal
size="xl"
size="3xl"
radius="md"
shadow="lg"
motionProps={{
Expand All @@ -394,22 +420,25 @@ export default function MainTable() {
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
>
<ModalHeader>View Data</ModalHeader>
<ModalContent>
<ModalBody>
<Card className="p-2 m-3" isBlurred shadow="sm">
<CardBody>
<pre className="overflow-auto">
{modalData && stringify(modalData, null, 2)} {/*TODO: Display the raw data here*/}
</pre>
<Card className="p-2 m-3" isBlurred shadow="sm" style={{ maxHeight: '70vh', overflow: 'auto' }}>
<CardBody className="relative">
<div className="absolute top-0 right-0 mb-2">
<Button isIconOnly size="sm" variant="light" aria-label="Copy data" onClick={copyToClipboard}>
<CopyIcon className="text-default-300" />
</Button>
</div>
<pre style={{ whiteSpace: 'pre-wrap' }}>
{modalData && modalData.raw}
</pre>
</CardBody>
</Card>
</ModalBody>
</ModalContent>
</Modal>
</CardBody>
</Card>

</>
);
}
}
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^5.2.1",
"react-toastify": "^10.0.5",
"tailwind-variants": "^0.1.20",
"tailwindcss": "3.4.3",
"typescript": "5.0.4"
Expand Down

0 comments on commit 39840bb

Please sign in to comment.