Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assign Roles to Space #13

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
*/

import {
EuiBadge,
type EuiBasicTableColumn,
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
EuiLoadingSpinner,
Expand Down Expand Up @@ -278,12 +281,25 @@ export class SpacesGridPage extends Component<Props, State> {
}),
sortable: true,
render: (value: string, rowRecord) => (
<EuiLink
{...reactRouterNavigate(this.props.history, this.getViewSpacePath(rowRecord))}
data-test-subj={`${rowRecord.id}-hyperlink`}
>
{value}
</EuiLink>
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiLink
{...reactRouterNavigate(this.props.history, this.getViewSpacePath(rowRecord))}
data-test-subj={`${rowRecord.id}-hyperlink`}
>
{value}
</EuiLink>
</EuiFlexItem>
{this.state.activeSpace?.name === rowRecord.name && (
<EuiFlexItem grow={false}>
<EuiBadge color="primary">
{i18n.translate('xpack.spaces.management.spacesGridPage.currentSpaceMarkerText', {
defaultMessage: 'current',
})}
</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
),
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ interface CreateParams {

export const spacesManagementApp = Object.freeze({
id: 'spaces',
create({ getStartServices, spacesManager, config, solutionNavExperiment }: CreateParams) {
create({
getStartServices,
spacesManager,
config,
solutionNavExperiment,
getRolesAPIClient,
}: CreateParams) {
const title = i18n.translate('xpack.spaces.displayName', {
defaultMessage: 'Spaces',
});
Expand Down Expand Up @@ -160,6 +166,7 @@ export const spacesManagementApp = Object.freeze({
onLoadSpace={onLoadSpace}
spaceId={spaceId}
selectedTabId={selectedTabId}
getRolesAPIClient={getRolesAPIClient}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,30 @@
import { useMemo } from 'react';

import type { KibanaFeature } from '@kbn/features-plugin/public';
import type { Role } from '@kbn/security-plugin-types-common';

import type { Space } from '../../../../common';
import type { ViewSpaceTab } from '../view_space_tabs';
import { getTabs } from '../view_space_tabs';
import { getTabs, type GetTabsProps, type ViewSpaceTab } from '../view_space_tabs';

export const useTabs = (
space: Space | null,
features: KibanaFeature[] | null,
roles: Role[],
currentSelectedTabId: string
): [ViewSpaceTab[], JSX.Element | undefined] => {
type UseTabsProps = Omit<GetTabsProps, 'space' | 'features'> & {
space: Space | null;
features: KibanaFeature[] | null;
currentSelectedTabId: string;
};

export const useTabs = ({
space,
features,
currentSelectedTabId,
...getTabsArgs
}: UseTabsProps): [ViewSpaceTab[], JSX.Element | undefined] => {
const [tabs, selectedTabContent] = useMemo(() => {
if (space == null || features == null) {
if (space === null || features === null) {
return [[]];
}
const _tabs = space != null ? getTabs(space, features, roles) : [];

const _tabs = space != null ? getTabs({ space, features, ...getTabsArgs }) : [];
return [_tabs, _tabs.find((obj) => obj.id === currentSelectedTabId)?.content];
}, [space, currentSelectedTabId, features, roles]);
}, [space, features, getTabsArgs, currentSelectedTabId]);

return [tabs, selectedTabContent];
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import type { FC, PropsWithChildren } from 'react';
import React, { createContext, useContext } from 'react';

import type { ApplicationStart } from '@kbn/core-application-browser';
import type { RolesAPIClient } from '@kbn/security-plugin-types-public';

import type { SpacesManager } from '../../../spaces_manager';

interface ViewSpaceServices {
export interface ViewSpaceServices {
serverBasePath: string;
getUrlForApp: ApplicationStart['getUrlForApp'];
navigateToUrl: ApplicationStart['navigateToUrl'];
spacesManager: SpacesManager;
getRolesAPIClient: () => Promise<RolesAPIClient>;
}

const ViewSpaceContext = createContext<ViewSpaceServices | null>(null);
Expand Down
91 changes: 45 additions & 46 deletions x-pack/plugins/spaces/public/management/view_space/view_space.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,46 @@ import {
import React, { lazy, Suspense, useEffect, useState } from 'react';
import type { FC } from 'react';

import type { ApplicationStart, Capabilities, ScopedHistory } from '@kbn/core/public';
import type { Capabilities, ScopedHistory } from '@kbn/core/public';
import type { FeaturesPluginStart, KibanaFeature } from '@kbn/features-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import type { Role } from '@kbn/security-plugin-types-common';

import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants';
import { useTabs } from './hooks/use_tabs';
import { ViewSpaceContextProvider } from './hooks/view_space_context_provider';
import {
ViewSpaceContextProvider,
type ViewSpaceServices,
} from './hooks/view_space_context_provider';
import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common';
import { getSpaceAvatarComponent } from '../../space_avatar';
import { SpaceSolutionBadge } from '../../space_solution_badge';
import type { SpacesManager } from '../../spaces_manager';

// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);

const getSelectedTabId = (selectedTabId?: string) => {
const getSelectedTabId = (canUserViewRoles: boolean, selectedTabId?: string) => {
// Validation of the selectedTabId routing parameter, default to the Content tab
return selectedTabId && [TAB_ID_FEATURES, TAB_ID_ROLES].includes(selectedTabId)
return selectedTabId &&
[TAB_ID_FEATURES, canUserViewRoles ? TAB_ID_ROLES : null]
.filter(Boolean)
.includes(selectedTabId)
? selectedTabId
: TAB_ID_CONTENT;
};

interface PageProps {
interface PageProps extends ViewSpaceServices {
spaceId?: string;
history: ScopedHistory;
selectedTabId?: string;
capabilities: Capabilities;
allowFeatureVisibility: boolean; // FIXME: handle this
solutionNavExperiment?: Promise<boolean>;
getFeatures: FeaturesPluginStart['getFeatures'];
getUrlForApp: ApplicationStart['getUrlForApp'];
navigateToUrl: ApplicationStart['navigateToUrl'];
serverBasePath: string;
spacesManager: SpacesManager;
history: ScopedHistory;
onLoadSpace: (space: Space) => void;
spaceId?: string;
selectedTabId?: string;
}

const handleApiError = (error: Error) => {
Expand All @@ -80,18 +81,25 @@ export const ViewSpacePage: FC<PageProps> = (props) => {
capabilities,
getUrlForApp,
navigateToUrl,
getRolesAPIClient,
} = props;

const selectedTabId = getSelectedTabId(_selectedTabId);
const [space, setSpace] = useState<Space | null>(null);
const [userActiveSpace, setUserActiveSpace] = useState<Space | null>(null);
const [features, setFeatures] = useState<KibanaFeature[] | null>(null);
const [roles, setRoles] = useState<Role[]>([]);
const [isLoadingSpace, setIsLoadingSpace] = useState(true);
const [isLoadingFeatures, setIsLoadingFeatures] = useState(true);
const [isLoadingRoles, setIsLoadingRoles] = useState(true);
const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId);
const [isSolutionNavEnabled, setIsSolutionNavEnabled] = useState(false);
const selectedTabId = getSelectedTabId(Boolean(capabilities?.roles?.view), _selectedTabId);
const [tabs, selectedTabContent] = useTabs({
space,
features,
roles,
capabilities,
currentSelectedTabId: selectedTabId,
});

useEffect(() => {
if (!spaceId) {
Expand Down Expand Up @@ -123,6 +131,7 @@ export const ViewSpacePage: FC<PageProps> = (props) => {
setIsLoadingRoles(false);
};

// maybe we do not make this call if user can't view roles? 🤔
getRoles().catch(handleApiError);
}, [spaceId, spacesManager]);

Expand Down Expand Up @@ -192,41 +201,16 @@ export const ViewSpacePage: FC<PageProps> = (props) => {
) : null;
};

const SwitchButton = () => {
if (userActiveSpace?.id === space.id) {
return null;
}

const { serverBasePath } = props;

// use href to force full page reload (needed in order to change spaces)
return (
<EuiButton
iconType="merge"
href={addSpaceIdToPath(
serverBasePath,
space.id,
`${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/view/${space.id}`
)}
data-test-subj="spaceSwitcherButton"
>
<FormattedMessage
id="xpack.spaces.management.spaceDetails.space.switchToSpaceButton.label"
defaultMessage="Switch to this space"
/>
</EuiButton>
);
};

return (
<ViewSpaceContextProvider
getRolesAPIClient={getRolesAPIClient}
spacesManager={spacesManager}
serverBasePath={props.serverBasePath}
navigateToUrl={navigateToUrl}
getUrlForApp={getUrlForApp}
>
<EuiText>
<EuiFlexGroup data-test-subj="spaceDetailsHeader">
<EuiFlexGroup data-test-subj="spaceDetailsHeader" alignItems="flexStart">
<EuiFlexItem grow={false}>
<HeaderAvatar />
</EuiFlexItem>
Expand Down Expand Up @@ -270,13 +254,28 @@ export const ViewSpacePage: FC<PageProps> = (props) => {
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center">
<EuiFlexGroup justifyContent="flexEnd" responsive={false}>
<EuiFlexItem>
<SettingsButton />
</EuiFlexItem>
<EuiFlexItem>
<SwitchButton />
</EuiFlexItem>
{userActiveSpace?.id !== space.id ? (
<EuiFlexItem>
<EuiButton
iconType="merge"
href={addSpaceIdToPath(
props.serverBasePath,
space.id,
`${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/view/${space.id}`
)}
data-test-subj="spaceSwitcherButton"
>
<FormattedMessage
id="xpack.spaces.management.spaceDetails.space.switchToSpaceButton.label"
defaultMessage="Switch to this space"
/>
</EuiButton>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Loading
Loading