feat: GroupSync extension UI (#91777)
* feat: supporting code for groupsync extension UI * Add result of running i18n extraction * Place the UI behind a feature toggle as well as the license feature * Also add access checks to route loading of groupsync route with feature toggle * Add access check on permissions to show External group sync in nav * fix: New version of multiOrgRoleOptions hook * Remove OSS route definition * Apply feedback on nav title
This commit is contained in:
@@ -154,6 +154,21 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
|
||||
})
|
||||
}
|
||||
|
||||
if s.license.FeatureEnabled("groupsync") &&
|
||||
s.features.IsEnabled(ctx, featuremgmt.FlagGroupAttributeSync) &&
|
||||
hasAccess(ac.EvalAny(
|
||||
ac.EvalPermission("groupsync.mappings:read"),
|
||||
ac.EvalPermission("groupsync.mappings:write"),
|
||||
)) {
|
||||
accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
|
||||
Text: "External group sync",
|
||||
Id: "groupsync",
|
||||
SubTitle: "Manage mappings of Identity Provider groups to Grafana Roles",
|
||||
Icon: "",
|
||||
Url: s.cfg.AppSubURL + "/admin/access/groupsync",
|
||||
})
|
||||
}
|
||||
|
||||
usersNode := &navtree.NavLink{
|
||||
Text: "Users and access",
|
||||
SubTitle: "Configure access for individual users, teams, and service accounts",
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { difference } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
import { useDeepCompareEffect } from 'react-use';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { Role, AccessControlAction } from 'app/types';
|
||||
|
||||
import { fetchRoleOptions } from './api';
|
||||
|
||||
type MultiOrgRoleOptions = Record<number, Role[]>;
|
||||
|
||||
export const useRoleOptions = (organizationId: number) => {
|
||||
const [orgId, setOrgId] = useState(organizationId);
|
||||
|
||||
@@ -18,3 +22,29 @@ export const useRoleOptions = (organizationId: number) => {
|
||||
|
||||
return [{ roleOptions: value }, setOrgId] as const;
|
||||
};
|
||||
|
||||
export const useMultiOrgRoleOptions = (orgIDs: number[]): MultiOrgRoleOptions => {
|
||||
const [orgRoleOptions, setOrgRoleOptions] = useState<MultiOrgRoleOptions>({});
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
if (!contextSrv.licensedAccessControlEnabled() || !contextSrv.hasPermission(AccessControlAction.ActionRolesList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentOrgIDs = Object.keys(orgRoleOptions).map((o) => (typeof o === 'number' ? o : parseInt(o, 10)));
|
||||
const newOrgIDs = difference(orgIDs, currentOrgIDs);
|
||||
|
||||
Promise.all(
|
||||
newOrgIDs.map((orgID) => {
|
||||
return fetchRoleOptions(orgID).then((roleOptions) => [orgID, roleOptions]);
|
||||
})
|
||||
).then((value) => {
|
||||
setOrgRoleOptions({
|
||||
...orgRoleOptions,
|
||||
...Object.fromEntries(value),
|
||||
});
|
||||
});
|
||||
}, [orgIDs]);
|
||||
|
||||
return orgRoleOptions;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
@@ -14,9 +14,20 @@ export interface Props {
|
||||
inputId?: string;
|
||||
autoFocus?: boolean;
|
||||
excludeOrgs?: UserOrg[];
|
||||
defaultOrganization?: Organization;
|
||||
}
|
||||
|
||||
export function OrgPicker({ onSelected, className, inputId, autoFocus, excludeOrgs }: Props) {
|
||||
function orgToSelectItem(org: Organization): OrgSelectItem {
|
||||
return {
|
||||
value: org,
|
||||
label: org.name,
|
||||
};
|
||||
}
|
||||
|
||||
export function OrgPicker({ onSelected, className, inputId, autoFocus, excludeOrgs, defaultOrganization }: Props) {
|
||||
const [selected, setSelected] = useState<OrgSelectItem | undefined>(
|
||||
defaultOrganization ? orgToSelectItem(defaultOrganization) : undefined
|
||||
);
|
||||
// For whatever reason the autoFocus prop doesn't seem to work
|
||||
// with AsyncSelect, hence this workaround. Maybe fixed in a later version?
|
||||
useEffect(() => {
|
||||
@@ -27,13 +38,12 @@ export function OrgPicker({ onSelected, className, inputId, autoFocus, excludeOr
|
||||
|
||||
const [orgOptionsState, getOrgOptions] = useAsyncFn(async () => {
|
||||
const orgs: Organization[] = await getBackendSrv().get('/api/orgs');
|
||||
const allOrgs = orgs.map((org) => ({ value: { id: org.id, name: org.name }, label: org.name }));
|
||||
const allOrgs = orgs.map(orgToSelectItem);
|
||||
if (excludeOrgs) {
|
||||
let idArray = excludeOrgs.map((anOrg) => anOrg.orgId);
|
||||
const filteredOrgs = allOrgs.filter((item) => {
|
||||
return !idArray.includes(item.value.id);
|
||||
return allOrgs.filter((item) => {
|
||||
return item.value !== undefined && !idArray.includes(item.value.id);
|
||||
});
|
||||
return filteredOrgs;
|
||||
} else {
|
||||
return allOrgs;
|
||||
}
|
||||
@@ -50,7 +60,11 @@ export function OrgPicker({ onSelected, className, inputId, autoFocus, excludeOr
|
||||
const input = rawInput.toLowerCase();
|
||||
return !!option.value?.name.toLowerCase().includes(input);
|
||||
}}
|
||||
onChange={onSelected}
|
||||
onChange={(item) => {
|
||||
onSelected(item);
|
||||
setSelected(item);
|
||||
}}
|
||||
value={selected}
|
||||
placeholder="Select organization"
|
||||
noOptionsMessage="No organizations found"
|
||||
/>
|
||||
|
||||
@@ -262,6 +262,8 @@ export function getNavSubTitle(navId: string | undefined) {
|
||||
return t('nav.api-keys.subtitle', 'Manage and create API keys that are used to interact with Grafana HTTP APIs');
|
||||
case 'serviceaccounts':
|
||||
return t('nav.service-accounts.subtitle', 'Use service accounts to run automated workloads in Grafana');
|
||||
case 'groupsync':
|
||||
return t('nav.groupsync.subtitle', 'Manage mappings of Identity Provider groups to Grafana Roles');
|
||||
case 'global-users':
|
||||
return t('nav.global-users.subtitle', 'Manage users in Grafana');
|
||||
case 'global-orgs':
|
||||
|
||||
@@ -137,6 +137,10 @@ export enum AccessControlAction {
|
||||
// Settings
|
||||
SettingsRead = 'settings:read',
|
||||
SettingsWrite = 'settings:write',
|
||||
|
||||
// GroupSync
|
||||
GroupSyncMappingsRead = 'groupsync.mappings:read',
|
||||
GroupSyncMappingsWrite = 'groupsync.mappings:write',
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
|
||||
@@ -1615,6 +1615,9 @@
|
||||
"grafana-quaderno": {
|
||||
"title": "Grafana Quaderno"
|
||||
},
|
||||
"groupsync": {
|
||||
"subtitle": "Manage mappings of Identity Provider groups to Grafana Roles"
|
||||
},
|
||||
"help": {
|
||||
"title": "Help"
|
||||
},
|
||||
|
||||
@@ -1615,6 +1615,9 @@
|
||||
"grafana-quaderno": {
|
||||
"title": "Ğřäƒäʼnä Qūäđęřʼnő"
|
||||
},
|
||||
"groupsync": {
|
||||
"subtitle": "Mäʼnäģę mäppįʼnģş őƒ Ĩđęʼnŧįŧy Přővįđęř ģřőūpş ŧő Ğřäƒäʼnä Ŗőľęş"
|
||||
},
|
||||
"help": {
|
||||
"title": "Ħęľp"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user