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

feat(JAQPOT-177): org associated models #36

Merged
merged 4 commits into from
Jul 22, 2024
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
90 changes: 77 additions & 13 deletions src/app/api.schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,18 @@ export interface paths {
get: operations["getDatasetById"];
};
"/v1/organizations": {
/** Get all organizations */
get: operations["getAllOrganizations"];
/** Get all organizations for a specific user */
get: operations["getAllOrganizationsForUser"];
/** Create a new organization */
post: operations["createOrganization"];
};
"/v1/user/organizations": {
/** Get all user organizations */
get: operations["getAllOrganizationsByUser"];
};
"/v1/organizations/{id}": {
/** Update an existing organization */
put: operations["updateOrganization"];
"/v1/organizations/{id}/partial": {
/** Partially update an existing organization */
patch: operations["partialUpdateOrganization"];
};
"/v1/organizations/{name}": {
/** Get organization by name */
Expand All @@ -109,6 +109,13 @@ export interface paths {
*/
post: operations["createInvitations"];
};
"/v1/organizations/{orgName}/associated-models": {
/**
* Get all models associated with an organization
* @description This endpoint allows users to retrieve all models associated with a specific organization.
*/
get: operations["getAllAssociatedModels"];
};
"/v1/organizations/{name}/invitations/{uuid}": {
/**
* Get the status of an invitation
Expand Down Expand Up @@ -162,6 +169,7 @@ export interface components {
canEdit?: boolean;
/** @description If the current user can delete the model */
canDelete?: boolean;
associatedOrganization?: components["schemas"]["Organization"];
tags?: string;
/**
* Format: date-time
Expand Down Expand Up @@ -315,16 +323,14 @@ export interface components {
/** @example my-awesome-org */
name: string;
creatorId?: string;
visibility: components["schemas"]["OrganizationVisibility"];
/** @example An awesome organization for managing models. */
description?: string;
userIds?: string[];
models?: components["schemas"]["Model"][];
/** @example [email protected] */
contactEmail: string;
/** @example +1234567890 */
contactPhone?: string;
/** @enum {string} */
visibility: "PUBLIC" | "PRIVATE";
/** @example http://www.my-awesome-org.com */
website?: string;
/** @example 123 Organization St., City, Country */
Expand All @@ -334,6 +340,8 @@ export interface components {
created_at?: Record<string, never>;
updated_at?: Record<string, never>;
};
/** @enum {string} */
OrganizationVisibility: "PUBLIC" | "PRIVATE";
OrganizationInvitation: {
/**
* Format: uuid
Expand Down Expand Up @@ -639,6 +647,8 @@ export interface operations {
description?: string;
visibility: components["schemas"]["ModelVisibility"];
organizationIds?: number[];
/** Format: int64 */
associatedOrganizationId?: number;
};
};
};
Expand Down Expand Up @@ -771,8 +781,8 @@ export interface operations {
};
};
};
/** Get all organizations */
getAllOrganizations: {
/** Get all organizations for a specific user */
getAllOrganizationsForUser: {
responses: {
/** @description Successful response */
200: {
Expand Down Expand Up @@ -807,16 +817,22 @@ export interface operations {
};
};
};
/** Update an existing organization */
updateOrganization: {
/** Partially update an existing organization */
partialUpdateOrganization: {
parameters: {
path: {
id: number;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["Organization"];
"application/json": {
name: string;
description?: string;
/** Format: email */
contactEmail: string;
visibility: components["schemas"]["OrganizationVisibility"];
};
};
};
responses: {
Expand Down Expand Up @@ -935,6 +951,54 @@ export interface operations {
};
};
};
/**
* Get all models associated with an organization
* @description This endpoint allows users to retrieve all models associated with a specific organization.
*/
getAllAssociatedModels: {
parameters: {
query?: {
page?: number;
size?: number;
};
path: {
/** @description Name of the organization */
orgName: string;
};
};
responses: {
/** @description Models retrieved successfully */
200: {
content: {
"application/json": {
content?: components["schemas"]["Model"][];
totalElements?: number;
totalPages?: number;
pageSize?: number;
pageNumber?: number;
};
};
};
/** @description Bad request, invalid input */
400: {
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Unauthorized, only authenticated users can access this endpoint */
401: {
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Organization not found */
404: {
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
/**
* Get the status of an invitation
* @description This endpoint allows a user to check the status of an invitation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextResponse } from 'next/server';
import {
ApiResponse,
errorResponse,
handleApiResponse,
} from '@/app/util/response';
import { auth } from '@/auth';
import { isAuthenticated } from '@/app/util/auth';

export async function GET(
request: Request,
{ params }: { params: { organizationSlug: string } },
): Promise<NextResponse<ApiResponse>> {
const session = await auth();
if (!isAuthenticated(session)) {
return errorResponse(
'You need to be authenticated to access this endpoint',
401,
);
}

const res = await fetch(
`${process.env.API_URL}/v1/organizations/${params.organizationSlug}/associated-models`,
{
headers: {
Authorization: `Bearer ${session!.token}`,
'Content-Type': 'application/json',
},
},
);

return handleApiResponse(res);
}
24 changes: 24 additions & 0 deletions src/app/dashboard/models/[modelId]/[tabName]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BeakerIcon,
UserIcon,
CalendarDaysIcon,
BuildingOfficeIcon,
} from '@heroicons/react/24/solid';
import ModelTabs from '@/app/dashboard/models/[modelId]/components/ModelTabs';
import { notFound, redirect } from 'next/navigation';
Expand All @@ -13,6 +14,9 @@ import { Metadata } from 'next';
import { generateSharedMetadata } from '@/app/shared.metadata';
import JaqpotTimeAgo from '@/app/dashboard/models/[modelId]/components/TimeAgo';
import { getErrorMessageFromResponse } from '@/app/util/response';
import { Link } from '@nextui-org/link';
import React from 'react';
import { Tooltip } from '@nextui-org/tooltip';

export async function generateMetadata({
params,
Expand Down Expand Up @@ -144,6 +148,26 @@ export default async function Page({ params }: { params: ModelPageParams }) {
</>
)}
</div>
<div className="flex items-center text-sm text-gray-400">
{model.associatedOrganization &&
model.associatedOrganization.visibility === 'PUBLIC' && (
<>
<Tooltip
content="This model was developed under the auspices of this project/organization."
closeDelay={0}
>
<BuildingOfficeIcon className="mr-2 size-5" />
</Tooltip>
<Link
color="foreground"
href={`/dashboard/organizations/${model.associatedOrganization.name}`}
className="font-medium"
>
@{model.associatedOrganization.name}
</Link>
</>
)}
</div>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default function ModelEditTab({ model }: FeaturesTabProps) {
];
const [isLoading, setIsLoading] = useState(false);

const { data: allOrganizations, error } = useSWR(
const { data: allOrganizationsForUser, error } = useSWR(
`/api/organizations`,
orgFetcher,
);
Expand All @@ -86,6 +86,7 @@ export default function ModelEditTab({ model }: FeaturesTabProps) {
visibility: model.visibility,
description: model.description ?? '',
organizationIds,
associatedOrganizationId: model.associatedOrganization?.id,
});

const [organizations, setOrganizations] = useState<Set<string>>(
Expand Down Expand Up @@ -207,7 +208,7 @@ export default function ModelEditTab({ model }: FeaturesTabProps) {
</Select>
</div>

{formData.visibility === 'ORG_SHARED' && allOrganizations && (
{formData.visibility === 'ORG_SHARED' && allOrganizationsForUser && (
<div>
<Select
defaultSelectedKeys={organizations}
Expand All @@ -218,7 +219,24 @@ export default function ModelEditTab({ model }: FeaturesTabProps) {
// @ts-ignore
onSelectionChange={handleOrganizationsChange}
>
{allOrganizations.map((org) => (
{allOrganizationsForUser.map((org) => (
<SelectItem key={org.id!.toString()}>{org.name}</SelectItem>
))}
</Select>
</div>
)}

{allOrganizationsForUser && (
<div>
<Select
selectedKeys={formData.associatedOrganizationId?.toString()}
name="associatedOrganizationId"
label="Associated Organization"
description="This field is optional and it shows the organization or project within which the model was developed. You will see this information on the model's detail page, with the organization's name being clickable. By clicking on the name, you can visit the organization's page to learn more about it and see other models associated with the same organization."
className="max-w-xl"
onChange={handleChange}
>
{allOrganizationsForUser.map((org) => (
<SelectItem key={org.id!.toString()}>{org.name}</SelectItem>
))}
</Select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { OrganizationDto } from '@/app/api.types';
import OrganizationEditTab from '@/app/dashboard/organizations/[orgName]/components/tabs/OrganizationEditTab';
import OrganizationInvitationsTab from '@/app/dashboard/organizations/[orgName]/components/tabs/OrganizationInvitationsTab';
import MarkdownRenderer from '@/app/dashboard/models/[modelId]/components/MarkdownRenderer';
import OrganizationAssociatedModelsTab from '@/app/dashboard/organizations/[orgName]/components/tabs/OrganizationAssociatedModelsTab';

interface OrganizationTabsProps {
organization: OrganizationDto;
Expand All @@ -30,6 +31,9 @@ export default function OrganizationTabs({
<Tab key="description" title="Description">
<MarkdownRenderer>{organization.description}</MarkdownRenderer>
</Tab>
<Tab key="models" title="Associated models">
<OrganizationAssociatedModelsTab organization={organization} />
</Tab>
{organization.canEdit && (
<Tab key="invitations" title="Invitations">
<OrganizationInvitationsTab organization={organization} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { OrganizationDto } from '@/app/api.types';
import ModelsTable from '@/app/dashboard/models/components/ModelsTable';
import React from 'react';

interface OrganizationAssociatedModelsTabProps {
organization: OrganizationDto;
}

export default function OrganizationAssociatedModelsTab({
organization,
}: OrganizationAssociatedModelsTabProps) {
return (
<div>
<ModelsTable
modelsEndpoint={`/api/organizations/${organization.name}/associated-models`}
/>
</div>
);
}
6 changes: 3 additions & 3 deletions src/app/dashboard/organizations/[orgName]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default async function Page({
return (
<div className="min-h-screen">
<OrganizationBreadcrumbs organization={organization} />
<div className="max-w-4xl mx-auto rounded-lg">
<div className="flex text-3xl font-semibold py-3 items-center leading-7 sm:truncate sm:text-3xl sm:tracking-tight">
<BuildingOfficeIcon className="size-8 mr-2" />
<div className="rounded-lg">
<div className="flex items-center py-3 text-3xl font-semibold leading-7 sm:truncate sm:text-3xl sm:tracking-tight">
<BuildingOfficeIcon className="mr-2 size-8" />
{organization.name}
</div>
<OrganizationTabs organization={organization} />
Expand Down
Loading