Compare commits
1 Commits
sriram/SQL
...
provisioni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a0ebf3e0 |
@@ -0,0 +1,45 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { t } from '@grafana/i18n';
|
||||||
|
import { EmptyState, FilterInput, Stack } from '@grafana/ui';
|
||||||
|
import { Connection } from 'app/api/clients/provisioning/v0alpha1';
|
||||||
|
|
||||||
|
import { ConnectionListItem } from './ConnectionListItem';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: Connection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectionList({ items }: Props) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const filteredItems = items.filter((item) => {
|
||||||
|
if (!query) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
const name = item.metadata?.name?.toLowerCase() ?? '';
|
||||||
|
const providerType = item.spec?.type?.toLowerCase() ?? '';
|
||||||
|
return name.includes(lowerQuery) || providerType.includes(lowerQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction={'column'} gap={3}>
|
||||||
|
<FilterInput
|
||||||
|
placeholder={t('provisioning.connections.search-placeholder', 'Search connections')}
|
||||||
|
value={query}
|
||||||
|
onChange={setQuery}
|
||||||
|
/>
|
||||||
|
<Stack direction={'column'} gap={2}>
|
||||||
|
{filteredItems.length ? (
|
||||||
|
filteredItems.map((item) => <ConnectionListItem key={item.metadata?.name} connection={item} />)
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
variant="not-found"
|
||||||
|
message={t('provisioning.connections.no-results', 'No results matching your query')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Trans } from '@grafana/i18n';
|
||||||
|
import { Card, LinkButton, Stack, Text, TextLink } from '@grafana/ui';
|
||||||
|
import { Connection } from 'app/api/clients/provisioning/v0alpha1';
|
||||||
|
|
||||||
|
import { RepoIcon } from '../Shared/RepoIcon';
|
||||||
|
import { CONNECTIONS_URL } from '../constants';
|
||||||
|
|
||||||
|
import { ConnectionStatusBadge } from './ConnectionStatusBadge';
|
||||||
|
import { DeleteConnectionButton } from './DeleteConnectionButton';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
connection: Connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectionListItem({ connection }: Props) {
|
||||||
|
const { metadata, spec, status } = connection;
|
||||||
|
const name = metadata?.name ?? '';
|
||||||
|
const providerType = spec?.type;
|
||||||
|
const url = spec?.url;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card noMargin key={name}>
|
||||||
|
<Card.Figure>
|
||||||
|
<RepoIcon type={providerType} />
|
||||||
|
</Card.Figure>
|
||||||
|
<Card.Heading>
|
||||||
|
<Stack gap={2} direction="row" alignItems="center">
|
||||||
|
<Text variant="h3">{name}</Text>
|
||||||
|
<ConnectionStatusBadge status={status} />
|
||||||
|
</Stack>
|
||||||
|
</Card.Heading>
|
||||||
|
|
||||||
|
{url && (
|
||||||
|
<Card.Meta>
|
||||||
|
<TextLink external href={url}>
|
||||||
|
{url}
|
||||||
|
</TextLink>
|
||||||
|
</Card.Meta>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card.Actions>
|
||||||
|
<Stack gap={1} direction="row">
|
||||||
|
<LinkButton icon="eye" href={`${CONNECTIONS_URL}/${name}`} variant="primary" size="md">
|
||||||
|
<Trans i18nKey="provisioning.connections.view">View</Trans>
|
||||||
|
</LinkButton>
|
||||||
|
<DeleteConnectionButton name={name} connection={connection} />
|
||||||
|
</Stack>
|
||||||
|
</Card.Actions>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { t } from '@grafana/i18n';
|
||||||
|
import { Badge, IconName } from '@grafana/ui';
|
||||||
|
import { ConnectionStatus } from 'app/api/clients/provisioning/v0alpha1';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
status?: ConnectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BadgeConfig {
|
||||||
|
color: 'green' | 'red' | 'darkgrey';
|
||||||
|
text: string;
|
||||||
|
icon: IconName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBadgeConfig(status?: ConnectionStatus): BadgeConfig {
|
||||||
|
if (!status) {
|
||||||
|
return {
|
||||||
|
color: 'darkgrey',
|
||||||
|
text: t('provisioning.connections.status-unknown', 'Unknown'),
|
||||||
|
icon: 'question-circle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status.state) {
|
||||||
|
case 'connected':
|
||||||
|
return {
|
||||||
|
color: 'green',
|
||||||
|
text: t('provisioning.connections.status-connected', 'Connected'),
|
||||||
|
icon: 'check',
|
||||||
|
};
|
||||||
|
case 'disconnected':
|
||||||
|
return {
|
||||||
|
color: 'red',
|
||||||
|
text: t('provisioning.connections.status-disconnected', 'Disconnected'),
|
||||||
|
icon: 'times-circle',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'darkgrey',
|
||||||
|
text: t('provisioning.connections.status-unknown', 'Unknown'),
|
||||||
|
icon: 'question-circle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectionStatusBadge({ status }: Props) {
|
||||||
|
const config = getBadgeConfig(status);
|
||||||
|
|
||||||
|
return <Badge color={config.color} text={config.text} icon={config.icon} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { t, Trans } from '@grafana/i18n';
|
||||||
|
import { Alert, Button, EmptyState, Stack, Text } from '@grafana/ui';
|
||||||
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
|
import { useConnectionList } from '../hooks/useConnectionList';
|
||||||
|
import { getErrorMessage } from '../utils/httpUtils';
|
||||||
|
|
||||||
|
import { ConnectionList } from './ConnectionList';
|
||||||
|
|
||||||
|
export default function ConnectionsPage() {
|
||||||
|
const [items, isLoading] = useConnectionList();
|
||||||
|
|
||||||
|
const hasError = !isLoading && !items;
|
||||||
|
const hasNoConnections = !isLoading && items?.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
navId="provisioning"
|
||||||
|
subTitle={t('provisioning.connections.page-subtitle', 'View and manage your app connections')}
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
disabled
|
||||||
|
tooltip={t('provisioning.connections.create-tooltip', 'Connection creation coming soon')}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="provisioning.connections.add-connection">Add connection</Trans>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Page.Contents isLoading={isLoading}>
|
||||||
|
<Stack direction={'column'} gap={3}>
|
||||||
|
{hasError && (
|
||||||
|
<Alert severity="error" title={t('provisioning.connections.error-loading', 'Failed to load connections')}>
|
||||||
|
{getErrorMessage(hasError)}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasNoConnections && (
|
||||||
|
<EmptyState
|
||||||
|
variant="call-to-action"
|
||||||
|
message={t('provisioning.connections.no-connections', 'No connections configured')}
|
||||||
|
>
|
||||||
|
<Text element="p">
|
||||||
|
{t(
|
||||||
|
'provisioning.connections.no-connections-message',
|
||||||
|
'Add a connection to authenticate with external providers'
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{items && items.length > 0 && <ConnectionList items={items} />}
|
||||||
|
</Stack>
|
||||||
|
</Page.Contents>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { t, Trans } from '@grafana/i18n';
|
||||||
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
|
import { Button, ConfirmModal } from '@grafana/ui';
|
||||||
|
import { Connection, useDeleteConnectionMutation } from 'app/api/clients/provisioning/v0alpha1';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
connection: Connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteConnectionButton({ name, connection }: Props) {
|
||||||
|
const [deleteConnection, deleteRequest] = useDeleteConnectionMutation();
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
|
const onConfirm = useCallback(async () => {
|
||||||
|
reportInteraction('grafana_provisioning_connection_deleted', {
|
||||||
|
connectionName: name,
|
||||||
|
connectionType: connection?.spec?.type ?? 'unknown',
|
||||||
|
});
|
||||||
|
|
||||||
|
await deleteConnection({ name });
|
||||||
|
setShowModal(false);
|
||||||
|
}, [deleteConnection, name, connection]);
|
||||||
|
|
||||||
|
const isLoading = deleteRequest.isLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="destructive" size="md" disabled={isLoading} onClick={() => setShowModal(true)}>
|
||||||
|
<Trans i18nKey="provisioning.connections.delete">Delete</Trans>
|
||||||
|
</Button>
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={showModal}
|
||||||
|
title={t('provisioning.connections.delete-title', 'Delete connection')}
|
||||||
|
body={t(
|
||||||
|
'provisioning.connections.delete-confirm',
|
||||||
|
'Are you sure you want to delete this connection? This action cannot be undone.'
|
||||||
|
)}
|
||||||
|
confirmText={t('provisioning.connections.delete', 'Delete')}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
onDismiss={() => setShowModal(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export const PROVISIONING_URL = '/admin/provisioning';
|
export const PROVISIONING_URL = '/admin/provisioning';
|
||||||
|
export const CONNECTIONS_URL = `${PROVISIONING_URL}/connections`;
|
||||||
export const CONNECT_URL = `${PROVISIONING_URL}/connect`;
|
export const CONNECT_URL = `${PROVISIONING_URL}/connect`;
|
||||||
export const GETTING_STARTED_URL = `${PROVISIONING_URL}/getting-started`;
|
export const GETTING_STARTED_URL = `${PROVISIONING_URL}/getting-started`;
|
||||||
export const UPGRADE_URL = 'https://grafana.com/profile/org/subscription';
|
export const UPGRADE_URL = 'https://grafana.com/profile/org/subscription';
|
||||||
|
|||||||
19
public/app/features/provisioning/hooks/useConnectionList.ts
Normal file
19
public/app/features/provisioning/hooks/useConnectionList.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
|
||||||
|
import { ListConnectionApiArg, Connection, useListConnectionQuery } from 'app/api/clients/provisioning/v0alpha1';
|
||||||
|
|
||||||
|
// Sort connections alphabetically by name
|
||||||
|
export function useConnectionList(
|
||||||
|
options: ListConnectionApiArg | typeof skipToken = {}
|
||||||
|
): [Connection[] | undefined, boolean] {
|
||||||
|
const query = useListConnectionQuery(options);
|
||||||
|
const collator = new Intl.Collator(undefined, { numeric: true });
|
||||||
|
|
||||||
|
const sortedItems = query.data?.items?.slice().sort((a, b) => {
|
||||||
|
const nameA = a.metadata?.name ?? '';
|
||||||
|
const nameB = b.metadata?.name ?? '';
|
||||||
|
return collator.compare(nameA, nameB);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [sortedItems, query.isLoading];
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { RouteDescriptor } from 'app/core/navigation/types';
|
|||||||
import { DashboardRoutes } from 'app/types/dashboard';
|
import { DashboardRoutes } from 'app/types/dashboard';
|
||||||
|
|
||||||
import { checkRequiredFeatures } from '../GettingStarted/features';
|
import { checkRequiredFeatures } from '../GettingStarted/features';
|
||||||
import { PROVISIONING_URL, CONNECT_URL, GETTING_STARTED_URL } from '../constants';
|
import { PROVISIONING_URL, CONNECTIONS_URL, CONNECT_URL, GETTING_STARTED_URL } from '../constants';
|
||||||
|
|
||||||
export function getProvisioningRoutes(): RouteDescriptor[] {
|
export function getProvisioningRoutes(): RouteDescriptor[] {
|
||||||
if (!checkRequiredFeatures()) {
|
if (!checkRequiredFeatures()) {
|
||||||
@@ -36,6 +36,12 @@ export function getProvisioningRoutes(): RouteDescriptor[] {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: CONNECTIONS_URL,
|
||||||
|
component: SafeDynamicImport(
|
||||||
|
() => import(/* webpackChunkName: "ConnectionsPage"*/ 'app/features/provisioning/Connection/ConnectionsPage')
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `${CONNECT_URL}/:type`,
|
path: `${CONNECT_URL}/:type`,
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
|
|||||||
@@ -11797,6 +11797,23 @@
|
|||||||
"free-tier-limit-tooltip": "Free-tier accounts are restricted to one connection",
|
"free-tier-limit-tooltip": "Free-tier accounts are restricted to one connection",
|
||||||
"instance-fully-managed-tooltip": "Configuration is disabled because this instance is fully managed"
|
"instance-fully-managed-tooltip": "Configuration is disabled because this instance is fully managed"
|
||||||
},
|
},
|
||||||
|
"connections": {
|
||||||
|
"add-connection": "Add connection",
|
||||||
|
"create-tooltip": "Connection creation coming soon",
|
||||||
|
"delete": "Delete",
|
||||||
|
"delete-confirm": "Are you sure you want to delete this connection? This action cannot be undone.",
|
||||||
|
"delete-title": "Delete connection",
|
||||||
|
"error-loading": "Failed to load connections",
|
||||||
|
"no-connections": "No connections configured",
|
||||||
|
"no-connections-message": "Add a connection to authenticate with external providers",
|
||||||
|
"no-results": "No results matching your query",
|
||||||
|
"page-subtitle": "View and manage your app connections",
|
||||||
|
"search-placeholder": "Search connections",
|
||||||
|
"status-connected": "Connected",
|
||||||
|
"status-disconnected": "Disconnected",
|
||||||
|
"status-unknown": "Unknown",
|
||||||
|
"view": "View"
|
||||||
|
},
|
||||||
"delete-repository-button": {
|
"delete-repository-button": {
|
||||||
"button-delete": "Delete",
|
"button-delete": "Delete",
|
||||||
"confirm-delete-keep-resources": "Are you sure you want to delete the repository configuration but keep its resources?",
|
"confirm-delete-keep-resources": "Are you sure you want to delete the repository configuration but keep its resources?",
|
||||||
|
|||||||
Reference in New Issue
Block a user