diff --git a/ee/rbac/assets/permissions.yaml b/ee/rbac/assets/permissions.yaml index 971a4119a..7f5c4e607 100644 --- a/ee/rbac/assets/permissions.yaml +++ b/ee/rbac/assets/permissions.yaml @@ -70,6 +70,10 @@ permissions: description: "View the existing dashboards within the organization." - name: "organization.dashboards.manage" description: "Create new dashboard views." + - name: "organization.service_accounts.view" + description: "View service accounts within the organization." + - name: "organization.service_accounts.manage" + description: "Manage service accounts within the organization." project: - name: "project.view" description: "Access the project. This permission is needed to see any page within the project." diff --git a/ee/rbac/assets/roles.yaml b/ee/rbac/assets/roles.yaml index a64e1ffcb..2a6c8c6ff 100644 --- a/ee/rbac/assets/roles.yaml +++ b/ee/rbac/assets/roles.yaml @@ -39,6 +39,8 @@ roles: - "organization.custom_roles.view" - "organization.dashboards.view" - "organization.dashboards.manage" + - "organization.service_accounts.view" + - "organization.service_accounts.manage" - name: "Admin" description: "Admins can modify settings within the organization or any of its projects. However, they do not have access to billing information, and they cannot change general organization details, such as the organization name and URL." maps_to: "Admin" @@ -77,6 +79,8 @@ roles: - "organization.dashboards.view" - "organization.dashboards.manage" - "project.delete" + - "organization.service_accounts.view" + - "organization.service_accounts.manage" - name: "Member" description: "Members can access the organization's homepage and the projects they are assigned to. However, they are not able to modify any settings." permissions: diff --git a/front/Dockerfile b/front/Dockerfile index 0929df2a4..7765b402d 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -45,7 +45,7 @@ RUN mix sentry_recompile && mix compile --warnings-as-errors # -- elixir stage # -- node stage -FROM node:16-alpine as node +FROM node:16-alpine AS node WORKDIR /assets COPY front/assets/package.json front/assets/package-lock.json ./ RUN npm set progress=false && npm install diff --git a/front/assets/css/app-semaphore.css b/front/assets/css/app-semaphore.css index 3b61f3a2f..49988f443 100644 --- a/front/assets/css/app-semaphore.css +++ b/front/assets/css/app-semaphore.css @@ -2795,6 +2795,8 @@ img { max-width: 100%; } .b--indigo { border-color: #1570ff; } .b--dark-indigo { border-color: #00359f; } .b--orange { border-color: #fd7e14; } +.b--yellow { border-color: #FBC335; } +.b--blue { border-color: #2196F3; } .b--purple { border-color: #8658d6; } .b--dark-purple { border-color: #5122a5; } .b--dark-brown { border-color: #974510; } @@ -4585,6 +4587,7 @@ code, .code, pre { .bg-washed-purple { background-color: #f3ecff; } /* Yellows */ .yellow { color: #FBC335; } +.gold { color: #FBC335; } .lightest-yellow { color: #fff3bf; } .washed-yellow { color: #fffae4; } .bg-yellow { background-color: #FBC335; } diff --git a/front/assets/js/app.js b/front/assets/js/app.js index 7d8762e69..d2fd32f45 100644 --- a/front/assets/js/app.js +++ b/front/assets/js/app.js @@ -66,6 +66,7 @@ import { default as Agents} from "./agents"; import { default as AddPeople } from "./people/add_people"; import { default as EditPerson } from "./people/edit_person"; import { default as SyncPeople } from "./people/sync_people"; +import { default as ServiceAccounts } from "./service_accounts"; import { default as Report } from "./report"; import { InitializingScreen } from "./project_onboarding/initializing"; @@ -294,12 +295,23 @@ export var App = { GroupManagement.init(); new Star(); - const addPeopleAppRoot = document.getElementById("add-people"); - if (addPeopleAppRoot) { - AddPeople({ - dom: addPeopleAppRoot, - config: addPeopleAppRoot.dataset, - }); + + // Initialize Preact apps + const serviceAccountsEl = document.getElementById("service-accounts"); + if (serviceAccountsEl) { + const config = JSON.parse(serviceAccountsEl.dataset.config); + ServiceAccounts({ dom: serviceAccountsEl, config }); + } + + const addPeopleEl = document.getElementById("add-people"); + if (addPeopleEl) { + AddPeople({ dom: addPeopleEl, config: addPeopleEl.dataset }); + } + + const syncPeopleEl = document.querySelector(".app-sync-people"); + if (syncPeopleEl) { + const config = JSON.parse(syncPeopleEl.dataset.config); + SyncPeople({ dom: syncPeopleEl, config }); } document.querySelectorAll(".app-edit-person").forEach((editPersonAppRoot) => { @@ -516,6 +528,7 @@ export var App = { window.Notice.init(); + $(document).on("click", ".x-select-on-click", function (event) { event.currentTarget.setSelectionRange(0, event.currentTarget.value.length); }); diff --git a/front/assets/js/people/add_people/index.tsx b/front/assets/js/people/add_people/index.tsx index 7b28c5a1c..4ce823de7 100644 --- a/front/assets/js/people/add_people/index.tsx +++ b/front/assets/js/people/add_people/index.tsx @@ -44,7 +44,7 @@ export const App = () => { person_add {`Add people`} - close(false)} title="Add people"> + close(false)} title="Add new people" width="w-70-m"> @@ -85,7 +85,7 @@ const AddNewUsers = (props: { close: (reload: boolean) => void, }) => { const Link = (props: { icon: VNode, title: string, }) => { return ( void, }) => { }; return ( -
-
-

Add new people

-
- +
{userProviders.length > 1 && ( -
+
{userProviders.map(userProviderBox)}
)} @@ -158,7 +151,7 @@ const AddNewUsers = (props: { close: (reload: boolean) => void, }) => { return ( {loading && ( -
+
)} @@ -280,7 +273,7 @@ const ProvideVia = (props: ProvideViaProps) => { return (
{message}
-
+
)} {!arePeopleInvited && ( -
+
` + ${result.subject_type === "service_account" + ? `
smart_toy
` + : result.has_avatar + ? `` + : `
` } ${escapeHtml(result.name)} ` @@ -180,9 +190,11 @@ export var AddToProject = { `
- ${user.has_avatar - ? `` - : `` + ${user.subject_type === "service_account" + ? `
smart_toy
` + : user.has_avatar + ? `` + : `` }
${escapeHtml(user.name)}
diff --git a/front/assets/js/people/edit_person/index.tsx b/front/assets/js/people/edit_person/index.tsx index 0768868a6..1266dc281 100644 --- a/front/assets/js/people/edit_person/index.tsx +++ b/front/assets/js/people/edit_person/index.tsx @@ -163,53 +163,48 @@ export const Button = () => { manage_accounts Edit - -
-
-
-

Edit user

-
-
- -
- -
- + +
+
+ +
+
+ +
-
- - -
+
+ + +
-
- - - -
-
- + - -
+
diff --git a/front/assets/js/service_accounts/components/CreateServiceAccount.tsx b/front/assets/js/service_accounts/components/CreateServiceAccount.tsx new file mode 100644 index 000000000..36cc2b073 --- /dev/null +++ b/front/assets/js/service_accounts/components/CreateServiceAccount.tsx @@ -0,0 +1,139 @@ +import { useState, useContext } from "preact/hooks"; +import { Modal } from "js/toolbox"; +import { ConfigContext } from "../config"; +import { ServiceAccountsAPI } from "../utils/api"; +import { TokenDisplay } from "./TokenDisplay"; +import * as toolbox from "js/toolbox"; + +interface CreateServiceAccountProps { + isOpen: boolean; + onClose: () => void; + onCreated: () => void; +} + +export const CreateServiceAccount = ({ isOpen, onClose, onCreated }: CreateServiceAccountProps) => { + const config = useContext(ConfigContext); + const api = new ServiceAccountsAPI(config); + + const [name, setName] = useState(``); + const [description, setDescription] = useState(``); + const [selectedRoleId, setSelectedRoleId] = useState(``); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(``); + const [token, setToken] = useState(``); + + const handleSubmit = async (e: Event) => { + e.preventDefault(); + setError(null); + setLoading(true); + + const response = await api.create(name, description, selectedRoleId); + + if (response.error) { + setError(response.error); + setLoading(false); + } else if (response.data) { + setToken(response.data.api_token); + setLoading(false); + } + }; + + const handleClose = () => { + if (token) { + onCreated(); + } + setName(``); + setDescription(``); + setSelectedRoleId(``); + setError(``); + setToken(``); + onClose(); + }; + + const canSubmit = name.trim().length > 0 && selectedRoleId.length > 0 && !loading; + + return ( + + {!token ? ( +
void handleSubmit(e)}> +
+
+ + setName(e.currentTarget.value)} + placeholder="e.g., CI/CD Pipeline" + disabled={loading} + autoFocus + /> +
+ +
+ +