Skip to content

Commit

Permalink
Merge pull request #57 from bcgov/dev
Browse files Browse the repository at this point in the history
Release to Prod
  • Loading branch information
brysonjbest authored Dec 20, 2024
2 parents 3a9ff6d + 34bd454 commit 6d68370
Show file tree
Hide file tree
Showing 23 changed files with 2,654 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
.container {
position: relative;
width: 100%;
height: 100vh;
border: 1px solid #ccc;
border-radius: 6px;
min-height: 400px;
max-height: 80vh;
}

.controls {
position: absolute;
top: 20px;
left: 20px;
z-index: 1000;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: max-height 0.3s ease-in-out;
overflow: hidden;
margin: 5px;
border: 1px solid #ccc;
background-color: rgba(255, 255, 255, 0.9);
width: min-content;
}

.input {
width: min-content !important;
}

.select {
min-width: 100px;
width: 200px !important;
flex-shrink: 0;
}

.collapsible {
transition: opacity 0.2s ease;
}

.legend {
border-top: 1px solid #ccc;
padding-top: 8px;
font-size: 12px;
}

.legendTitle {
font-weight: bold;
margin: 0px 8px 0px 0px;
}

.legendItem {
display: flex;
align-items: center;
gap: 8px;
}

.legendLine {
width: 20px;
height: 2px;
}

.parentLine {
composes: legendLine;
background-color: #4169e1;
}

.childLine {
composes: legendLine;
background-color: #32cd32;
}

.selectedDot {
width: 12px;
height: 12px;
background-color: #ff7f50;
border-radius: 50%;
}

.helpList {
margin: 0;
padding-left: 20px;
}

.instructions {
margin: 0;
composes: legend;
word-wrap: break-word;
}

.svg {
width: 100%;
height: 100%;
background: #fff;
}
132 changes: 132 additions & 0 deletions app/components/RuleRelationsDisplay/RuleRelationsDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useEffect, useRef, useState, RefObject, createContext } from "react";
import { CategoryObject } from "@/app/types/ruleInfo";
import { RuleMapRule, RuleNode } from "@/app/types/rulemap";
import styles from "./RuleRelationsDisplay.module.css";
import { RuleGraphControls } from "./subcomponents/RuleGraphControls";
import { DescriptionManager } from "./subcomponents/DescriptionManager";
import { RuleGraph } from "./subcomponents/RuleGraph";

interface RuleModalContextType {
selectedRule: RuleNode | null;
openModal: (rule: RuleNode) => void;
closeModal: () => void;
}

export const RuleModalContext = createContext<RuleModalContextType | null>(null);

export interface RuleGraphProps {
rules: RuleMapRule[];
categories: CategoryObject[];
searchTerm?: string;
setSearchTerm?: (value: string) => void;
embeddedCategory?: string;
width?: number;
height?: number;
filter?: string | string[];
location?: Location;
basicLegend?: boolean;
}

/**
* Manages the visualization of rule relationships in a graph format
* Includes search, category filtering and draft rules toggle
*/
export default function RuleRelationsGraph({
rules,
categories,
searchTerm = "",
setSearchTerm,
width = 1000,
height = 1000,
filter,
location,
basicLegend,
}: RuleGraphProps) {
const containerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width, height });
const [isLegendMinimized, setIsLegendMinimized] = useState(true);
const [categoryFilter, setCategoryFilter] = useState(filter && filter?.length > 0 ? filter : undefined);
const [showDraftRules, setShowDraftRules] = useState(true);
const [selectedRule, setSelectedRule] = useState<RuleNode | null>(null);

const modalContext: RuleModalContextType = {
selectedRule,
openModal: (rule: RuleNode) => setSelectedRule(rule),
closeModal: () => setSelectedRule(null),
};

/**
* Sets up a ResizeObserver to handle responsive sizing
* Updates dimensions when the container size changes
*/
useEffect(() => {
if (!containerRef.current) return;

const resizeObserver = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
setDimensions({ width, height });
});

resizeObserver.observe(containerRef.current);
return () => resizeObserver.disconnect();
}, []);

useEffect(() => {
if (filter && filter?.length > 0) {
setCategoryFilter(filter);
}
}, [filter]);

const handleSearchChange = (value: string) => {
setSearchTerm && setSearchTerm(value);
};

const handleCategoryChange = (value: string | string[]) => {
setCategoryFilter(value);
};

const handleShowDraftRulesChange = (value: boolean) => {
setShowDraftRules(value);
};

const handleLegendToggle = () => {
setIsLegendMinimized(!isLegendMinimized);
};

const handleClearFilters = () => {
setSearchTerm && setSearchTerm("");
setCategoryFilter("");
};

return (
<div ref={containerRef} className={styles.container}>
<RuleModalContext.Provider value={modalContext}>
<RuleGraphControls
searchTerm={searchTerm}
categoryFilter={categoryFilter}
showDraftRules={showDraftRules}
isLegendMinimized={isLegendMinimized}
categories={categories}
embeddedCategory={filter}
onSearchChange={handleSearchChange}
onCategoryChange={handleCategoryChange}
onShowDraftRulesChange={handleShowDraftRulesChange}
onLegendToggle={handleLegendToggle}
onClearFilters={handleClearFilters}
location={location}
basicLegend={basicLegend}
/>
<RuleGraph
rules={rules}
svgRef={svgRef}
dimensions={dimensions}
searchTerm={searchTerm}
categoryFilter={categoryFilter}
showDraftRules={showDraftRules}
/>
<DescriptionManager />
</RuleModalContext.Provider>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useContext } from "react";
import { RuleDescription } from "./RuleDescription";
import Modal from "antd/es/modal/Modal";
import { RuleModalContext } from "../RuleRelationsDisplay";

// Manages the display of the rule description modal
export function DescriptionManager() {
const context = useContext(RuleModalContext);
if (!context || !context.selectedRule) return null;

const { selectedRule, closeModal } = context;

return (
<Modal
open={true}
onCancel={closeModal}
footer={null}
destroyOnClose
keyboard={true}
maskClosable={true}
aria-modal="true"
>
<div role="document" tabIndex={0}>
<RuleDescription
data={{
label: selectedRule.label,
name: selectedRule.name,
filepath: selectedRule.filepath,
description: selectedRule.description || undefined,
url: selectedRule.url,
isPublished: selectedRule.isPublished,
}}
onClose={closeModal}
visible={true}
/>
</div>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as d3 from "d3";

// Zoom and Pan controls for graph navigation
export const GraphNavigation = (
svg: d3.Selection<SVGSVGElement, unknown, null, undefined>,
g: d3.Selection<SVGGElement, unknown, null, undefined>,
zoom: d3.ZoomBehavior<Element, unknown>
) => {
const controls = svg
.append("g")
.attr("transform", `translate(10, ${svg.node()?.getBoundingClientRect().height! - 60})`);

controls.append("rect").attr("width", 30).attr("height", 90).attr("fill", "white").attr("stroke", "#999");

controls
.append("text")
.attr("x", 15)
.attr("y", 20)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("cursor", "pointer")
.text("+")
.on("click", () => {
svg
.transition()
.duration(750)
.call(zoom.scaleBy as any, 1.3);
});

controls
.append("text")
.attr("x", 15)
.attr("y", 50)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("cursor", "pointer")
.text("⟲")
.on("click", () => {
svg
.transition()
.duration(750)
.call(zoom.transform as any, d3.zoomIdentity);
});

controls
.append("text")
.attr("x", 15)
.attr("y", 80)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("cursor", "pointer")
.text("−")
.on("click", () => {
svg
.transition()
.duration(750)
.call(zoom.scaleBy as any, 0.7);
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.title {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
}

.metaInfo {
font-family: monospace;
font-size: 11px;
color: #666;
margin-bottom: 10px;
}

.description {
font-family: sans-serif;
font-size: 12px;
margin: 20px 0;
}

.link {
color: #0066cc;
font-size: 12px;
text-decoration: underline;
cursor: pointer;
display: block;
margin: 10px 0;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s ease;
}

.link:hover,
.link:focus {
background-color: #f0f7ff;
outline: none;
text-decoration: none;
}

.link:focus {
box-shadow: 0 0 0 2px #0066cc40;
}

.link[disabled] {
opacity: 0.5;
cursor: not-allowed;
}

.link[disabled]:hover,
.link[disabled]:focus {
background-color: transparent;
box-shadow: none;
}
Loading

0 comments on commit 6d68370

Please sign in to comment.