From ccaa4ee3b9748f6cdd5bef421d79fb5a8beabeaf Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 5 Apr 2025 20:22:15 -0700 Subject: [PATCH 1/7] fix: Init deny calendar ui v2 --- apps/webservice/package.json | 2 + .../_components/CreateDenyRule.css | 947 ++++++++++++++++++ .../_components/CreateDenyRule.tsx | 241 +++++ .../(app)/policies/deny-windows/page.tsx | 12 +- packages/api/package.json | 1 + packages/api/src/root.ts | 2 +- packages/api/src/router/policy/deny-window.ts | 258 +++++ .../router/{policy.ts => policy/router.ts} | 72 +- packages/db/src/schema/policy.ts | 2 +- .../src/rules/deployment-deny-rule.ts | 84 +- pnpm-lock.yaml | 198 +++- 11 files changed, 1727 insertions(+), 92 deletions(-) create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.css create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx create mode 100644 packages/api/src/router/policy/deny-window.ts rename packages/api/src/router/{policy.ts => policy/router.ts} (66%) diff --git a/apps/webservice/package.json b/apps/webservice/package.json index 769b6e5aa..92fc22f39 100644 --- a/apps/webservice/package.json +++ b/apps/webservice/package.json @@ -75,6 +75,7 @@ "pretty-ms": "^9.2.0", "randomcolor": "^0.6.2", "react": "19.0.0", + "react-big-calendar": "^1.18.0", "react-dom": "19.0.0", "react-grid-layout": "^1.5.1", "react-hook-form": "catalog:", @@ -103,6 +104,7 @@ "@types/node": "catalog:node22", "@types/randomcolor": "^0.5.9", "@types/react": "19.0.8", + "@types/react-big-calendar": "^1.16.1", "@types/react-dom": "19.0.3", "@types/react-grid-layout": "^1.3.5", "@types/swagger-ui-react": "^4.19.0", diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.css b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.css new file mode 100644 index 000000000..3cc697f51 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.css @@ -0,0 +1,947 @@ +@charset "UTF-8"; +.rbc-btn { + color: inherit; + font: inherit; + margin: 0; +} + +button.rbc-btn { + overflow: visible; + text-transform: none; + -webkit-appearance: button; + -moz-appearance: button; + appearance: button; + cursor: pointer; +} + +button[disabled].rbc-btn { + cursor: not-allowed; +} + +button.rbc-input::-moz-focus-inner { + border: 0; + padding: 0; +} + +.rbc-calendar { + -webkit-box-sizing: border-box; + box-sizing: border-box; + height: 100%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + + border-radius: 5px; +} + +.rbc-m-b-negative-3 { + margin-bottom: -3px; +} + +.rbc-h-full { + height: 100%; +} + +.rbc-calendar *, +.rbc-calendar *:before, +.rbc-calendar *:after { + -webkit-box-sizing: inherit; + box-sizing: inherit; +} + +.rbc-abs-full, .rbc-row-bg { + overflow: hidden; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.rbc-ellipsis, .rbc-show-more, .rbc-row-segment .rbc-event-content, .rbc-event-label { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rbc-rtl { + direction: rtl; +} + +.rbc-off-range { + color: hsl(var(--muted-foreground)); +} + +.rbc-off-range-bg { + background: hsl(var(--muted)); +} + +.rbc-header { + overflow: hidden; + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0 3px; + text-align: center; + vertical-align: middle; + font-weight: bold; + font-size: 90%; + min-height: 0; + border-bottom: 1px solid hsl(var(--border)); +} +.rbc-header + .rbc-header { + border-left: 1px solid hsl(var(--border)); +} +.rbc-rtl .rbc-header + .rbc-header { + border-left-width: 0; + border-right: 1px solid hsl(var(--border)); +} +.rbc-header > a, .rbc-header > a:active, .rbc-header > a:visited { + color: inherit; + text-decoration: none; +} + +.rbc-button-link { + color: inherit; + background: none; + margin: 0; + padding: 0; + border: none; + cursor: pointer; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.rbc-row-content { + position: relative; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; + z-index: 4; +} + +.rbc-row-content-scrollable { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + height: 100%; +} +.rbc-row-content-scrollable .rbc-row-content-scroll-container { + height: 100%; + overflow-y: scroll; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + /* Hide scrollbar for Chrome, Safari and Opera */ +} +.rbc-row-content-scrollable .rbc-row-content-scroll-container::-webkit-scrollbar { + display: none; +} + +.rbc-today { + background-color: hsl(var(--primary) / 0.1); +} + +.rbc-toolbar { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin-bottom: 10px; + font-size: 16px; +} +.rbc-toolbar .rbc-toolbar-label { + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + padding: 0 10px; + text-align: center; +} +.rbc-toolbar button { + color: hsl(var(--foreground)); + display: inline-block; + margin: 0; + text-align: center; + vertical-align: middle; + background: hsl(var(--background)); + background-image: none; + border: 1px solid hsl(var(--border)); + padding: 0.375rem 1rem; + border-radius: 4px; + line-height: normal; + white-space: nowrap; +} +.rbc-toolbar button:active, .rbc-toolbar button.rbc-active { + background-image: none; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + background-color: hsl(var(--accent)); + border-color: hsl(var(--border)); +} +.rbc-toolbar button:active:hover, .rbc-toolbar button:active:focus, .rbc-toolbar button.rbc-active:hover, .rbc-toolbar button.rbc-active:focus { + color: hsl(var(--foreground)); + background-color: hsl(var(--accent)); + border-color: hsl(var(--border)); +} +.rbc-toolbar button:focus { + color: hsl(var(--foreground)); + background-color: hsl(var(--accent)); + border-color: hsl(var(--border)); +} +.rbc-toolbar button:hover { + color: hsl(var(--foreground)); + cursor: pointer; + background-color: hsl(var(--accent)); + border-color: hsl(var(--border)); +} + +.rbc-btn-group { + display: inline-block; + white-space: nowrap; +} +.rbc-btn-group > button:first-child:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.rbc-btn-group > button:last-child:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.rbc-rtl .rbc-btn-group > button:first-child:not(:last-child) { + border-radius: 4px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.rbc-rtl .rbc-btn-group > button:last-child:not(:first-child) { + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.rbc-btn-group > button:not(:first-child):not(:last-child) { + border-radius: 0; +} +.rbc-btn-group button + button { + margin-left: -1px; +} +.rbc-rtl .rbc-btn-group button + button { + margin-left: 0; + margin-right: -1px; +} +.rbc-btn-group + .rbc-btn-group, .rbc-btn-group + button { + margin-left: 10px; +} + +@media (max-width: 767px) { + .rbc-toolbar { + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + } +} +.rbc-event, .rbc-day-slot .rbc-background-event { + border: none; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: none; + box-shadow: none; + margin: 0; + /* padding: 2px 5px; */ + background-color: hsl(var(--primary)); + border-radius: 5px; + color: hsl(var(--primary-foreground)); + cursor: pointer; + width: 100%; + text-align: left; +} +.rbc-slot-selecting .rbc-event, .rbc-slot-selecting .rbc-day-slot .rbc-background-event, .rbc-day-slot .rbc-slot-selecting .rbc-background-event { + cursor: inherit; + pointer-events: none; +} +.rbc-event.rbc-selected, .rbc-day-slot .rbc-selected.rbc-background-event { + background-color: hsl(var(--primary)); + opacity: 0.8; +} +.rbc-event:focus, .rbc-day-slot .rbc-background-event:focus { + outline: 5px auto hsl(var(--primary)); +} + +.rbc-event-label { + /* font-size: 80%; */ + display: none; +} + +.rbc-event-overlaps { + -webkit-box-shadow: -1px 1px 5px 0px rgba(51, 51, 51, 0.5); + box-shadow: -1px 1px 5px 0px rgba(51, 51, 51, 0.5); +} + +.rbc-event-continues-prior { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.rbc-event-continues-after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.rbc-event-continues-earlier { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.rbc-event-continues-later { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.rbc-row { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; +} + +.rbc-row-segment { + padding: 0 1px 1px 1px; +} +.rbc-selected-cell { + background-color: rgba(0, 0, 0, 0.1); +} + +.rbc-show-more { + background-color: hsl(var(--background) / 0.3); + z-index: 4; + font-weight: bold; + font-size: 85%; + height: auto; + line-height: normal; + color: hsl(var(--primary)); +} +.rbc-show-more:hover, .rbc-show-more:focus { + color: hsl(var(--primary)); + opacity: 0.8; +} + +.rbc-month-view { + position: relative; + border: 1px solid hsl(var(--border)); + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex: 1 0 0px; + flex: 1 0 0; + width: 100%; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; + height: 100%; +} + +.rbc-month-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; +} + +.rbc-month-row { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + position: relative; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex: 1 0 0px; + flex: 1 0 0; + -ms-flex-preferred-size: 0px; + flex-basis: 0px; + overflow: hidden; + height: 100%; +} +.rbc-month-row + .rbc-month-row { + border-top: 1px solid hsl(var(--border)); +} + +.rbc-date-cell { + -webkit-box-flex: 1; + -ms-flex: 1 1 0px; + flex: 1 1 0; + min-width: 0; + padding-right: 5px; + text-align: right; +} +.rbc-date-cell.rbc-now { + font-weight: bold; +} +.rbc-date-cell > a, .rbc-date-cell > a:active, .rbc-date-cell > a:visited { + color: inherit; + text-decoration: none; +} + +.rbc-row-bg { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex: 1; + -ms-flex: 1 0 0px; + flex: 1 0 0; + overflow: hidden; + right: 1px; +} + +.rbc-day-bg { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; +} +.rbc-day-bg + .rbc-day-bg { + border-left: 1px solid hsl(var(--border)); +} +.rbc-rtl .rbc-day-bg + .rbc-day-bg { + border-left-width: 0; + border-right: 1px solid hsl(var(--border)); +} + +.rbc-overlay { + position: absolute; + z-index: 5; + border: 1px solid hsl(var(--border)); + background-color: hsl(var(--background)); + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.25); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.25); + padding: 10px; +} +.rbc-overlay > * + * { + margin-top: 1px; +} + +.rbc-overlay-header { + border-bottom: 1px solid hsl(var(--border)); + margin: -10px -10px 5px -10px; + padding: 2px 10px; +} + +.rbc-agenda-view { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex: 1 0 0px; + flex: 1 0 0; + overflow: auto; +} +.rbc-agenda-view table.rbc-agenda-table { + width: 100%; + border: 1px solid hsl(var(--border)); + border-spacing: 0; + border-collapse: collapse; +} +.rbc-agenda-view table.rbc-agenda-table tbody > tr > td { + padding: 5px 10px; + vertical-align: top; +} +.rbc-agenda-view table.rbc-agenda-table .rbc-agenda-time-cell { + padding-left: 15px; + padding-right: 15px; + text-transform: lowercase; +} +.rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td { + border-left: 1px solid hsl(var(--border)); +} +.rbc-rtl .rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td { + border-left-width: 0; + border-right: 1px solid hsl(var(--border)); +} +.rbc-agenda-view table.rbc-agenda-table tbody > tr + tr { + border-top: 1px solid hsl(var(--border)); +} +.rbc-agenda-view table.rbc-agenda-table thead > tr > th { + padding: 3px 5px; + text-align: left; + border-bottom: 1px solid hsl(var(--border)); +} +.rbc-rtl .rbc-agenda-view table.rbc-agenda-table thead > tr > th { + text-align: right; +} + +.rbc-agenda-time-cell { + text-transform: lowercase; +} +.rbc-agenda-time-cell .rbc-continues-after:after { + content: " »"; +} +.rbc-agenda-time-cell .rbc-continues-prior:before { + content: "« "; +} + +.rbc-agenda-date-cell, +.rbc-agenda-time-cell { + white-space: nowrap; +} + +.rbc-agenda-event-cell { + width: 100%; +} + +.rbc-time-column { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-height: 100%; +} +.rbc-time-column .rbc-timeslot-group { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.rbc-timeslot-group { + border-bottom: 1px solid hsl(var(--border)); + min-height: 40px; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; +} + +.rbc-time-gutter, +.rbc-header-gutter { + -webkit-box-flex: 0; + -ms-flex: none; + flex: none; + width: 75px; +} + +.rbc-label { + padding: 0 5px; +} + +.rbc-day-slot { + position: relative; +} +.rbc-day-slot .rbc-events-container { + bottom: 0; + left: 0; + position: absolute; + right: 0; + /* margin-right: 10px; */ + top: 0; +} +.rbc-day-slot .rbc-events-container.rbc-rtl { + left: 10px; + right: 0; +} +.rbc-day-slot .rbc-event, .rbc-day-slot .rbc-background-event { + border: 1px solid #265985; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + max-height: 100%; + min-height: 20px; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-flow: column wrap; + flex-flow: column wrap; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + overflow: hidden; + position: absolute; +} +.rbc-day-slot .rbc-background-event { + opacity: 0.75; +} +.rbc-day-slot .rbc-event-label { + -webkit-box-flex: 0; + -ms-flex: none; + flex: none; + padding-right: 5px; + width: auto; +} +.rbc-day-slot .rbc-event-content { + width: 100%; + -webkit-box-flex: 1; + -ms-flex: 1 1 0px; + flex: 1 1 0; + word-wrap: break-word; + line-height: 1; + height: 100%; + min-height: 1em; +} +.rbc-day-slot .rbc-time-slot { + /* border-top: 1px solid #404040; */ +} + +.rbc-time-view-resources .rbc-time-gutter, +.rbc-time-view-resources .rbc-time-header-gutter { + position: sticky; + left: 0; + background-color: hsl(var(--background)); + border-right: 1px solid hsl(var(--border)); + z-index: 10; + /* margin-right: -1px; */ + width: 75px; +} +.rbc-time-view-resources .rbc-time-header { + overflow: hidden; +} +.rbc-time-view-resources .rbc-time-header-content { + min-width: auto; + -webkit-box-flex: 1; + -ms-flex: 1 0 0px; + flex: 1 0 0; + -ms-flex-preferred-size: 0px; + flex-basis: 0px; +} +.rbc-time-view-resources .rbc-time-header-cell-single-day { + display: none; +} +.rbc-time-view-resources .rbc-day-slot { + min-width: 75px; +} +.rbc-time-view-resources .rbc-header, +.rbc-time-view-resources .rbc-day-bg { + width: 75px; + -webkit-box-flex: 1; + -ms-flex: 1 1 0px; + flex: 1 1 0; + -ms-flex-preferred-size: 0px; + flex-basis: 0px; +} + +.rbc-time-header-content + .rbc-time-header-content { + margin-left: -1px; +} + +.rbc-time-slot { + -webkit-box-flex: 1; + -ms-flex: 1 0 0px; + flex: 1 0 0; +} +.rbc-time-slot.rbc-now { + font-weight: bold; +} + +.rbc-day-header { + text-align: center; +} + +.rbc-slot-selection { + z-index: 10; + position: absolute; + background-color: hsl(var(--primary) / 0.3); + color: white; + font-size: 75%; + width: 100%; + padding: 3px; +} + +.rbc-slot-selecting { + cursor: move; +} + +.rbc-time-view { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + width: 100%; + border: 1px solid hsl(var(--border)); + min-height: 0; + overflow: hidden; +} +.rbc-time-view .rbc-time-gutter { + white-space: nowrap; + text-align: right; +} +.rbc-time-view .rbc-allday-cell { + -webkit-box-sizing: content-box; + box-sizing: content-box; + width: 100%; + height: 100%; + position: relative; +} +.rbc-time-view .rbc-allday-cell + .rbc-allday-cell { + border-left: 1px solid hsl(var(--border)); +} +.rbc-time-view .rbc-allday-events { + position: relative; + z-index: 4; +} +.rbc-time-view .rbc-row { + -webkit-box-sizing: border-box; + box-sizing: border-box; + min-height: 20px; +} + +.rbc-time-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + border-bottom: 1px solid hsl(var(--border)); +} +.rbc-time-header.rbc-overflowing { + border-right: 1px solid hsl(var(--border)); +} +.rbc-rtl .rbc-time-header.rbc-overflowing { + border-right-width: 0; + border-left: 1px solid hsl(var(--border)); +} +.rbc-time-header > .rbc-row:first-child { + border-bottom: 1px solid hsl(var(--border)); +} +.rbc-time-header > .rbc-row.rbc-row-resource { + border-bottom: 1px solid hsl(var(--border)); +} + +.rbc-time-header-cell-single-day { + display: none; +} + +.rbc-time-header-content { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + min-width: 0; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + border-left: 1px solid hsl(var(--border)); + overflow: hidden; +} +.rbc-rtl .rbc-time-header-content { + border-left-width: 0; + border-right: 1px solid hsl(var(--border)); +} +.rbc-time-header-content > .rbc-row.rbc-row-resource { + border-bottom: 1px solid hsl(var(--border)); + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.rbc-time-content { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + width: 100%; + border-top: 2px solid hsl(var(--border)); + overflow-y: auto; + position: relative; +} + +/* Custom scrollbar styles */ +.rbc-time-content::-webkit-scrollbar { + width: 8px; +} + +.rbc-time-content::-webkit-scrollbar-track { + background: hsl(var(--background)); +} + +.rbc-time-content::-webkit-scrollbar-thumb { + background: hsl(var(--border)); + border-radius: 4px; +} + +.rbc-time-content::-webkit-scrollbar-thumb:hover { + background: hsl(var(--border)); +} + +/* Firefox */ +.rbc-time-content { + scrollbar-width: thin; + scrollbar-color: hsl(var(--border)) hsl(var(--background)); +} + +.rbc-time-content > .rbc-time-gutter { + -webkit-box-flex: 0; + -ms-flex: none; + flex: none; +} +.rbc-time-content > * + * > * { + border-left: 1px solid hsl(var(--border)); +} +.rbc-rtl .rbc-time-content > * + * > * { + border-left-width: 0; + border-right: 1px solid hsl(var(--border)); +} +.rbc-time-content > .rbc-day-slot { + width: 100%; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; + min-width: 0; +} + +.rbc-current-time-indicator { + position: absolute; + z-index: 3; + left: 0; + right: 0; + height: 1px; + background-color: hsl(var(--primary)); + pointer-events: none; +} + +.rbc-resource-grouping.rbc-time-header-content { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; +} +.rbc-resource-grouping .rbc-row .rbc-header { + width: 141px; +} + +/*# sourceMappingURL=react-big-calendar.css.map */ + +.rbc-addons-dnd .rbc-addons-dnd-row-body { + position: relative; +} +.rbc-addons-dnd .rbc-addons-dnd-drag-row { + position: absolute; + top: 0; + left: 0; + right: 0; +} +.rbc-addons-dnd .rbc-addons-dnd-over { + background-color: rgba(0, 0, 0, 0.3); +} +.rbc-addons-dnd .rbc-event { + transition: opacity 150ms; +} +.rbc-addons-dnd .rbc-event:hover .rbc-addons-dnd-resize-ns-icon, .rbc-addons-dnd .rbc-event:hover .rbc-addons-dnd-resize-ew-icon { + display: block; +} +.rbc-addons-dnd .rbc-addons-dnd-dragged-event { + opacity: 0; +} +.rbc-addons-dnd.rbc-addons-dnd-is-dragging .rbc-event:not(.rbc-addons-dnd-dragged-event):not(.rbc-addons-dnd-drag-preview) { + opacity: 0.5; +} +.rbc-addons-dnd .rbc-addons-dnd-resizable { + position: relative; + width: 100%; + height: 100%; +} +.rbc-addons-dnd .rbc-addons-dnd-resize-ns-anchor { + width: 100%; + text-align: center; + position: absolute; +} +.rbc-addons-dnd .rbc-addons-dnd-resize-ns-anchor:first-child { + top: 0; +} +.rbc-addons-dnd .rbc-addons-dnd-resize-ns-anchor:last-child { + bottom: 0; +} +.rbc-addons-dnd .rbc-addons-dnd-resize-ns-anchor .rbc-addons-dnd-resize-ns-icon { + display: none; + border-top: 3px double; + margin: 0 auto; + width: 10px; + cursor: ns-resize; +} +.rbc-addons-dnd .rbc-addons-dnd-resize-ew-anchor { + position: absolute; + top: 4px; + bottom: 0; +} +.rbc-addons-dnd .rbc-addons-dnd-resize-ew-anchor:first-child { + left: 0; +} +.rbc-addons-dnd .rbc-addons-dnd-resize-ew-anchor:last-child { + right: 0; +} +.rbc-addons-dnd .rbc-addons-dnd-resize-ew-anchor .rbc-addons-dnd-resize-ew-icon { + display: none; + border-left: 3px double; + margin-top: auto; + margin-bottom: auto; + height: 10px; + cursor: ew-resize; +} \ No newline at end of file diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx new file mode 100644 index 000000000..2a804ce9a --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx @@ -0,0 +1,241 @@ +"use client"; + +import React, { useEffect, useMemo, useState } from "react"; +import { + differenceInMilliseconds, + endOfDay, + endOfWeek, + format, + getDay, + parse, + startOfDay, + startOfWeek, +} from "date-fns"; +import { enUS } from "date-fns/locale"; +import { Calendar, dateFnsLocalizer, Views } from "react-big-calendar"; +import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop"; + +import "./CreateDenyRule.css"; + +import { Popover, PopoverContent, PopoverTrigger } from "@ctrlplane/ui/popover"; + +import { api } from "~/trpc/react"; + +const locales = { "en-US": enUS }; + +const localizer = dateFnsLocalizer({ + format, + parse, + startOfWeek, + getDay, + locales, +}); + +const DnDCalendar = withDragAndDrop(Calendar); + +type Event = { + id: string; + title: string; + start: Date; + end: Date; +}; + +const EventComponent: React.FC<{ + event: any; + creatingDenyWindow: boolean; +}> = ({ event, creatingDenyWindow }) => { + const [open, setOpen] = useState(false); + const start = format(event.start, "h:mm a"); + const end = format(event.end, "h:mm a"); + return ( + + +
{ + e.stopPropagation(); + console.log("clicked!", event); + }} + > +
+ {" "} + {start} - {end} +
+
{event.title}
+
+
+ +
TEST TEXT
+
+
+ ); +}; + +type EventChange = { + event: object; + start: Date; + end: Date; +}; + +type EventCreate = { + start: Date; + end: Date; +}; + +export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ + workspaceId, +}) => { + const { timeZone } = Intl.DateTimeFormat().resolvedOptions(); + const now = useMemo(() => new Date(), []); + + const [creatingDenyWindow, setCreatingDenyWindow] = useState(false); + + const [currentRange, setCurrentRange] = useState<{ + start: Date; + end: Date; + }>({ start: startOfWeek(now), end: endOfWeek(now) }); + + const denyWindowsQ = api.policy.denyWindow.list.byWorkspaceId.useQuery({ + workspaceId, + start: currentRange.start, + end: currentRange.end, + timeZone, + }); + + const denyWindows = useMemo( + () => denyWindowsQ.data ?? [], + [denyWindowsQ.data], + ); + const [events, setEvents] = useState( + denyWindows.flatMap((denyWindow) => denyWindow.events), + ); + + useEffect( + () => setEvents(denyWindows.flatMap((denyWindow) => denyWindow.events)), + [denyWindows], + ); + + const resizeDenyWindow = api.policy.denyWindow.resize.useMutation(); + const dragDenyWindow = api.policy.denyWindow.drag.useMutation(); + const createDenyWindow = api.policy.denyWindow.createInCalendar.useMutation(); + + const handleEventResize = (event: EventChange) => { + const { start, end } = event; + const e = event.event as { + end: Date; + start: Date; + id: string; + }; + + const uuidRegex = + /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-\d+$/i; + const match = uuidRegex.exec(e.id); + const denyWindowId = match ? match[1] : null; + const denyWindow = denyWindows.find( + (denyWindow) => denyWindow.id === denyWindowId, + ); + const ev = denyWindow?.events.find((event) => event.id === e.id); + if (denyWindow == null || ev == null) return; + + const dtstartOffset = differenceInMilliseconds(start, ev.start); + const dtendOffset = differenceInMilliseconds(end, ev.end); + + const { id } = denyWindow; + resizeDenyWindow.mutate({ windowId: id, dtstartOffset, dtendOffset }); + + setEvents((prev) => { + const newEvents = prev.filter((event) => event.id !== e.id); + return [...newEvents, { ...ev, start: start, end: end }]; + }); + }; + + const handleEventDrag = (event: EventChange) => { + const { start, end } = event; + const e = event.event as { + end: Date; + start: Date; + id: string; + }; + + const uuidRegex = + /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-\d+$/i; + const match = uuidRegex.exec(e.id); + const denyWindowId = match ? match[1] : null; + const denyWindow = denyWindows.find( + (denyWindow) => denyWindow.id === denyWindowId, + ); + const ev = denyWindow?.events.find((event) => event.id === e.id); + if (denyWindow == null || ev == null) return; + + const offset = differenceInMilliseconds(start, ev.start); + const dayOfNewStart = getDay(start); + + const { id } = denyWindow; + dragDenyWindow.mutate({ windowId: id, offset, day: dayOfNewStart }); + + setEvents((prev) => { + const newEvents = prev.filter((event) => event.id !== e.id); + return [...newEvents, { ...ev, start: start, end: end }]; + }); + }; + + const handleEventCreate = (event: EventCreate) => { + console.log("creating deny window", event); + const { start, end } = event; + // console.log("creating deny window", start, end); + // createDenyWindow.mutate({ + // policyId: "123", + // start, + // end, + // timeZone, + // }); + // setEvents((prev) => [...prev, { id: "temp", start, end, title: "" }]); + }; + + return ( +
setCreatingDenyWindow(false)}> + { + if (Array.isArray(range)) { + const rangeStart = range.at(0); + const rangeEnd = range.at(-1); + + if (rangeStart && rangeEnd) + setCurrentRange({ + start: startOfDay(new Date(rangeStart)), + end: endOfDay(new Date(rangeEnd)), + }); + + return; + } + const { start, end } = range; + + setCurrentRange({ + start: new Date(start), + end: endOfDay(new Date(end)), + }); + }} + defaultView={Views.WEEK} + localizer={localizer} + events={events} + resizableAccessor={() => true} + draggableAccessor={() => true} + selectable={true} + onSelectSlot={(event) => handleEventCreate(event as EventCreate)} + onEventDrop={(event) => handleEventDrag(event as EventChange)} + onEventResize={(event) => handleEventResize(event as EventChange)} + style={{ height: 500 }} + step={30} + resizable={true} + components={{ + event: (props) => ( + + ), + }} + /> +
+ ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/page.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/page.tsx index 9c5693f31..146d0f0d0 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/page.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/page.tsx @@ -1,3 +1,4 @@ +import { notFound } from "next/navigation"; import { IconMenu2 } from "@tabler/icons-react"; import { @@ -20,14 +21,19 @@ import { SidebarTrigger } from "@ctrlplane/ui/sidebar"; import { Sidebars } from "~/app/[workspaceSlug]/sidebars"; import { urls } from "~/app/urls"; +import { api } from "~/trpc/server"; import { PageHeader } from "../../_components/PageHeader"; +import { CreateDenyRuleDialog } from "./_components/CreateDenyRule"; export default async function DenyWindowsPage({ params, }: { params: Promise<{ workspaceSlug: string }>; }) { - const workspaceSlug = (await params).workspaceSlug; + const { workspaceSlug } = await params; + const workspace = await api.workspace.bySlug(workspaceSlug); + if (workspace == null) notFound(); + return (
@@ -54,7 +60,7 @@ export default async function DenyWindowsPage({
- + Available Deny Windows @@ -75,6 +81,8 @@ export default async function DenyWindowsPage({ + +
); diff --git a/packages/api/package.json b/packages/api/package.json index 70539ef19..1ba8cef40 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -29,6 +29,7 @@ "@ctrlplane/events": "workspace:*", "@ctrlplane/job-dispatch": "workspace:*", "@ctrlplane/logger": "workspace:*", + "@ctrlplane/rule-engine": "workspace:*", "@ctrlplane/secrets": "workspace:*", "@ctrlplane/validators": "workspace:*", "@octokit/auth-app": "catalog:", diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index f82039270..a2bd2a413 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -3,7 +3,7 @@ import { deploymentRouter } from "./router/deployment"; import { environmentRouter } from "./router/environment"; import { githubRouter } from "./router/github"; import { jobRouter } from "./router/job"; -import { policyRouter } from "./router/policy"; +import { policyRouter } from "./router/policy/router"; import { resourceRouter } from "./router/resources"; import { runbookRouter } from "./router/runbook"; import { runtimeRouter } from "./router/runtime"; diff --git a/packages/api/src/router/policy/deny-window.ts b/packages/api/src/router/policy/deny-window.ts new file mode 100644 index 000000000..4b22ea2e6 --- /dev/null +++ b/packages/api/src/router/policy/deny-window.ts @@ -0,0 +1,258 @@ +import type * as rrule from "rrule"; +import { addMilliseconds } from "date-fns"; +import { z } from "zod"; + +import { eq, takeFirst } from "@ctrlplane/db"; +import { + createPolicyRuleDenyWindow, + policy, + policyRuleDenyWindow, + updatePolicyRuleDenyWindow, +} from "@ctrlplane/db/schema"; +import { DeploymentDenyRule } from "@ctrlplane/rule-engine"; +import { Permission } from "@ctrlplane/validators/auth"; + +import { createTRPCRouter, protectedProcedure } from "../../trpc"; + +type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6; +const weekdayMap: Record = { + 0: "SU", + 1: "MO", + 2: "TU", + 3: "WE", + 4: "TH", + 5: "FR", + 6: "SA", +}; + +export const denyWindowRouter = createTRPCRouter({ + list: createTRPCRouter({ + byWorkspaceId: protectedProcedure + .meta({ + authorizationCheck: ({ canUser, input }) => + canUser + .perform(Permission.PolicyGet) + .on({ type: "workspace", id: input.workspaceId }), + }) + .input( + z.object({ + workspaceId: z.string().uuid(), + start: z.date(), + end: z.date(), + timeZone: z.string(), + }), + ) + .query(async ({ ctx, input }) => { + const denyWindows = await ctx.db + .select() + .from(policyRuleDenyWindow) + .innerJoin(policy, eq(policyRuleDenyWindow.policyId, policy.id)) + .where(eq(policy.workspaceId, input.workspaceId)) + .then((rows) => rows.map((row) => row.policy_rule_deny_window)); + + return denyWindows.flatMap((denyWindow) => { + const rrule = { ...denyWindow.rrule, tzid: denyWindow.timeZone }; + const dtstart = + denyWindow.rrule.dtstart == null + ? null + : new Date(denyWindow.rrule.dtstart); + const rule = new DeploymentDenyRule({ + ...rrule, + dtend: denyWindow.dtend, + dtstart, + }); + const windows = rule.getWindowsInRange(input.start, input.end); + const events = windows.map((window, idx) => ({ + id: `${denyWindow.id}-${idx}`, + start: window.start, + end: window.end, + title: denyWindow.name === "" ? "Deny Window" : denyWindow.name, + })); + return { ...denyWindow, events }; + }); + }), + }), + + create: protectedProcedure + .meta({ + authorizationCheck: ({ canUser, input }) => + canUser + .perform(Permission.PolicyCreate) + .on({ type: "policy", id: input.policyId }), + }) + .input(createPolicyRuleDenyWindow) + .mutation(({ ctx, input }) => { + return ctx.db + .insert(policyRuleDenyWindow) + .values(input) + .returning() + .then(takeFirst); + }), + + createInCalendar: protectedProcedure + .meta({ + authorizationCheck: ({ canUser, input }) => + canUser + .perform(Permission.PolicyCreate) + .on({ type: "policy", id: input.policyId }), + }) + .input( + z.object({ + policyId: z.string().uuid(), + start: z.date(), + end: z.date(), + timeZone: z.string(), + }), + ) + .mutation(({ ctx, input }) => { + console.log(input); + + return; + }), + + resize: protectedProcedure + .meta({ + authorizationCheck: async ({ ctx, canUser, input }) => { + const denyWindow = await ctx.db + .select() + .from(policyRuleDenyWindow) + .where(eq(policyRuleDenyWindow.id, input.windowId)) + .then(takeFirst); + + return canUser + .perform(Permission.PolicyUpdate) + .on({ type: "policy", id: denyWindow.policyId }); + }, + }) + .input( + z.object({ + windowId: z.string().uuid(), + dtstartOffset: z.number(), + dtendOffset: z.number(), + }), + ) + .mutation(async ({ ctx, input }) => { + const denyWindow = await ctx.db + .select() + .from(policyRuleDenyWindow) + .where(eq(policyRuleDenyWindow.id, input.windowId)) + .then(takeFirst); + + const currStart = denyWindow.rrule.dtstart; + const currEnd = denyWindow.dtend; + + const newStart = + currStart != null + ? addMilliseconds(currStart, input.dtstartOffset) + : null; + + const newRrule = { ...denyWindow.rrule, dtstart: newStart }; + const newdtend = + currEnd != null ? addMilliseconds(currEnd, input.dtendOffset) : null; + + return ctx.db + .update(policyRuleDenyWindow) + .set({ rrule: newRrule, dtend: newdtend }) + .where(eq(policyRuleDenyWindow.id, input.windowId)) + .returning() + .then(takeFirst); + }), + + drag: protectedProcedure + .meta({ + authorizationCheck: async ({ ctx, canUser, input }) => { + const denyWindow = await ctx.db + .select() + .from(policyRuleDenyWindow) + .where(eq(policyRuleDenyWindow.id, input.windowId)) + .then(takeFirst); + + return canUser + .perform(Permission.PolicyUpdate) + .on({ type: "policy", id: denyWindow.policyId }); + }, + }) + .input( + z.object({ + windowId: z.string().uuid(), + offset: z.number(), + day: z.number().transform((val) => weekdayMap[val as Weekday]), + }), + ) + .mutation(async ({ ctx, input }) => { + const denyWindow = await ctx.db + .select() + .from(policyRuleDenyWindow) + .where(eq(policyRuleDenyWindow.id, input.windowId)) + .then(takeFirst); + + const currStart = denyWindow.rrule.dtstart; + const currEnd = denyWindow.dtend; + + const newStart = + currStart != null ? addMilliseconds(currStart, input.offset) : null; + + const newRrule = { + ...denyWindow.rrule, + dtstart: newStart, + byweekday: [input.day as rrule.ByWeekday], + }; + const newdtend = + currEnd != null ? addMilliseconds(currEnd, input.offset) : null; + + return ctx.db + .update(policyRuleDenyWindow) + .set({ rrule: newRrule, dtend: newdtend }) + .where(eq(policyRuleDenyWindow.id, input.windowId)) + .returning() + .then(takeFirst); + }), + update: protectedProcedure + .meta({ + authorizationCheck: async ({ canUser, input, ctx }) => { + const denyWindow = await ctx.db + .select() + .from(policyRuleDenyWindow) + .where(eq(policyRuleDenyWindow.id, input.id)) + .then(takeFirst); + + return canUser + .perform(Permission.PolicyUpdate) + .on({ type: "policy", id: denyWindow.policyId }); + }, + }) + .input( + z.object({ id: z.string().uuid(), data: updatePolicyRuleDenyWindow }), + ) + .mutation(({ ctx, input }) => { + return ctx.db + .update(policyRuleDenyWindow) + .set(input.data) + .where(eq(policyRuleDenyWindow.id, input.id)) + .returning() + .then(takeFirst); + }), + + delete: protectedProcedure + .meta({ + authorizationCheck: async ({ canUser, input, ctx }) => { + const denyWindow = await ctx.db + .select() + .from(policyRuleDenyWindow) + .where(eq(policyRuleDenyWindow.id, input)) + .then(takeFirst); + + return canUser + .perform(Permission.PolicyDelete) + .on({ type: "policy", id: denyWindow.policyId }); + }, + }) + .input(z.string().uuid()) + .mutation(({ ctx, input }) => + ctx.db + .delete(policyRuleDenyWindow) + .where(eq(policyRuleDenyWindow.id, input)) + .returning() + .then(takeFirst), + ), +}); diff --git a/packages/api/src/router/policy.ts b/packages/api/src/router/policy/router.ts similarity index 66% rename from packages/api/src/router/policy.ts rename to packages/api/src/router/policy/router.ts index c31b51aab..011a37d21 100644 --- a/packages/api/src/router/policy.ts +++ b/packages/api/src/router/policy/router.ts @@ -4,18 +4,16 @@ import { z } from "zod"; import { eq, takeFirst } from "@ctrlplane/db"; import { createPolicy, - createPolicyRuleDenyWindow, createPolicyTarget, policy, - policyRuleDenyWindow, policyTarget, updatePolicy, - updatePolicyRuleDenyWindow, updatePolicyTarget, } from "@ctrlplane/db/schema"; import { Permission } from "@ctrlplane/validators/auth"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { createTRPCRouter, protectedProcedure } from "../../trpc"; +import { denyWindowRouter } from "./deny-window"; export const policyRouter = createTRPCRouter({ list: protectedProcedure @@ -151,69 +149,5 @@ export const policyRouter = createTRPCRouter({ .then(takeFirst), ), - // Deny Window endpoints - createDenyWindow: protectedProcedure - .meta({ - authorizationCheck: ({ canUser, input }) => - canUser - .perform(Permission.PolicyCreate) - .on({ type: "policy", id: input.policyId }), - }) - .input(createPolicyRuleDenyWindow) - .mutation(({ ctx, input }) => { - return ctx.db - .insert(policyRuleDenyWindow) - .values(input) - .returning() - .then(takeFirst); - }), - - updateDenyWindow: protectedProcedure - .meta({ - authorizationCheck: async ({ canUser, input, ctx }) => { - const denyWindow = await ctx.db - .select() - .from(policyRuleDenyWindow) - .where(eq(policyRuleDenyWindow.id, input.id)) - .then(takeFirst); - - return canUser - .perform(Permission.PolicyUpdate) - .on({ type: "policy", id: denyWindow.policyId }); - }, - }) - .input( - z.object({ id: z.string().uuid(), data: updatePolicyRuleDenyWindow }), - ) - .mutation(({ ctx, input }) => { - return ctx.db - .update(policyRuleDenyWindow) - .set(input.data) - .where(eq(policyRuleDenyWindow.id, input.id)) - .returning() - .then(takeFirst); - }), - - deleteDenyWindow: protectedProcedure - .meta({ - authorizationCheck: async ({ canUser, input, ctx }) => { - const denyWindow = await ctx.db - .select() - .from(policyRuleDenyWindow) - .where(eq(policyRuleDenyWindow.id, input)) - .then(takeFirst); - - return canUser - .perform(Permission.PolicyDelete) - .on({ type: "policy", id: denyWindow.policyId }); - }, - }) - .input(z.string().uuid()) - .mutation(({ ctx, input }) => - ctx.db - .delete(policyRuleDenyWindow) - .where(eq(policyRuleDenyWindow.id, input)) - .returning() - .then(takeFirst), - ), + denyWindow: denyWindowRouter, }); diff --git a/packages/db/src/schema/policy.ts b/packages/db/src/schema/policy.ts index 546343a20..d6b1eadca 100644 --- a/packages/db/src/schema/policy.ts +++ b/packages/db/src/schema/policy.ts @@ -206,7 +206,7 @@ const rruleSchema = z // Using a separate variable to make TypeScript happy const typedRruleSchema = rruleSchema as unknown as z.ZodType; -const policyRuleDenyWindowInsertSchema = createInsertSchema( +export const policyRuleDenyWindowInsertSchema = createInsertSchema( policyRuleDenyWindow, { policyId: z.string().uuid(), diff --git a/packages/rule-engine/src/rules/deployment-deny-rule.ts b/packages/rule-engine/src/rules/deployment-deny-rule.ts index ce2713c01..7739cb8dd 100644 --- a/packages/rule-engine/src/rules/deployment-deny-rule.ts +++ b/packages/rule-engine/src/rules/deployment-deny-rule.ts @@ -4,8 +4,9 @@ import { differenceInMilliseconds, isSameDay, isWithinInterval, + subHours, } from "date-fns"; -import rrule from "rrule"; +import * as rrule from "rrule"; import type { DeploymentResourceContext, @@ -158,6 +159,87 @@ export class DeploymentDenyRule implements DeploymentResourceRule { return isSameDay(occurrence, now, { in: tz(this.timezone) }); } + /** + * Returns all denied windows within a given date range + * + * @param start - Start of the date range to check (inclusive) + * @param end - End of the date range to check (inclusive) + * @returns Array of denied windows, where each window has a start and end time + */ + getWindowsInRange(start: Date, end: Date): Array<{ start: Date; end: Date }> { + // since the rrule just treats its internal timezone as UTC, we need to convert + // the requested times to the rule's timezone + const startParts = getDatePartsInTimeZone(start, this.timezone); + const endParts = getDatePartsInTimeZone(end, this.timezone); + const startDt = datetime( + startParts.year, + startParts.month, + startParts.day, + startParts.hour, + startParts.minute, + startParts.second, + ); + const endDt = datetime( + endParts.year, + endParts.month, + endParts.day, + endParts.hour, + endParts.minute, + endParts.second, + ); + + const occurrences = this.rrule.between(startDt, endDt, true); + + if (occurrences.length === 0) return []; + + // Calculate duration if dtend is specified + const durationMs = + this.dtend != null && this.dtstart != null + ? differenceInMilliseconds(this.dtend, this.dtstart) + : 0; + + // Create windows for each occurrence + return occurrences.map((occurrence) => { + const windowStart = occurrence; + const windowEnd = + this.dtend != null + ? addMilliseconds(occurrence, durationMs) + : this.castTimezone( + new Date( + occurrence.getFullYear(), + occurrence.getMonth(), + occurrence.getDate(), + 23, + 59, + 59, + ), + this.timezone, + ); + + /** + * Window start and end are in the rrule's internal pretend UTC timezone + * Since we know the rule's timezone, we first figure out the offset of this timezone + * to the actual UTC time. Then we convert the window start and end to the actual UTC time + * by subtracting the offset. We end up not needing to know the requester's timezone at all + * because timezone in parts will + */ + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: this.timezone, + timeZoneName: "longOffset", + }); + + const offsetStr = formatter.format(windowStart).split("GMT")[1]; + const offsetHours = parseInt(offsetStr?.split(":")[0] ?? "0", 10); + + const realStartUTC = subHours(windowStart, offsetHours); + const realEndUTC = subHours(windowEnd, offsetHours); + + // Create UTC dates using Date.UTC to get the correct UTC time + + return { start: realStartUTC, end: realEndUTC }; + }); + } + /** * Converts a date to the specified timezone * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ffe68165..b7a0cf5d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -613,6 +613,9 @@ importers: react: specifier: 19.0.0 version: 19.0.0 + react-big-calendar: + specifier: ^1.18.0 + version: 1.18.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react-dom: specifier: 19.0.0 version: 19.0.0(react@19.0.0) @@ -692,6 +695,9 @@ importers: '@types/react': specifier: 19.0.8 version: 19.0.8 + '@types/react-big-calendar': + specifier: ^1.16.1 + version: 1.16.1 '@types/react-dom': specifier: 19.0.3 version: 19.0.3(@types/react@19.0.8) @@ -841,6 +847,9 @@ importers: '@ctrlplane/logger': specifier: workspace:* version: link:../logger + '@ctrlplane/rule-engine': + specifier: workspace:* + version: link:../rule-engine '@ctrlplane/secrets': specifier: workspace:* version: link:../secrets @@ -4151,6 +4160,9 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -5534,6 +5546,11 @@ packages: react: '>=17' react-dom: '>=17' + '@restart/hooks@0.4.16': + resolution: {integrity: sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==} + peerDependencies: + react: '>=16.8.0' + '@rollup/rollup-android-arm-eabi@4.24.0': resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} cpu: [arm] @@ -6125,6 +6142,9 @@ packages: '@types/dagre@0.7.52': resolution: {integrity: sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==} + '@types/date-arithmetic@4.1.4': + resolution: {integrity: sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==} + '@types/diff-match-patch@1.0.36': resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} @@ -6203,6 +6223,9 @@ packages: '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/prop-types@15.7.14': + resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + '@types/qs@6.9.16': resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} @@ -6215,6 +6238,9 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-big-calendar@1.16.1': + resolution: {integrity: sha512-pDHFcVWx+BvZbX6U39R4l8c9930vKnfx+09lf4W8r8HuxBDLzGk7Q63ncBmqqnQImEFNDKfwa6MDyu90cfzJ2A==} + '@types/react-dom@19.0.3': resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} peerDependencies: @@ -6277,6 +6303,9 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/warning@3.0.3': + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + '@types/ws@8.5.12': resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} @@ -7209,6 +7238,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + date-arithmetic@4.1.0: + resolution: {integrity: sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -7219,6 +7251,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -7319,6 +7354,10 @@ packages: deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -8109,6 +8148,9 @@ packages: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -8774,6 +8816,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -8923,6 +8968,9 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -9012,6 +9060,12 @@ packages: module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + monaco-editor@0.52.0: resolution: {integrity: sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==} @@ -9800,6 +9854,12 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-big-calendar@1.18.0: + resolution: {integrity: sha512-bGrCdyfnCGe2qnIdEoGkGgQdEFOiGO1Tq7RLkI1a2t8ZudyEAKekFtneO2/ltKQEQK6zH76YdJ7vR9UMyD+ULw==} + peerDependencies: + react: ^16.14.0 || ^17 || ^18 || ^19 + react-dom: ^16.14.0 || ^17 || ^18 || ^19 + react-copy-to-clipboard@5.1.0: resolution: {integrity: sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==} peerDependencies: @@ -9876,6 +9936,15 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-overlays@5.2.1: + resolution: {integrity: sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==} + peerDependencies: + react: '>=16.3.0' + react-dom: '>=16.3.0' + react-promise-suspense@0.3.4: resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} @@ -10945,6 +11014,11 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + uncontrollable@7.2.1: + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -11158,6 +11232,9 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -14247,6 +14324,8 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@popperjs/core@2.11.8': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -14274,7 +14353,7 @@ snapshots: '@radix-ui/primitive@1.0.1': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/primitive@1.1.0': {} @@ -14385,7 +14464,7 @@ snapshots: '@radix-ui/react-compose-refs@1.0.1(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 react: 19.0.0 optionalDependencies: '@types/react': 19.0.8 @@ -14398,7 +14477,7 @@ snapshots: '@radix-ui/react-context@1.0.1(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 react: 19.0.0 optionalDependencies: '@types/react': 19.0.8 @@ -14417,7 +14496,7 @@ snapshots: '@radix-ui/react-dialog@1.0.5(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.8)(react@19.0.0) '@radix-ui/react-context': 1.0.1(@types/react@19.0.8)(react@19.0.0) @@ -14468,7 +14547,7 @@ snapshots: '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.8)(react@19.0.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -14510,7 +14589,7 @@ snapshots: '@radix-ui/react-focus-guards@1.0.1(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 react: 19.0.0 optionalDependencies: '@types/react': 19.0.8 @@ -14523,7 +14602,7 @@ snapshots: '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.8)(react@19.0.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@19.0.8)(react@19.0.0) @@ -14567,7 +14646,7 @@ snapshots: '@radix-ui/react-id@1.0.1(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@19.0.8)(react@19.0.0) react: 19.0.0 optionalDependencies: @@ -14698,7 +14777,7 @@ snapshots: '@radix-ui/react-portal@1.0.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -14718,7 +14797,7 @@ snapshots: '@radix-ui/react-presence@1.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.8)(react@19.0.0) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@19.0.8)(react@19.0.0) react: 19.0.0 @@ -14739,7 +14818,7 @@ snapshots: '@radix-ui/react-primitive@1.0.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/react-slot': 1.0.2(@types/react@19.0.8)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -14848,7 +14927,7 @@ snapshots: '@radix-ui/react-slot@1.0.2(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/react-compose-refs': 1.0.1(@types/react@19.0.8)(react@19.0.0) react: 19.0.0 optionalDependencies: @@ -14914,7 +14993,7 @@ snapshots: '@radix-ui/react-use-callback-ref@1.0.1(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 react: 19.0.0 optionalDependencies: '@types/react': 19.0.8 @@ -14927,7 +15006,7 @@ snapshots: '@radix-ui/react-use-controllable-state@1.0.1(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@19.0.8)(react@19.0.0) react: 19.0.0 optionalDependencies: @@ -14942,7 +15021,7 @@ snapshots: '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@19.0.8)(react@19.0.0) react: 19.0.0 optionalDependencies: @@ -14957,7 +15036,7 @@ snapshots: '@radix-ui/react-use-layout-effect@1.0.1(@types/react@19.0.8)(react@19.0.0)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 react: 19.0.0 optionalDependencies: '@types/react': 19.0.8 @@ -16071,6 +16150,11 @@ snapshots: - '@types/react' - immer + '@restart/hooks@0.4.16(react@19.0.0)': + dependencies: + dequal: 2.0.3 + react: 19.0.0 + '@rollup/rollup-android-arm-eabi@4.24.0': optional: true @@ -17033,6 +17117,8 @@ snapshots: '@types/dagre@0.7.52': {} + '@types/date-arithmetic@4.1.4': {} + '@types/diff-match-patch@1.0.36': {} '@types/estree@1.0.6': {} @@ -17121,6 +17207,8 @@ snapshots: pg-protocol: 1.7.0 pg-types: 2.2.0 + '@types/prop-types@15.7.14': {} + '@types/qs@6.9.16': {} '@types/ramda@0.30.2': @@ -17131,6 +17219,12 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-big-calendar@1.16.1': + dependencies: + '@types/date-arithmetic': 4.1.4 + '@types/prop-types': 15.7.14 + '@types/react': 19.0.8 + '@types/react-dom@19.0.3(@types/react@19.0.8)': dependencies: '@types/react': 19.0.8 @@ -17196,6 +17290,8 @@ snapshots: '@types/uuid@9.0.8': {} + '@types/warning@3.0.3': {} + '@types/ws@8.5.12': dependencies: '@types/node': 22.13.10 @@ -18238,6 +18334,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + date-arithmetic@4.1.0: {} + date-fns@2.30.0: dependencies: '@babel/runtime': 7.26.10 @@ -18246,6 +18344,8 @@ snapshots: date-fns@4.1.0: {} + dayjs@1.11.13: {} + debounce@1.2.1: {} debounce@2.0.0: {} @@ -18322,6 +18422,8 @@ snapshots: deprecation@2.3.1: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-libc@2.0.3: {} @@ -18346,7 +18448,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 csstype: 3.1.3 dom-serializer@2.0.0: @@ -19405,6 +19507,8 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + globalize@0.1.1: {} + globals@11.12.0: {} globals@14.0.0: {} @@ -20159,6 +20263,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash.camelcase@4.3.0: {} lodash.castarray@4.4.0: {} @@ -20286,6 +20392,8 @@ snapshots: media-typer@0.3.0: {} + memoize-one@6.0.0: {} + merge-descriptors@1.0.3: {} merge-stream@2.0.0: {} @@ -20356,6 +20464,12 @@ snapshots: module-details-from-path@1.0.3: {} + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + monaco-editor@0.52.0: {} mrmime@2.0.1: {} @@ -21154,6 +21268,27 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + react-big-calendar@1.18.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.26.10 + clsx: 1.2.1 + date-arithmetic: 4.1.0 + dayjs: 1.11.13 + dom-helpers: 5.2.1 + globalize: 0.1.1 + invariant: 2.2.4 + lodash: 4.17.21 + lodash-es: 4.17.21 + luxon: 3.5.0 + memoize-one: 6.0.0 + moment: 2.30.1 + moment-timezone: 0.5.48 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-overlays: 5.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + uncontrollable: 7.2.1(react@19.0.0) + react-copy-to-clipboard@5.1.0(react@19.0.0): dependencies: copy-to-clipboard: 3.3.3 @@ -21252,6 +21387,21 @@ snapshots: react-is@18.3.1: {} + react-lifecycles-compat@3.0.4: {} + + react-overlays@5.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.26.10 + '@popperjs/core': 2.11.8 + '@restart/hooks': 0.4.16(react@19.0.0) + '@types/warning': 3.0.3 + dom-helpers: 5.2.1 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + uncontrollable: 7.2.1(react@19.0.0) + warning: 4.0.3 + react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 @@ -21365,7 +21515,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.10 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -22577,6 +22727,14 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 + uncontrollable@7.2.1(react@19.0.0): + dependencies: + '@babel/runtime': 7.26.10 + '@types/react': 19.0.8 + invariant: 2.2.4 + react: 19.0.0 + react-lifecycles-compat: 3.0.4 + undici-types@6.20.0: {} undici@5.28.4: @@ -22788,6 +22946,10 @@ snapshots: xml-name-validator: 5.0.0 optional: true + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + wcwidth@1.0.1: dependencies: defaults: 1.0.4 From 1d49e29db6055334bcac2b134393d2b514421fed Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 5 Apr 2025 20:46:30 -0700 Subject: [PATCH 2/7] cleanup --- .../_components/CreateDenyRule.tsx | 104 +++++++++--------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx index 2a804ce9a..fbdaf3f6c 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx @@ -43,12 +43,19 @@ type Event = { const EventComponent: React.FC<{ event: any; creatingDenyWindow: boolean; -}> = ({ event, creatingDenyWindow }) => { + onClose: () => void; +}> = ({ event, creatingDenyWindow, onClose }) => { const [open, setOpen] = useState(false); const start = format(event.start, "h:mm a"); const end = format(event.end, "h:mm a"); return ( - + { + if (!open) onClose(); + setOpen(open); + }} + >
= ({ const handleEventCreate = (event: EventCreate) => { console.log("creating deny window", event); const { start, end } = event; - // console.log("creating deny window", start, end); - // createDenyWindow.mutate({ - // policyId: "123", - // start, + // // console.log("creating deny window", start, end); + // // createDenyWindow.mutate({ + // // policyId: "123", + // // start, // end, // timeZone, // }); - // setEvents((prev) => [...prev, { id: "temp", start, end, title: "" }]); + setEvents((prev) => [...prev, { id: "new", start, end, title: "" }]); }; return ( -
setCreatingDenyWindow(false)}> - { - if (Array.isArray(range)) { - const rangeStart = range.at(0); - const rangeEnd = range.at(-1); - - if (rangeStart && rangeEnd) - setCurrentRange({ - start: startOfDay(new Date(rangeStart)), - end: endOfDay(new Date(rangeEnd)), - }); - - return; - } - const { start, end } = range; - - setCurrentRange({ - start: new Date(start), - end: endOfDay(new Date(end)), - }); - }} - defaultView={Views.WEEK} - localizer={localizer} - events={events} - resizableAccessor={() => true} - draggableAccessor={() => true} - selectable={true} - onSelectSlot={(event) => handleEventCreate(event as EventCreate)} - onEventDrop={(event) => handleEventDrag(event as EventChange)} - onEventResize={(event) => handleEventResize(event as EventChange)} - style={{ height: 500 }} - step={30} - resizable={true} - components={{ - event: (props) => ( - - ), - }} - /> -
+ { + if (Array.isArray(range)) { + const rangeStart = range.at(0); + const rangeEnd = range.at(-1); + + if (rangeStart && rangeEnd) + setCurrentRange({ + start: startOfDay(new Date(rangeStart)), + end: endOfDay(new Date(rangeEnd)), + }); + + return; + } + const { start, end } = range; + + setCurrentRange({ + start: new Date(start), + end: endOfDay(new Date(end)), + }); + }} + defaultView={Views.WEEK} + localizer={localizer} + events={events} + resizableAccessor={() => true} + draggableAccessor={() => true} + selectable={true} + onSelectSlot={(event) => handleEventCreate(event as EventCreate)} + onEventDrop={(event) => handleEventDrag(event as EventChange)} + onEventResize={(event) => handleEventResize(event as EventChange)} + style={{ height: 500 }} + step={30} + resizable={true} + components={{ + event: (props) => ( + + ), + }} + /> ); }; From 695f8bc38902a672acbe8510137026838e220640 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 5 Apr 2025 22:32:09 -0700 Subject: [PATCH 3/7] more --- .../_components/CreateDenyRule.tsx | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx index fbdaf3f6c..b4ad56c9c 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx @@ -43,19 +43,14 @@ type Event = { const EventComponent: React.FC<{ event: any; creatingDenyWindow: boolean; - onClose: () => void; -}> = ({ event, creatingDenyWindow, onClose }) => { - const [open, setOpen] = useState(false); +}> = ({ event, creatingDenyWindow }) => { + const [open, setOpen] = useState( + creatingDenyWindow && event.id === "new", + ); const start = format(event.start, "h:mm a"); const end = format(event.end, "h:mm a"); return ( - { - if (!open) onClose(); - setOpen(open); - }} - > +
{event.title}
- +
TEST TEXT
@@ -196,6 +191,7 @@ export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ // end, // timeZone, // }); + setCreatingDenyWindow(true); setEvents((prev) => [...prev, { id: "new", start, end, title: "" }]); }; @@ -227,7 +223,14 @@ export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ resizableAccessor={() => true} draggableAccessor={() => true} selectable={true} - onSelectSlot={(event) => handleEventCreate(event as EventCreate)} + onSelectSlot={(event) => { + if (!creatingDenyWindow) { + handleEventCreate(event as EventCreate); + return; + } + setCreatingDenyWindow(false); + setEvents((prev) => prev.filter((event) => event.id !== "new")); + }} onEventDrop={(event) => handleEventDrag(event as EventChange)} onEventResize={(event) => handleEventResize(event as EventChange)} style={{ height: 500 }} From b26d5e4717162da173f25368ba31448ffe897155 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sun, 6 Apr 2025 13:12:44 -0700 Subject: [PATCH 4/7] more change --- .../_components/CreateDenyRule.tsx | 160 +++++++++++++++--- .../_components/DenyWindowContext.tsx | 29 ++++ packages/api/src/router/policy/deny-window.ts | 33 +++- packages/db/src/schema/policy.ts | 5 +- 4 files changed, 190 insertions(+), 37 deletions(-) create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/DenyWindowContext.tsx diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx index b4ad56c9c..6b885a00c 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx @@ -15,11 +15,20 @@ import { enUS } from "date-fns/locale"; import { Calendar, dateFnsLocalizer, Views } from "react-big-calendar"; import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop"; +import * as SCHEMA from "@ctrlplane/db/schema"; + import "./CreateDenyRule.css"; +import { IconEdit } from "@tabler/icons-react"; +import { Form } from "react-hook-form"; + +import { Button } from "@ctrlplane/ui/button"; +import { FormField, useForm } from "@ctrlplane/ui/form"; +import { Input } from "@ctrlplane/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@ctrlplane/ui/popover"; import { api } from "~/trpc/react"; +import { DenyWindowProvider, useDenyWindow } from "./DenyWindowContext"; const locales = { "en-US": enUS }; @@ -40,23 +49,94 @@ type Event = { end: Date; }; +const EditDenyWindow: React.FC<{ + denyWindow: SCHEMA.PolicyRuleDenyWindow; + setEditing: () => void; +}> = ({ denyWindow, setEditing }) => { + const form = useForm({ + schema: SCHEMA.updatePolicyRuleDenyWindow, + defaultValues: denyWindow, + }); + + const updateDenyWindow = api.policy.denyWindow.update.useMutation(); + const onSubmit = form.handleSubmit((data) => { + console.log("data", data); + // updateDenyWindow.mutate({ + // id: denyWindow.id, + // data, + // }); + // setEditing(); + }); + + return ( +
+ + ( + + )} + /> + + + ); +}; + +const DenyWindowInfo: React.FC<{ + denyWindow: SCHEMA.PolicyRuleDenyWindow & { policy: SCHEMA.Policy }; + setEditing: () => void; + fullStartString: string; + endString: string; +}> = ({ denyWindow, setEditing, fullStartString, endString }) => ( +
+
+
+
{denyWindow.name}
+ +
+
+
+ {fullStartString} - {endString} +
+
+ {Number(denyWindow.rrule.freq) === 1 && "Monthly"} + {Number(denyWindow.rrule.freq) === 2 && "Weekly"} + {Number(denyWindow.rrule.freq) === 3 && "Daily"} +
+
+
+ {(denyWindow.description?.length ?? 0) > 0 && ( +
+ {denyWindow.description} +
+ )} +
+); + const EventComponent: React.FC<{ event: any; - creatingDenyWindow: boolean; -}> = ({ event, creatingDenyWindow }) => { - const [open, setOpen] = useState( - creatingDenyWindow && event.id === "new", - ); + denyWindow: SCHEMA.PolicyRuleDenyWindow & { policy: SCHEMA.Policy }; +}> = ({ event, denyWindow }) => { + const [editing, setEditing] = useState(false); + const { openEventId, setOpenEventId } = useDenyWindow(); const start = format(event.start, "h:mm a"); const end = format(event.end, "h:mm a"); + const fullStartString = format(event.start, "EEEE, MMMM d, h:mm aa"); return ( - +
{ - e.stopPropagation(); - console.log("clicked!", event); + onClick={() => { + setOpenEventId(event.id); }} >
@@ -66,8 +146,25 @@ const EventComponent: React.FC<{
{event.title}
- -
TEST TEXT
+ + {!editing && ( + setEditing(true)} + fullStartString={fullStartString} + endString={end} + /> + )} + {editing && ( + setEditing(false)} + /> + )} ); @@ -86,11 +183,21 @@ type EventCreate = { export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ workspaceId, +}) => { + return ( + + + + ); +}; + +const CreateDenyRuleDialogContent: React.FC<{ workspaceId: string }> = ({ + workspaceId, }) => { const { timeZone } = Intl.DateTimeFormat().resolvedOptions(); const now = useMemo(() => new Date(), []); - - const [creatingDenyWindow, setCreatingDenyWindow] = useState(false); + const { openEventId, setOpenEventId } = useDenyWindow(); + console.log("openEventId", openEventId); const [currentRange, setCurrentRange] = useState<{ start: Date; @@ -129,10 +236,7 @@ export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ id: string; }; - const uuidRegex = - /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-\d+$/i; - const match = uuidRegex.exec(e.id); - const denyWindowId = match ? match[1] : null; + const [denyWindowId] = e.id.split("|"); const denyWindow = denyWindows.find( (denyWindow) => denyWindow.id === denyWindowId, ); @@ -159,10 +263,7 @@ export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ id: string; }; - const uuidRegex = - /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-\d+$/i; - const match = uuidRegex.exec(e.id); - const denyWindowId = match ? match[1] : null; + const [denyWindowId] = e.id.split("|"); const denyWindow = denyWindows.find( (denyWindow) => denyWindow.id === denyWindowId, ); @@ -191,7 +292,6 @@ export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ // end, // timeZone, // }); - setCreatingDenyWindow(true); setEvents((prev) => [...prev, { id: "new", start, end, title: "" }]); }; @@ -224,11 +324,11 @@ export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ draggableAccessor={() => true} selectable={true} onSelectSlot={(event) => { - if (!creatingDenyWindow) { + if (!openEventId) { handleEventCreate(event as EventCreate); return; } - setCreatingDenyWindow(false); + setOpenEventId(null); setEvents((prev) => prev.filter((event) => event.id !== "new")); }} onEventDrop={(event) => handleEventDrag(event as EventChange)} @@ -237,9 +337,15 @@ export const CreateDenyRuleDialog: React.FC<{ workspaceId: string }> = ({ step={30} resizable={true} components={{ - event: (props) => ( - - ), + event: (props) => { + const event = props.event as Event; + const [denyWindowId] = event.id.split("|"); + const denyWindow = denyWindows.find( + (denyWindow) => denyWindow.id === denyWindowId, + ); + if (denyWindow == null) return null; + return ; + }, }} /> ); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/DenyWindowContext.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/DenyWindowContext.tsx new file mode 100644 index 000000000..a32a89853 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/DenyWindowContext.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useContext, useState } from "react"; + +type DenyWindowContextType = { + openEventId: string | null; + setOpenEventId: (id: string | null) => void; +}; + +const DenyWindowContext = createContext( + undefined, +); + +export const DenyWindowProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [openEventId, setOpenEventId] = useState(null); + + return ( + + {children} + + ); +}; + +export const useDenyWindow = () => { + const context = useContext(DenyWindowContext); + if (context === undefined) + throw new Error("useDenyWindow must be used within a DenyWindowProvider"); + return context; +}; diff --git a/packages/api/src/router/policy/deny-window.ts b/packages/api/src/router/policy/deny-window.ts index 4b22ea2e6..225b56a5b 100644 --- a/packages/api/src/router/policy/deny-window.ts +++ b/packages/api/src/router/policy/deny-window.ts @@ -47,10 +47,10 @@ export const denyWindowRouter = createTRPCRouter({ .select() .from(policyRuleDenyWindow) .innerJoin(policy, eq(policyRuleDenyWindow.policyId, policy.id)) - .where(eq(policy.workspaceId, input.workspaceId)) - .then((rows) => rows.map((row) => row.policy_rule_deny_window)); + .where(eq(policy.workspaceId, input.workspaceId)); - return denyWindows.flatMap((denyWindow) => { + return denyWindows.flatMap((dw) => { + const { policy_rule_deny_window: denyWindow, policy } = dw; const rrule = { ...denyWindow.rrule, tzid: denyWindow.timeZone }; const dtstart = denyWindow.rrule.dtstart == null @@ -63,12 +63,12 @@ export const denyWindowRouter = createTRPCRouter({ }); const windows = rule.getWindowsInRange(input.start, input.end); const events = windows.map((window, idx) => ({ - id: `${denyWindow.id}-${idx}`, + id: `${denyWindow.id}|${idx}`, start: window.start, end: window.end, title: denyWindow.name === "" ? "Deny Window" : denyWindow.name, })); - return { ...denyWindow, events }; + return { ...denyWindow, events, policy }; }); }), }), @@ -78,13 +78,28 @@ export const denyWindowRouter = createTRPCRouter({ authorizationCheck: ({ canUser, input }) => canUser .perform(Permission.PolicyCreate) - .on({ type: "policy", id: input.policyId }), + .on({ type: "workspace", id: input.worspaceId }), }) - .input(createPolicyRuleDenyWindow) - .mutation(({ ctx, input }) => { + .input( + z.object({ + workspaceId: z.string().uuid(), + data: createPolicyRuleDenyWindow, + }), + ) + .mutation(async ({ ctx, input }) => { + const { workspaceId, data } = input; + const policyId: string = + data.policyId ?? + (await ctx.db + .insert(policy) + .values({ workspaceId, name: data.name }) + .returning() + .then(takeFirst) + .then((policy) => policy.id)); + return ctx.db .insert(policyRuleDenyWindow) - .values(input) + .values({ ...data, policyId }) .returning() .then(takeFirst); }), diff --git a/packages/db/src/schema/policy.ts b/packages/db/src/schema/policy.ts index d6b1eadca..5e8a6e5d8 100644 --- a/packages/db/src/schema/policy.ts +++ b/packages/db/src/schema/policy.ts @@ -230,7 +230,10 @@ export type CreatePolicyTarget = z.infer; export const updatePolicyTarget = policyTargetInsertSchema.partial(); export type UpdatePolicyTarget = z.infer; -export const createPolicyRuleDenyWindow = policyRuleDenyWindowInsertSchema; +export const createPolicyRuleDenyWindow = + policyRuleDenyWindowInsertSchema.extend({ + policyId: z.string().uuid().optional(), + }); export type CreatePolicyRuleDenyWindow = z.infer< typeof createPolicyRuleDenyWindow >; From 6ee3aa201c6dd920766da54a7182481ce782f11a Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sun, 6 Apr 2025 21:49:55 -0700 Subject: [PATCH 5/7] updates --- .../deny-windows/_components/CreateDenyRule.tsx | 11 +++++------ packages/api/src/router/policy/deny-window.ts | 8 +++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx index 6b885a00c..8cba7511f 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx @@ -92,7 +92,11 @@ const DenyWindowInfo: React.FC<{
-
{denyWindow.name}
+
+ {denyWindow.policy.name === "" + ? "Deny Window" + : denyWindow.policy.name} +
- {(denyWindow.description?.length ?? 0) > 0 && ( -
- {denyWindow.description} -
- )}
); diff --git a/packages/api/src/router/policy/deny-window.ts b/packages/api/src/router/policy/deny-window.ts index 225b56a5b..b072bda6e 100644 --- a/packages/api/src/router/policy/deny-window.ts +++ b/packages/api/src/router/policy/deny-window.ts @@ -66,7 +66,7 @@ export const denyWindowRouter = createTRPCRouter({ id: `${denyWindow.id}|${idx}`, start: window.start, end: window.end, - title: denyWindow.name === "" ? "Deny Window" : denyWindow.name, + title: "Deny Window", })); return { ...denyWindow, events, policy }; }); @@ -83,7 +83,9 @@ export const denyWindowRouter = createTRPCRouter({ .input( z.object({ workspaceId: z.string().uuid(), - data: createPolicyRuleDenyWindow, + data: createPolicyRuleDenyWindow.extend({ + policyId: z.string().uuid().optional(), + }), }), ) .mutation(async ({ ctx, input }) => { @@ -92,7 +94,7 @@ export const denyWindowRouter = createTRPCRouter({ data.policyId ?? (await ctx.db .insert(policy) - .values({ workspaceId, name: data.name }) + .values({ workspaceId, name: "" }) .returning() .then(takeFirst) .then((policy) => policy.id)); From 2ad024f38bd05d633daad49f202a4444f7ac484d Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Tue, 8 Apr 2025 13:56:25 -0700 Subject: [PATCH 6/7] stlying --- .../_components/CreateDenyRule.tsx | 137 ++++++++++++++++-- packages/ui/src/datetime-picker.tsx | 53 ++++++- 2 files changed, 167 insertions(+), 23 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx index 8cba7511f..f2472df4b 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/deny-windows/_components/CreateDenyRule.tsx @@ -1,5 +1,6 @@ "use client"; +import type * as SCHEMA from "@ctrlplane/db/schema"; import React, { useEffect, useMemo, useState } from "react"; import { differenceInMilliseconds, @@ -15,17 +16,29 @@ import { enUS } from "date-fns/locale"; import { Calendar, dateFnsLocalizer, Views } from "react-big-calendar"; import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop"; -import * as SCHEMA from "@ctrlplane/db/schema"; - import "./CreateDenyRule.css"; -import { IconEdit } from "@tabler/icons-react"; -import { Form } from "react-hook-form"; +import { IconDeviceFloppy, IconEdit, IconX } from "@tabler/icons-react"; +import { z } from "zod"; import { Button } from "@ctrlplane/ui/button"; -import { FormField, useForm } from "@ctrlplane/ui/form"; -import { Input } from "@ctrlplane/ui/input"; +import { TimePicker } from "@ctrlplane/ui/datetime-picker"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + useForm, +} from "@ctrlplane/ui/form"; import { Popover, PopoverContent, PopoverTrigger } from "@ctrlplane/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ctrlplane/ui/select"; import { api } from "~/trpc/react"; import { DenyWindowProvider, useDenyWindow } from "./DenyWindowContext"; @@ -50,12 +63,26 @@ type Event = { }; const EditDenyWindow: React.FC<{ - denyWindow: SCHEMA.PolicyRuleDenyWindow; + denyWindow: SCHEMA.PolicyRuleDenyWindow & { policy: SCHEMA.Policy }; + event: Event; setEditing: () => void; -}> = ({ denyWindow, setEditing }) => { +}> = ({ denyWindow, event, setEditing }) => { const form = useForm({ - schema: SCHEMA.updatePolicyRuleDenyWindow, - defaultValues: denyWindow, + schema: z.object({ + start: z.date(), + end: z.date(), + recurrence: z.enum(["daily", "weekly", "monthly"]), + }), + defaultValues: { + start: event.start, + end: event.end, + recurrence: + Number(denyWindow.rrule.freq) === 1 + ? "monthly" + : Number(denyWindow.rrule.freq) === 2 + ? "weekly" + : "daily", + }, }); const updateDenyWindow = api.policy.denyWindow.update.useMutation(); @@ -70,12 +97,91 @@ const EditDenyWindow: React.FC<{ return (
- + +
+
+ {denyWindow.policy.name === "" + ? "Deny Window" + : denyWindow.policy.name} +
+
+ + +
+
+ ( + + Start + + + + + )} + /> + ( + + End + + + + + )} + /> ( - + name="recurrence" + render={({ field: { value, onChange } }) => ( + + Recurrence + + + + )} /> @@ -148,7 +254,7 @@ const EventComponent: React.FC<{ {!editing && ( setEditing(false)} /> )} diff --git a/packages/ui/src/datetime-picker.tsx b/packages/ui/src/datetime-picker.tsx index 8b1bf1642..2721b4fac 100644 --- a/packages/ui/src/datetime-picker.tsx +++ b/packages/ui/src/datetime-picker.tsx @@ -376,6 +376,7 @@ interface PeriodSelectorProps { onDateChange?: (date: Date | undefined) => void; onRightFocus?: () => void; onLeftFocus?: () => void; + className?: string; } const TimePeriodSelect = React.forwardRef< @@ -383,7 +384,15 @@ const TimePeriodSelect = React.forwardRef< PeriodSelectorProps >( ( - { period, setPeriod, date, onDateChange, onLeftFocus, onRightFocus }, + { + period, + setPeriod, + date, + onDateChange, + onLeftFocus, + onRightFocus, + className, + }, ref, ) => { const handleKeyDown = (e: React.KeyboardEvent) => { @@ -413,14 +422,17 @@ const TimePeriodSelect = React.forwardRef< }; return ( -
+