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)
-
-
+