chore: implement fetch all organizations endpoint (#13941)

* chore: implement fetch all organizations endpoint
* update ui to use list all orgs
This commit is contained in:
Steven Masley
2024-07-18 17:28:36 -05:00
committed by GitHub
parent 6f20a64f9d
commit aa6e6e3d58
11 changed files with 169 additions and 18 deletions
+26
View File
@@ -2037,6 +2037,32 @@ const docTemplate = `{
}
},
"/organizations": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Organizations"
],
"summary": "Get organizations",
"operationId": "get-organizations",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Organization"
}
}
}
}
},
"post": {
"security": [
{
+22
View File
@@ -1779,6 +1779,28 @@
}
},
"/organizations": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Organizations"],
"summary": "Get organizations",
"operationId": "get-organizations",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Organization"
}
}
}
}
},
"post": {
"security": [
{
+1
View File
@@ -865,6 +865,7 @@ func New(options *Options) *API {
apiKeyMiddleware,
)
r.Post("/", api.postOrganizations)
r.Get("/", api.organizations)
r.Route("/{organization}", func(r chi.Router) {
r.Use(
httpmw.ExtractOrganizationParam(options.Database),
+26
View File
@@ -11,12 +11,38 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/codersdk"
)
// @Summary Get organizations
// @ID get-organizations
// @Security CoderSessionToken
// @Produce json
// @Tags Organizations
// @Success 200 {object} []codersdk.Organization
// @Router /organizations [get]
func (api *API) organizations(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organizations, err := api.Database.GetOrganizations(ctx)
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching organizations.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(organizations, convertOrganization))
}
// @Summary Get organization by ID
// @ID get-organization-by-id
// @Security CoderSessionToken
+7 -2
View File
@@ -27,10 +27,15 @@ func TestMultiOrgFetch(t *testing.T) {
require.NoError(t, err)
}
orgs, err := client.OrganizationsByUser(ctx, codersdk.Me)
myOrgs, err := client.OrganizationsByUser(ctx, codersdk.Me)
require.NoError(t, err)
require.NotNil(t, myOrgs)
require.Len(t, myOrgs, len(makeOrgs)+1)
orgs, err := client.Organizations(ctx)
require.NoError(t, err)
require.NotNil(t, orgs)
require.Len(t, orgs, len(makeOrgs)+1)
require.ElementsMatch(t, myOrgs, orgs)
}
func TestOrganizationsByUser(t *testing.T) {
+15
View File
@@ -215,6 +215,21 @@ func (c *Client) OrganizationByName(ctx context.Context, name string) (Organizat
return organization, json.NewDecoder(res.Body).Decode(&organization)
}
func (c *Client) Organizations(ctx context.Context) ([]Organization, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/organizations", nil)
if err != nil {
return []Organization{}, xerrors.Errorf("execute request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return []Organization{}, ReadBodyAsError(res)
}
var organizations []Organization
return organizations, json.NewDecoder(res.Body).Decode(&organizations)
}
func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) {
// OrganizationByName uses the exact same endpoint. It accepts a name or uuid.
// We just provide this function for type safety.
+56
View File
@@ -87,6 +87,62 @@ curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get organizations
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/organizations \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /organizations`
### Example responses
> 200 Response
```json
[
{
"created_at": "2019-08-24T14:15:22Z",
"description": "string",
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"is_default": true,
"name": "string",
"updated_at": "2019-08-24T14:15:22Z"
}
]
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Organization](schemas.md#codersdkorganization) |
<h3 id="get-organizations-responseschema">Response Schema</h3>
Status Code **200**
| Name | Type | Required | Restrictions | Description |
| ---------------- | ----------------- | -------- | ------------ | ----------- |
| `[array item]` | array | false | | |
| `» created_at` | string(date-time) | true | | |
| `» description` | string | false | | |
| `» display_name` | string | false | | |
| `» icon` | string | false | | |
| `» id` | string(uuid) | true | | |
| `» is_default` | boolean | true | | |
| `» name` | string | false | | |
| `» updated_at` | string(date-time) | true | | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Create organization
### Code samples
+1 -1
View File
@@ -565,7 +565,7 @@ class ApiMethods {
getOrganizations = async (): Promise<TypesGen.Organization[]> => {
const response = await this.axios.get<TypesGen.Organization[]>(
"/api/v2/users/me/organizations",
"/api/v2/organizations",
);
return response.data;
};
+13 -4
View File
@@ -4,7 +4,7 @@ import type {
CreateOrganizationRequest,
UpdateOrganizationRequest,
} from "api/typesGenerated";
import { meKey, myOrganizationsKey } from "./users";
import { meKey } from "./users";
export const createOrganization = (queryClient: QueryClient) => {
return {
@@ -13,7 +13,7 @@ export const createOrganization = (queryClient: QueryClient) => {
onSuccess: async () => {
await queryClient.invalidateQueries(meKey);
await queryClient.invalidateQueries(myOrganizationsKey);
await queryClient.invalidateQueries(organizationsKey);
},
};
};
@@ -29,7 +29,7 @@ export const updateOrganization = (queryClient: QueryClient) => {
API.updateOrganization(variables.orgId, variables.req),
onSuccess: async () => {
await queryClient.invalidateQueries(myOrganizationsKey);
await queryClient.invalidateQueries(organizationsKey);
},
};
};
@@ -40,7 +40,7 @@ export const deleteOrganization = (queryClient: QueryClient) => {
onSuccess: async () => {
await queryClient.invalidateQueries(meKey);
await queryClient.invalidateQueries(myOrganizationsKey);
await queryClient.invalidateQueries(organizationsKey);
},
};
};
@@ -78,3 +78,12 @@ export const removeOrganizationMember = (
},
};
};
export const organizationsKey = ["organizations", "me"] as const;
export const organizations = () => {
return {
queryKey: organizationsKey,
queryFn: () => API.getOrganizations(),
};
};
-9
View File
@@ -249,12 +249,3 @@ export const updateAppearanceSettings = (
},
};
};
export const myOrganizationsKey = ["organizations", "me"] as const;
export const myOrganizations = () => {
return {
queryKey: myOrganizationsKey,
queryFn: () => API.getOrganizations(),
};
};
@@ -2,7 +2,7 @@ import { createContext, type FC, Suspense, useContext } from "react";
import { useQuery } from "react-query";
import { Outlet, useLocation, useParams } from "react-router-dom";
import { deploymentConfig } from "api/queries/deployment";
import { myOrganizations } from "api/queries/users";
import { organizations } from "api/queries/organizations";
import type { Organization } from "api/typesGenerated";
import { Loader } from "components/Loader/Loader";
import { Margins } from "components/Margins/Margins";
@@ -39,7 +39,7 @@ export const ManagementSettingsLayout: FC = () => {
const { experiments } = useDashboard();
const { organization } = useParams() as { organization: string };
const deploymentConfigQuery = useQuery(deploymentConfig());
const organizationsQuery = useQuery(myOrganizations());
const organizationsQuery = useQuery(organizations());
const multiOrgExperimentEnabled = experiments.includes("multi-organization");