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:
Kamil Kisiela 2022-08-26 12:15:36 +02:00 committed by GitHub
parent 8cd7a4a885
commit d9b0aa65f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 88 additions and 37 deletions

View file

@ -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",

View file

@ -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'),

View file

@ -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}
</>