From d53022cbfb7c947fbaa6b2fb559dd4145213a089 Mon Sep 17 00:00:00 2001 From: jusrhee Date: Thu, 14 Mar 2024 23:15:14 -0400 Subject: [PATCH] Fe perm (#4412) --- api/types/project.go | 3 + dashboard/src/assets/role.svg | 9 + dashboard/src/components/porter/Back.tsx | 39 +- .../src/components/porter/Expandable.tsx | 85 +++- dashboard/src/components/porter/Modal.tsx | 42 +- .../expanded-app/status/LogsModal.tsx | 68 +-- .../main/home/project-settings/InviteList.tsx | 306 +++++++++++-- .../home/project-settings/PermissionGroup.tsx | 55 +++ .../home/project-settings/ProjectSettings.tsx | 81 ++-- .../main/home/project-settings/RoleModal.tsx | 427 ++++++++++++++++++ dashboard/src/shared/types.tsx | 1 + internal/models/project.go | 9 + 12 files changed, 959 insertions(+), 166 deletions(-) create mode 100644 dashboard/src/assets/role.svg create mode 100644 dashboard/src/main/home/project-settings/PermissionGroup.tsx create mode 100644 dashboard/src/main/home/project-settings/RoleModal.tsx diff --git a/api/types/project.go b/api/types/project.go index e516d4ed7d..2b961bdf7d 100644 --- a/api/types/project.go +++ b/api/types/project.go @@ -24,6 +24,7 @@ type ProjectList struct { ValidateApplyV2 bool `json:"validate_apply_v2"` AdvancedInfraEnabled bool `json:"advanced_infra_enabled"` SandboxEnabled bool `json:"sandbox_enabled"` + AdvancedRbacEnabled bool `json:"advanced_rbac_enabled"` } // Project type for entries in api responses for everything other than `GET /projects` @@ -54,6 +55,7 @@ type Project struct { ManagedDeploymentTargetsEnabled bool `json:"managed_deployment_targets_enabled"` AdvancedInfraEnabled bool `json:"advanced_infra_enabled"` SandboxEnabled bool `json:"sandbox_enabled"` + AdvancedRbacEnabled bool `json:"advanced_rbac_enabled"` } // FeatureFlags is a struct that contains old feature flag representations @@ -74,6 +76,7 @@ type FeatureFlags struct { StacksEnabled string `json:"stacks_enabled,omitempty"` ValidateApplyV2 bool `json:"validate_apply_v2"` ManagedDeploymentTargetsEnabled bool `json:"managed_deployment_targets_enabled"` + AdvancedRbacEnabled bool `json:"advanced_rbac_enabled"` } // CreateProjectRequest is a struct that contains the information diff --git a/dashboard/src/assets/role.svg b/dashboard/src/assets/role.svg new file mode 100644 index 0000000000..ed76ad7c99 --- /dev/null +++ b/dashboard/src/assets/role.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dashboard/src/components/porter/Back.tsx b/dashboard/src/components/porter/Back.tsx index 195add88dc..ede1951fad 100644 --- a/dashboard/src/components/porter/Back.tsx +++ b/dashboard/src/components/porter/Back.tsx @@ -1,20 +1,17 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; +import { Link } from "react-router-dom"; import styled from "styled-components"; import leftArrow from "assets/left-arrow.svg"; -import Text from "./Text"; + import Container from "./Container"; -import { Link } from "react-router-dom"; type Props = { to?: string; onClick?: () => void; }; -const Back: React.FC = ({ - to, - onClick, -}) => { +const Back: React.FC = ({ to, onClick }) => { return ( {to ? ( @@ -57,17 +54,17 @@ const BackLink = styled(Link)` `; const StyledBack = styled.div` -color: #aaaabb88; -font-size: 13px; -margin-bottom: 15px; -display: flex; -margin-top: -10px; -z-index: 999; -padding: 5px; -padding-right: 7px; -border-radius: 5px; -cursor: pointer; -:hover { - background: #ffffff11; -} -`; \ No newline at end of file + color: #aaaabb88; + font-size: 13px; + margin-bottom: 15px; + display: flex; + margin-top: -10px; + z-index: 999; + padding: 5px; + padding-right: 7px; + border-radius: 5px; + cursor: pointer; + :hover { + background: #ffffff11; + } +`; diff --git a/dashboard/src/components/porter/Expandable.tsx b/dashboard/src/components/porter/Expandable.tsx index f1cd73ce59..fc92c713f7 100644 --- a/dashboard/src/components/porter/Expandable.tsx +++ b/dashboard/src/components/porter/Expandable.tsx @@ -6,6 +6,7 @@ type Props = { children: React.ReactNode; style?: React.CSSProperties; preExpanded?: boolean; + alt?: boolean; }; // TODO: support footer for consolidation w/ app services @@ -13,26 +14,42 @@ const Expandable: React.FC = ({ header, children, style, - preExpanded + preExpanded, + alt, }) => { const [isExpanded, setIsExpanded] = useState(preExpanded || false); + if (alt) { + return ( + + { + setIsExpanded(!isExpanded); + }} + > + arrow_drop_down + {header} + + + {children} + + + ); + } + return (
{ setIsExpanded(!isExpanded) }} + onClick={() => { + setIsExpanded(!isExpanded); + }} > - - arrow_drop_down - - - {header} - + arrow_drop_down + {header}
- - {children} - + {children}
); }; @@ -42,8 +59,8 @@ export default Expandable; const ExpandedContents = styled.div<{ isExpanded: boolean }>` transition: all 0.5s; overflow: hidden; - max-height: ${({ isExpanded }) => isExpanded ? "500px" : "0"}; - padding: ${({ isExpanded }) => isExpanded ? "20px" : "0"}; + max-height: ${({ isExpanded }) => (isExpanded ? "500px" : "0")}; + padding: ${({ isExpanded }) => (isExpanded ? "20px" : "0")}; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; background: ${(props) => props.theme.fg + "66"}; @@ -101,3 +118,45 @@ const Header = styled.div<{ isExpanded: boolean }>` const StyledExpandable = styled.div` transition: all 0.2s; `; + +const AltHeader = styled.div<{ isExpanded: boolean }>` + transition: all 0.2s; + display: flex; + align-items: center; + cursor: pointer; + color: #aaaabbaa; + position: relative; + :hover { + color: ${(props) => props.theme.text.primary}; + } + + .dropdown { + font-size: 20px; + cursor: pointer; + border-radius: 20px; + margin-left: -5px; + margin-right: 8px; + transform: ${({ isExpanded }) => !isExpanded && "rotate(-90deg)"}; + } + + animation: fadeIn 0.3s 0s; + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +`; + +const AltExpandedContents = styled.div<{ isExpanded: boolean }>` + transition: all 0.5s; + margin-left: 4px; + overflow: hidden; + max-height: ${({ isExpanded }) => (isExpanded ? "500px" : "0")}; + padding-top: 10px; + padding-left: ${({ isExpanded }) => (isExpanded ? "18px" : "0")}; + border-left: ${({ isExpanded }) => isExpanded && "1px solid #494b4f"}; + color: ${(props) => props.theme.text.primary}; +`; diff --git a/dashboard/src/components/porter/Modal.tsx b/dashboard/src/components/porter/Modal.tsx index 0acd110871..531c6c0369 100644 --- a/dashboard/src/components/porter/Modal.tsx +++ b/dashboard/src/components/porter/Modal.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import styled from "styled-components"; import { createPortal } from "react-dom"; +import styled from "styled-components"; type Props = { closeModal?: () => void; @@ -8,29 +8,23 @@ type Props = { width?: string; }; -const Modal: React.FC = ({ - closeModal, - children, - width, -}) => { +const Modal: React.FC = ({ closeModal, children, width }) => { return ( <> - { - createPortal( - - - - {closeModal && ( - - close - - )} - {children} - - , - document.body - ) - } + {createPortal( + + + + {closeModal && ( + + close + + )} + {children} + + , + document.body + )} ); }; @@ -103,7 +97,7 @@ const StyledModal = styled.div<{ border-radius: 10px; border: 1px solid #494b4f; font-size: 13px; - width: ${props => props.width || "600px"}; + width: ${(props) => props.width || "600px"}; background: #42444933; backdrop-filter: saturate(150%) blur(8px); @@ -118,4 +112,4 @@ const StyledModal = styled.div<{ transform: translateY(0px); } } -`; \ No newline at end of file +`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx index 3554b5efa5..2f5cb2a364 100644 --- a/dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx +++ b/dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx @@ -1,40 +1,50 @@ import React, { useEffect, useRef } from "react"; + import Modal from "components/porter/Modal"; -import TitleSection from "components/TitleSection"; import Text from "components/porter/Text"; +import TitleSection from "components/TitleSection"; + import danger from "assets/danger.svg"; +import { type PorterLog } from "../logs/types"; import ExpandedIncidentLogs from "./ExpandedIncidentLogs"; -import { PorterLog } from "../logs/types"; -interface LogsModalProps { - logs: PorterLog[]; - setModalVisible: (x: boolean) => void; - logsName: string; -} -const LogsModal: React.FC = ({ logs, logsName, setModalVisible }) => { - const scrollToBottomRef = useRef(null); - const scrollToBottom = () => { - if (scrollToBottomRef.current) { - scrollToBottomRef.current.scrollIntoView({ - behavior: "smooth", - block: "end", - }); - } +type LogsModalProps = { + logs: PorterLog[]; + setModalVisible: (x: boolean) => void; + logsName: string; +}; +const LogsModal: React.FC = ({ + logs, + logsName, + setModalVisible, +}) => { + const scrollToBottomRef = useRef(null); + const scrollToBottom = () => { + if (scrollToBottomRef.current) { + scrollToBottomRef.current.scrollIntoView({ + behavior: "smooth", + block: "end", + }); } - useEffect(() => { - scrollToBottom(); - }, [scrollToBottomRef]); - + }; + useEffect(() => { + scrollToBottom(); + }, [scrollToBottomRef]); - return ( - setModalVisible(false)} width={"800px"}> - - Logs for {logsName} - - - - ); + return ( + { + setModalVisible(false); + }} + width={"800px"} + > + + Logs for {logsName} + + + + ); }; -export default LogsModal; \ No newline at end of file +export default LogsModal; diff --git a/dashboard/src/main/home/project-settings/InviteList.tsx b/dashboard/src/main/home/project-settings/InviteList.tsx index fce3237584..4561c89746 100644 --- a/dashboard/src/main/home/project-settings/InviteList.tsx +++ b/dashboard/src/main/home/project-settings/InviteList.tsx @@ -1,19 +1,24 @@ import React, { useContext, useEffect, useMemo, useState } from "react"; +import { type Column } from "react-table"; import styled from "styled-components"; -import { type InviteType } from "shared/types"; -import api from "shared/api"; -import { Context } from "shared/Context"; - -import Loading from "components/Loading"; -import InputRow from "components/form-components/InputRow"; -import Helper from "components/form-components/Helper"; -import Heading from "components/form-components/Heading"; import CopyToClipboard from "components/CopyToClipboard"; -import { type Column } from "react-table"; +import Heading from "components/form-components/Heading"; +import Helper from "components/form-components/Helper"; +import InputRow from "components/form-components/InputRow"; +import Loading from "components/Loading"; import Table from "components/OldTable"; +import Button from "components/porter/Button"; +import Spacer from "components/porter/Spacer"; import RadioSelector from "components/RadioSelector"; +import api from "shared/api"; +import { Context } from "shared/Context"; +import { type InviteType } from "shared/types"; + +import PermissionGroup from "./PermissionGroup"; +import RoleModal from "./RoleModal"; + type Props = {}; export type Collaborator = { @@ -24,7 +29,7 @@ export type Collaborator = { kind: string; }; -const InvitePage: React.FunctionComponent = ({ }) => { +const InvitePage: React.FunctionComponent = ({}) => { const { currentProject, setCurrentModal, @@ -41,6 +46,7 @@ const InvitePage: React.FunctionComponent = ({ }) => { const [roleList, setRoleList] = useState([]); const [isInvalidEmail, setIsInvalidEmail] = useState(false); const [isHTTPS] = useState(() => window.location.protocol === "https:"); + const [showNewGroupModal, setShowNewGroupModal] = useState(false); useEffect(() => { api @@ -150,7 +156,9 @@ const InvitePage: React.FunctionComponent = ({ }) => { } ) .then(getData) - .catch((err) => { console.log(err); }); + .catch((err) => { + console.log(err); + }); }; const replaceInvite = ( @@ -164,15 +172,16 @@ const InvitePage: React.FunctionComponent = ({ }) => { { email: inviteEmail, kind }, { id: currentProject.id } ) - .then(async () => - await api.deleteInvite( - "", - {}, - { - id: currentProject.id, - invId: inviteId, - } - ) + .then( + async () => + await api.deleteInvite( + "", + {}, + { + id: currentProject.id, + invId: inviteId, + } + ) ) .then(getData) .catch((err) => { @@ -188,7 +197,8 @@ const InvitePage: React.FunctionComponent = ({ }) => { const trimmedEmail = email.trim(); setEmail(trimmedEmail); - const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + const regex = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; if (!regex.test(trimmedEmail.toLowerCase())) { setIsInvalidEmail(true); return; @@ -222,13 +232,15 @@ const InvitePage: React.FunctionComponent = ({ }) => { }; const columns = useMemo< - Array> + Array< + Column<{ + email: string; + id: number; + status: string; + invite_link: string; + kind: string; + }> + > >( () => [ { @@ -258,13 +270,13 @@ const InvitePage: React.FunctionComponent = ({ }) => { if (row.values.status === "expired") { return ( - { replaceInvite( + onClick={() => { + replaceInvite( row.values.email, row.original.id, row.values.kind - ); } - } + ); + }} > Generate a new link @@ -298,13 +310,17 @@ const InvitePage: React.FunctionComponent = ({ }) => { { openEditModal(row.original); }} + onClick={() => { + openEditModal(row.original); + }} > more_vert { removeCollaborator(row.original.id); }} + onClick={() => { + removeCollaborator(row.original.id); + }} > delete @@ -315,13 +331,17 @@ const InvitePage: React.FunctionComponent = ({ }) => { { openEditModal(row.original); }} + onClick={() => { + openEditModal(row.original); + }} > more_vert { deleteInvite(row.original.id); }} + onClick={() => { + deleteInvite(row.original.id); + }} > delete @@ -338,7 +358,8 @@ const InvitePage: React.FunctionComponent = ({ }) => { inviteList.sort((a: any, b: any) => (a.email > b.email ? 1 : -1)); inviteList.sort((a: any, b: any) => (a.accepted > b.accepted ? 1 : -1)); const buildInviteLink = (token: string) => ` - ${isHTTPS ? "https://" : ""}${window.location.host}/api/projects/${currentProject.id + ${isHTTPS ? "https://" : ""}${window.location.host}/api/projects/${ + currentProject.id }/invites/${token} `; @@ -404,18 +425,212 @@ const InvitePage: React.FunctionComponent = ({ }) => { return ( <> <> + {currentProject?.advanced_rbac_enabled && ( + <> + Permission groups + Manage permission groups for your organization. + + + + + + + {showNewGroupModal && ( + { + setShowNewGroupModal(false); + }} + /> + )} + + )} Share project Generate a project invite for another user. { setEmail(newEmail); }} + setValue={(newEmail: string) => { + setEmail(newEmail); + }} width="100%" placeholder="ex: mrp@porter.run" /> - Specify a role for this user. + Specify a project role for this user. = ({ }) => { /> - { validateEmail(); }}> + { + validateEmail(); + }} + > Create invite {isInvalidEmail && ( @@ -456,12 +676,18 @@ const InvitePage: React.FunctionComponent = ({ }) => { ) )} + ); }; export default InvitePage; +const I = styled.i` + margin-right: 10px; + font-size: 18px; +`; + const Flex = styled.div` display: flex; align-items: center; diff --git a/dashboard/src/main/home/project-settings/PermissionGroup.tsx b/dashboard/src/main/home/project-settings/PermissionGroup.tsx new file mode 100644 index 0000000000..14da016597 --- /dev/null +++ b/dashboard/src/main/home/project-settings/PermissionGroup.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import styled from "styled-components"; + +import role from "assets/role.svg"; + +import RoleModal from "./RoleModal"; + +type PermissionGroupProps = { + name: string; + permissions?: any; +}; + +const PermissionGroup: React.FC = ({ + name, + permissions, +}) => { + const [showModal, setShowModal] = useState(false); + + return ( + <> + { + setShowModal(true); + }} + > + {name} + + {showModal && ( + { + setShowModal(false); + }} + /> + )} + + ); +}; + +export default PermissionGroup; + +const StyledPermissionGroup = styled.div` + display: inline-block; + border-radius: 5px; + margin-right: 10px; + cursor: pointer; + font-size: 13px; + padding: 7px 10px; + background: ${({ theme }) => theme.clickable.bg}; + border: 1px solid ${({ theme }) => theme.border}; + width: fit-content; + margin-bottom: 15px; +`; diff --git a/dashboard/src/main/home/project-settings/ProjectSettings.tsx b/dashboard/src/main/home/project-settings/ProjectSettings.tsx index 487fc39c5e..7435d8459d 100644 --- a/dashboard/src/main/home/project-settings/ProjectSettings.tsx +++ b/dashboard/src/main/home/project-settings/ProjectSettings.tsx @@ -1,28 +1,33 @@ import React, { Component, useContext, useEffect, useState } from "react"; +import _ from "lodash"; +import { + withRouter, + WithRouterProps, + type RouteComponentProps, +} from "react-router"; import styled from "styled-components"; +import Heading from "components/form-components/Heading"; +import Helper from "components/form-components/Helper"; +import Button from "components/porter/Button"; +import Error from "components/porter/Error"; +import Input from "components/porter/Input"; +import Link from "components/porter/Link"; +import Spacer from "components/porter/Spacer"; +import TabRegion from "components/TabRegion"; + +import api from "shared/api"; +import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc"; +import { isAlphanumeric } from "shared/common"; import { Context } from "shared/Context"; +import { getQueryParam } from "shared/routing"; import settingsGrad from "assets/settings-grad.svg"; -import InvitePage from "./InviteList"; -import TabRegion from "components/TabRegion"; -import Heading from "components/form-components/Heading"; -import Helper from "components/form-components/Helper"; import DashboardHeader from "../cluster-dashboard/DashboardHeader"; -import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc"; -import { RouteComponentProps, withRouter, WithRouterProps } from "react-router"; -import { getQueryParam } from "shared/routing"; import APITokensSection from "./APITokensSection"; -import _ from "lodash"; -import Link from "components/porter/Link"; -import Spacer from "components/porter/Spacer"; -import ProjectDeleteConsent from "./ProjectDeleteConsent"; +import InvitePage from "./InviteList"; import Metadata from "./Metadata"; -import Button from "components/porter/Button"; -import Input from "components/porter/Input"; -import { isAlphanumeric } from "shared/common"; -import api from "shared/api"; -import Error from "components/porter/Error"; +import ProjectDeleteConsent from "./ProjectDeleteConsent"; type PropsType = RouteComponentProps & WithAuthProps & {}; type ValidationError = { @@ -32,7 +37,7 @@ type ValidationError = { type StateType = { projectName: string; currentTab: string; - tabOptions: { value: string; label: string }[]; + tabOptions: Array<{ value: string; label: string }>; showCostConfirmModal: boolean; }; @@ -48,8 +53,7 @@ function ProjectSettings(props: any) { const [buttonStatus, setButtonStatus] = useState(""); useEffect(() => { - const selectedTab = - getQueryParam(props, "selected_tab") || "manage-access"; + const selectedTab = getQueryParam(props, "selected_tab") || "manage-access"; if (currentTab !== selectedTab) { setCurrentTab(selectedTab); @@ -60,12 +64,10 @@ function ProjectSettings(props: any) { if (projectName !== currentProject.name) { setProjectName(currentProject.name); } - }, []); - useEffect(() => { - let { currentProject } = context; + const { currentProject } = context; if (projectName !== currentProject.name) { setProjectName(currentProject.name); } @@ -99,7 +101,6 @@ function ProjectSettings(props: any) { }); } - if (!_.isEqual(tabOpts, tabOptions)) { setTabOptions(tabOpts); } @@ -108,7 +109,6 @@ function ProjectSettings(props: any) { if (selectedTab && selectedTab !== currentTab) { setCurrentTab(selectedTab); } - }, [context, projectName, currentTab, props, tabOptions]); const validateProjectName = (): ValidationError => { @@ -145,19 +145,19 @@ function ProjectSettings(props: any) { await api.renameProject( "", { - name: name, + name, }, { project_id: context.currentProject.id, - }) + } + ); setButtonStatus("success"); window.location.reload(); - } catch (err) { - console.log(err) + console.log(err); setButtonStatus(); } - } + }; const renderTabContents = () => { if (!props.isAuthorized("settings", "", ["get", "delete"])) { @@ -166,9 +166,8 @@ function ProjectSettings(props: any) { if (currentTab === "manage-access") { return ; - } - else if (currentTab == "metadata") { - return + } else if (currentTab == "metadata") { + return ; } else if (currentTab === "api-tokens") { return ; } else if (currentTab === "billing") { @@ -188,18 +187,23 @@ function ProjectSettings(props: any) { } else { return ( <> - Rename Project - + (lowercase letters, numbers, and "-" only) - - + + + )} + + ); +}; + +export default RoleModal; + +const ScrollWrapper = styled.div` + overflow-y: auto; + max-height: calc(100vh - 360px); +`; diff --git a/dashboard/src/shared/types.tsx b/dashboard/src/shared/types.tsx index f483c614dd..5ec7953e18 100644 --- a/dashboard/src/shared/types.tsx +++ b/dashboard/src/shared/types.tsx @@ -334,6 +334,7 @@ export type ProjectType = { managed_deployment_targets_enabled: boolean; aws_ack_auth_enabled: boolean; sandbox_enabled: boolean; + advanced_rbac_enabled: boolean; roles: Array<{ id: number; kind: string; diff --git a/internal/models/project.go b/internal/models/project.go index 772ddf4c02..020eaf3265 100644 --- a/internal/models/project.go +++ b/internal/models/project.go @@ -81,6 +81,9 @@ const ( // AdvancedInfraEnabled controls whether a project can use advanced infrastructure settings AdvancedInfraEnabled FeatureFlagLabel = "advanced_infra_enabled" + + // AdvancedRbacEnabled controls whether a project can use advanced rbac settings + AdvancedRbacEnabled FeatureFlagLabel = "advanced_rbac_enabled" ) // ProjectFeatureFlags keeps track of all project-related feature flags @@ -107,6 +110,7 @@ var ProjectFeatureFlags = map[FeatureFlagLabel]bool{ ValidateApplyV2: true, ManagedDeploymentTargetsEnabled: false, AdvancedInfraEnabled: false, + AdvancedRbacEnabled: false, } type ProjectPlan string @@ -201,6 +205,7 @@ type Project struct { EnableSandbox bool `gorm:"default:false"` EnableReprovision bool `gorm:"default:false"` AdvancedInfraEnabled bool `gorm:"default:false"` + AdvancedRbacEnabled bool `gorm:"default:false"` } // GetFeatureFlag calls launchdarkly for the specified flag @@ -249,6 +254,8 @@ func (p *Project) GetFeatureFlag(flagName FeatureFlagLabel, launchDarklyClient * return false case "advanced_infra_enabled": return false + case "advanced_rbac_enabled": + return p.AdvancedRbacEnabled } } @@ -299,6 +306,7 @@ func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Proje ManagedDeploymentTargetsEnabled: p.GetFeatureFlag(ManagedDeploymentTargetsEnabled, launchDarklyClient), AdvancedInfraEnabled: p.GetFeatureFlag(AdvancedInfraEnabled, launchDarklyClient), SandboxEnabled: p.EnableSandbox, + AdvancedRbacEnabled: p.GetFeatureFlag(AdvancedRbacEnabled, launchDarklyClient), } } @@ -334,6 +342,7 @@ func (p *Project) ToProjectListType() *types.ProjectList { ValidateApplyV2: p.ValidateApplyV2, FullAddOns: p.FullAddOns, AdvancedInfraEnabled: p.AdvancedInfraEnabled, + AdvancedRbacEnabled: p.AdvancedRbacEnabled, } }