mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Add "Copy Invite Link" button to invitations (#317)
* Add Copy Invite Link button to invitations * Adjust the logic behind picking agg tables Closes #316
This commit is contained in:
parent
8cd7a4a885
commit
d9b0aa65f9
3 changed files with 88 additions and 37 deletions
7
.vscode/terminals.json
vendored
7
.vscode/terminals.json
vendored
|
|
@ -58,6 +58,13 @@
|
|||
"cwd": "packages/services/cdn-worker",
|
||||
"command": "yarn dev"
|
||||
},
|
||||
{
|
||||
"name": "emails:dev",
|
||||
"description": "Run Emails serivce",
|
||||
"open": true,
|
||||
"cwd": "packages/services/emails",
|
||||
"command": "yarn dev"
|
||||
},
|
||||
{
|
||||
"name": "usage:dev",
|
||||
"description": "Run Usage Service",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { format, addMinutes, subDays, isAfter } from 'date-fns';
|
|||
import type { Span } from '@sentry/types';
|
||||
import { batch } from '@theguild/buddy';
|
||||
import { ClickHouse, RowOf } from './clickhouse-client';
|
||||
import { calculateTimeWindow, maxResolution } from './helpers';
|
||||
import { calculateTimeWindow } from './helpers';
|
||||
import type { DateRange } from '../../../shared/entities';
|
||||
import { sentry } from '../../../shared/sentry';
|
||||
|
||||
|
|
@ -94,7 +94,8 @@ function pickQueryByPeriod(
|
|||
span?: Span | undefined;
|
||||
};
|
||||
},
|
||||
period: DateRange | null
|
||||
period: DateRange | null,
|
||||
resolution?: number
|
||||
) {
|
||||
if (!period) {
|
||||
return queryMap.daily;
|
||||
|
|
@ -102,12 +103,25 @@ function pickQueryByPeriod(
|
|||
|
||||
const distance = period.to.getTime() - period.from.getTime();
|
||||
const distanceInHours = distance / 1000 / 60 / 60;
|
||||
const distanceInDays = distance / 1000 / 60 / 60 / 24;
|
||||
|
||||
if (distanceInHours >= 24) {
|
||||
if (resolution) {
|
||||
if (distanceInDays >= resolution) {
|
||||
return queryMap.daily;
|
||||
}
|
||||
|
||||
if (distanceInHours >= resolution) {
|
||||
return queryMap.hourly;
|
||||
}
|
||||
|
||||
return queryMap.regular;
|
||||
}
|
||||
|
||||
if (distanceInHours > 24) {
|
||||
return queryMap.daily;
|
||||
}
|
||||
|
||||
if (distanceInHours >= 1) {
|
||||
if (distanceInHours > 1) {
|
||||
return queryMap.hourly;
|
||||
}
|
||||
|
||||
|
|
@ -115,19 +129,18 @@ function pickQueryByPeriod(
|
|||
}
|
||||
|
||||
// Remove after legacy tables are no longer used
|
||||
function canUseHourlyAggTable({
|
||||
period,
|
||||
resolution = maxResolution,
|
||||
}: {
|
||||
period?: DateRange;
|
||||
resolution?: number;
|
||||
}): boolean {
|
||||
function canUseHourlyAggTable({ period, resolution }: { period?: DateRange; resolution?: number }): boolean {
|
||||
if (period) {
|
||||
const distance = period.to.getTime() - period.from.getTime();
|
||||
const distanceInHours = distance / 1000 / 60 / 60;
|
||||
|
||||
// We can't show data in 90 time-windows from past 24 hours (based on hourly table)
|
||||
if (distanceInHours < resolution) {
|
||||
if (resolution && distanceInHours < resolution) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We can't show data from less past n minutes based on hourly table if the range is less than 1 hours
|
||||
if (distanceInHours < 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1180,9 +1193,10 @@ export class OperationsReader {
|
|||
span,
|
||||
},
|
||||
},
|
||||
period
|
||||
period,
|
||||
resolution
|
||||
)
|
||||
: canUseHourlyAggTable({ period })
|
||||
: canUseHourlyAggTable({ period, resolution })
|
||||
? {
|
||||
query: `
|
||||
SELECT
|
||||
|
|
@ -1458,6 +1472,7 @@ export class OperationsReader {
|
|||
from: subDays(new Date(), daysLimit),
|
||||
to: new Date(),
|
||||
};
|
||||
const resolution = 90;
|
||||
const result = await this.clickHouse.query<{
|
||||
date: number;
|
||||
total: string;
|
||||
|
|
@ -1471,13 +1486,13 @@ export class OperationsReader {
|
|||
toStartOfInterval(timestamp, INTERVAL ${this.clickHouse.translateWindow(
|
||||
calculateTimeWindow({
|
||||
period,
|
||||
resolution: 90,
|
||||
resolution,
|
||||
})
|
||||
)}, 'UTC'),
|
||||
'UTC'),
|
||||
1000) as date,
|
||||
sum(total) as total
|
||||
FROM ${daysLimit > 1 ? 'operations_daily' : 'operations_hourly'}
|
||||
FROM ${daysLimit > 1 && daysLimit >= resolution ? 'operations_daily' : 'operations_hourly'}
|
||||
WHERE timestamp >= subtractDays(NOW(), ${daysLimit})
|
||||
GROUP BY date
|
||||
ORDER BY date
|
||||
|
|
@ -1496,7 +1511,7 @@ export class OperationsReader {
|
|||
from: subDays(new Date(), daysLimit),
|
||||
to: new Date(),
|
||||
},
|
||||
resolution: 90,
|
||||
resolution,
|
||||
})
|
||||
)}, 'UTC'),
|
||||
'UTC'),
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { ReactElement, useEffect, useState } from 'react';
|
||||
import { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { IconProps } from '@chakra-ui/react';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { useFormik } from 'formik';
|
||||
import { gql, useMutation, useQuery } from 'urql';
|
||||
import { DocumentType, gql, useMutation, useQuery } from 'urql';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { useUser } from '@/components/auth/AuthProvider';
|
||||
import { OrganizationLayout } from '@/components/layouts';
|
||||
import { Avatar, Button, Card, Checkbox, DropdownMenu, Input, Title } from '@/components/v2';
|
||||
import { GitHubIcon, GoogleIcon, KeyIcon, MoreIcon, SettingsIcon, TrashIcon } from '@/components/v2/icon';
|
||||
import { CopyIcon, GitHubIcon, GoogleIcon, KeyIcon, MoreIcon, SettingsIcon, TrashIcon } from '@/components/v2/icon';
|
||||
import { ChangePermissionsModal, DeleteMembersModal } from '@/components/v2/modals';
|
||||
import { AuthProvider, MeDocument, OrganizationFieldsFragment, OrganizationType } from '@/graphql';
|
||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||
import { useClipboard } from '@/lib/hooks/use-clipboard';
|
||||
import { useNotifications } from '@/lib/hooks/use-notifications';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { useToggle } from '@/lib/hooks/use-toggle';
|
||||
|
|
@ -27,13 +28,13 @@ const authProviderIcons = {
|
|||
[AuthProvider.Google]: GoogleIcon,
|
||||
} as Record<AuthProvider, React.FC<IconProps> | undefined>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const Members_Invitation = gql(/* GraphQL */ `
|
||||
fragment Members_Invitation on OrganizationInvitation {
|
||||
id
|
||||
createdAt
|
||||
expiresAt
|
||||
email
|
||||
code
|
||||
}
|
||||
`);
|
||||
|
||||
|
|
@ -129,7 +130,7 @@ const InvitationDeleteButton = ({ email, organizationCleanId }: { email: string;
|
|||
const [mutation, mutate] = useMutation(InvitationDeleteButton_DeleteInvitation);
|
||||
|
||||
return (
|
||||
<Button
|
||||
<DropdownMenu.Item
|
||||
disabled={mutation.fetching}
|
||||
onClick={() => {
|
||||
mutate({
|
||||
|
|
@ -140,8 +141,8 @@ const InvitationDeleteButton = ({ email, organizationCleanId }: { email: string;
|
|||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
<TrashIcon /> Remove
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -169,6 +170,44 @@ export const Members_OrganizationMembers = gql(/* GraphQL */ `
|
|||
}
|
||||
`);
|
||||
|
||||
const Invitation = ({
|
||||
invitation,
|
||||
organizationCleanId,
|
||||
}: {
|
||||
invitation: DocumentType<typeof Members_Invitation>;
|
||||
organizationCleanId: string;
|
||||
}) => {
|
||||
const copyToClipboard = useClipboard();
|
||||
const copyLink = useCallback(async () => {
|
||||
await copyToClipboard(`${window.location.origin}/join/${invitation.code}`);
|
||||
}, [invitation.code, copyToClipboard]);
|
||||
|
||||
return (
|
||||
<Card className="flex items-center gap-2.5 bg-gray-800/40">
|
||||
<div className="grow overflow-hidden">
|
||||
<h3 className="line-clamp-1 font-medium">{invitation.email}</h3>
|
||||
<h4 className="text-sm font-light text-gray-500">
|
||||
Invitation expires on {DateFormatter.format(new Date(invitation.expiresAt))}
|
||||
</h4>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Button rotate={90}>
|
||||
<MoreIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content sideOffset={5} align="start">
|
||||
<DropdownMenu.Item onClick={copyLink}>
|
||||
<CopyIcon />
|
||||
Copy invite link
|
||||
</DropdownMenu.Item>
|
||||
<InvitationDeleteButton organizationCleanId={organizationCleanId} email={invitation.email} />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const Page = ({ organization }: { organization: OrganizationFieldsFragment }) => {
|
||||
useOrganizationAccess({
|
||||
scope: OrganizationAccessScope.Members,
|
||||
|
|
@ -297,19 +336,9 @@ const Page = ({ organization }: { organization: OrganizationFieldsFragment }) =>
|
|||
{invitations?.length ? (
|
||||
<div className="pt-3">
|
||||
<div className="border-t-4 border-solid pb-6"></div>
|
||||
{invitations?.map(node => {
|
||||
return (
|
||||
<Card key={node.id} className="flex items-center gap-2.5 bg-gray-800/40">
|
||||
<div className="grow overflow-hidden">
|
||||
<h3 className="line-clamp-1 font-medium">{node.email}</h3>
|
||||
<h4 className="text-sm font-light text-gray-500">
|
||||
Invitation expires on {DateFormatter.format(new Date(node.expiresAt))}
|
||||
</h4>
|
||||
</div>
|
||||
<InvitationDeleteButton organizationCleanId={org.cleanId} email={node.email} />
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{invitations.map(node => (
|
||||
<Invitation key={node.id} invitation={node} organizationCleanId={org.cleanId} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in a new issue