Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6516216
API contract
mtojek Apr 24, 2023
dc202c4
Send agent metrics
mtojek Apr 24, 2023
7747f2d
Ignore metrics to save bandwidth
mtojek Apr 24, 2023
9fd4ddb
fix lint
mtojek Apr 24, 2023
9af0246
logEntry
mtojek Apr 24, 2023
4207dff
make gen
mtojek Apr 24, 2023
99fe1bf
Use errGroup
mtojek Apr 24, 2023
df80e9b
Use MustNewConstMetric
mtojek Apr 25, 2023
d86496e
PoC works
mtojek Apr 25, 2023
10e6d8d
Metrics aggregator with channels
mtojek Apr 25, 2023
8df9eea
Metrics expiry
mtojek Apr 25, 2023
1f5273b
histograms
mtojek Apr 25, 2023
1b8c486
unit test
mtojek Apr 26, 2023
423420b
fmt
mtojek Apr 26, 2023
23bbe94
test: metrics can expire
mtojek Apr 26, 2023
b7011ae
Aggregator
mtojek Apr 26, 2023
29a8702
Address PR comments
mtojek Apr 26, 2023
7acd113
wrap errors
mtojek Apr 26, 2023
b15c7b7
fix
mtojek Apr 26, 2023
2ae7e4e
Update coderd/prometheusmetrics/aggregator.go
mtojek Apr 27, 2023
b04d232
refactor: PTY & SSH (#7100)
spikecurtis Apr 24, 2023
1d93f66
feat(community-templates): Added vscode-server-template (#7219)
nanospearing Apr 24, 2023
c604633
chore: Proxy health status checks + endpoint (#7233)
Emyrk Apr 24, 2023
7d84745
Revert "feat(UI): add workspace restart button (#7137)" (#7268)
Kira-Pilot Apr 24, 2023
407c332
refactor(site): Group app and agent actions together (#7267)
BrunoQuaresma Apr 24, 2023
49b81df
fix(coderd): ensure that user API keys are deleted when a user is (#7…
johnstcn Apr 24, 2023
44217de
chore(dogfood): remove unnecessary docker host replace (#7269)
coadler Apr 25, 2023
e659c36
Fix macOS pty race with dropped output (#7278)
spikecurtis Apr 25, 2023
6dc8b1f
feat: add regions endpoint for proxies feature (#7277)
deansheather Apr 25, 2023
d2233be
fix(healthcheck): don't allow panics to exit coderd (#7276)
coadler Apr 25, 2023
f3f5bed
chore: add security advisories to docs (#7282)
johnstcn Apr 25, 2023
50f60cb
fix(site): Do not show template params if there is no param to be dis…
BrunoQuaresma Apr 25, 2023
1bf1b06
fix(site): Fix default value for options (#7265)
BrunoQuaresma Apr 25, 2023
5f6b4dc
chore: fix flake in apptest reconnecting-pty test (#7281)
deansheather Apr 26, 2023
9141f7c
Reconnecting PTY waits for command output or EOF (#7279)
spikecurtis Apr 26, 2023
e0879b5
docs(site): Mention template editor in template edit docs (#7261)
BrunoQuaresma Apr 26, 2023
b6322d1
fix(site): Fix secondary buttons with popovers (#7296)
BrunoQuaresma Apr 26, 2023
1e3eb06
chore: change some wording in the dashboard (#7293)
bpmct Apr 26, 2023
366859b
feat(agent): add http debug routes for magicsock (#7287)
coadler Apr 26, 2023
ed8106d
feat: add license expiration warning (#7264)
rodrimaia Apr 26, 2023
5733abc
feat: add license settings UI (#7210)
rodrimaia Apr 26, 2023
4937e75
chore: add envbox documentation (#7198)
sreya Apr 26, 2023
619e470
docs: Fix relay link in HA doc (#7159)
winter0mute Apr 27, 2023
16b5353
Merge branch 'main' into 6724-api-collect-metrics
mtojek Apr 27, 2023
c1bd4d2
Refactor Collect channel
mtojek Apr 27, 2023
8baed98
fix
mtojek Apr 27, 2023
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
Prev Previous commit
Next Next commit
feat: add license settings UI (#7210)
* wip: license page

* WIP

* WIP

* wip

* wip

* wip

* wip

* wip

* wip

* Apply suggestions from code review

Co-authored-by: Ben Potter <ben@coder.com>

* wip: ui improvements

* wip: extract components

* wip: stories

* wip: stories

* fixes from PR reviews

* fix stories

* fix empty license page

* fix copy

* fix

* wip

* add golang test

---------

Co-authored-by: Ben Potter <ben@coder.com>
  • Loading branch information
2 people authored and mtojek committed Apr 27, 2023
commit 5733abcd5d3ba7510205656a41cc01a32eedf9b4
7 changes: 7 additions & 0 deletions enterprise/coderd/license/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ func Entitlements(
return entitlements, xerrors.Errorf("query active user count: %w", err)
}

// always shows active user count regardless of license
entitlements.Features[codersdk.FeatureUserLimit] = codersdk.Feature{
Entitlement: codersdk.EntitlementNotEntitled,
Enabled: enablements[codersdk.FeatureUserLimit],
Actual: &activeUserCount,
}

allFeatures := false

// Here we loop through licenses to detect enabled features.
Expand Down
9 changes: 9 additions & 0 deletions enterprise/coderd/license/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ func TestEntitlements(t *testing.T) {
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
})
t.Run("Always return the current user count", func(t *testing.T) {
t.Parallel()
db := dbfake.New()
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.False(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
require.Equal(t, *entitlements.Features[codersdk.FeatureUserLimit].Actual, int64(0))
})
t.Run("SingleLicenseNothing", func(t *testing.T) {
t.Parallel()
db := dbfake.New()
Expand Down
2 changes: 2 additions & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,15 @@
"react": "18.2.0",
"react-chartjs-2": "4.3.1",
"react-color": "2.19.3",
"react-confetti": "^6.1.0",
"react-dom": "18.2.0",
"react-headless-tabs": "6.0.3",
"react-helmet-async": "1.3.0",
"react-i18next": "12.1.1",
"react-markdown": "8.0.3",
"react-router-dom": "6.4.1",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.4.0",
"react-virtualized-auto-sizer": "1.0.7",
"react-window": "1.8.8",
"remark-gfm": "3.0.1",
Expand Down
13 changes: 13 additions & 0 deletions site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ const TemplateSchedulePage = lazy(
),
)

const LicensesSettingsPage = lazy(
() =>
import(
"./pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage"
),
)
const AddNewLicensePage = lazy(
() =>
import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"),
)

export const AppRouter: FC = () => {
return (
<Suspense fallback={<FullScreenLoader />}>
Expand Down Expand Up @@ -244,6 +255,8 @@ export const AppRouter: FC = () => {
element={<DeploySettingsLayout />}
>
<Route path="general" element={<GeneralSettingsPage />} />
<Route path="licenses" element={<LicensesSettingsPage />} />
<Route path="licenses/add" element={<AddNewLicensePage />} />
<Route path="security" element={<SecuritySettingsPage />} />
<Route path="appearance" element={<AppearanceSettingsPage />} />
<Route path="network" element={<NetworkSettingsPage />} />
Expand Down
31 changes: 31 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,37 @@ export const getWorkspaceBuildParameters = async (
)
return response.data
}
type Claims = {
license_expires?: jwt.NumericDate
account_type?: string
account_id?: string
trial: boolean
all_features: boolean
version: number
features: Record<string, number>
require_telemetry?: boolean
}

export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & {
claims: Claims
expires_at: string
}

export const getLicenses = async (): Promise<GetLicensesResponse[]> => {
const response = await axios.get(`/api/v2/licenses`)
return response.data
}

export const createLicense = async (
data: TypesGen.AddLicenseRequest,
): Promise<TypesGen.AddLicenseRequest> => {
const response = await axios.post(`/api/v2/licenses`, data)
return response.data
}

export const removeLicense = async (licenseId: number): Promise<void> => {
await axios.delete(`/api/v2/licenses/${licenseId}`)
}

export class MissingBuildParameters extends Error {
parameters: TypesGen.TemplateVersionParameter[] = []
Expand Down
7 changes: 7 additions & 0 deletions site/src/components/DeploySettingsLayout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { makeStyles } from "@material-ui/core/styles"
import Brush from "@material-ui/icons/Brush"
import LaunchOutlined from "@material-ui/icons/LaunchOutlined"
import ApprovalIcon from "@material-ui/icons/VerifiedUserOutlined"
import LockRounded from "@material-ui/icons/LockOutlined"
import Globe from "@material-ui/icons/PublicOutlined"
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
Expand Down Expand Up @@ -48,6 +49,12 @@ export const Sidebar: React.FC = () => {
>
General
</SidebarNavItem>
<SidebarNavItem
href="licenses"
icon={<SidebarNavItemIcon icon={ApprovalIcon} />}
>
Licenses
</SidebarNavItem>
<SidebarNavItem
href="appearance"
icon={<SidebarNavItemIcon icon={Brush} />}
Expand Down
178 changes: 178 additions & 0 deletions site/src/components/FileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { makeStyles } from "@material-ui/core/styles"
import { Stack } from "components/Stack/Stack"
import { FC, DragEvent, useRef, ReactNode } from "react"
import UploadIcon from "@material-ui/icons/CloudUploadOutlined"
import { useClickable } from "hooks/useClickable"
import CircularProgress from "@material-ui/core/CircularProgress"
import { combineClasses } from "utils/combineClasses"
import IconButton from "@material-ui/core/IconButton"
import RemoveIcon from "@material-ui/icons/DeleteOutline"
import FileIcon from "@material-ui/icons/FolderOutlined"

const useFileDrop = (
callback: (file: File) => void,
fileTypeRequired?: string,
): {
onDragOver: (e: DragEvent<HTMLDivElement>) => void
onDrop: (e: DragEvent<HTMLDivElement>) => void
} => {
const onDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
}

const onDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
const file = e.dataTransfer.files[0]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- file can be undefined
if (!file) {
return
}
if (fileTypeRequired && file.type !== fileTypeRequired) {
return
}
callback(file)
}

return {
onDragOver,
onDrop,
}
}

export interface FileUploadProps {
isUploading: boolean
onUpload: (file: File) => void
onRemove?: () => void
file?: File
removeLabel: string
title: string
description?: ReactNode
extension?: string
fileTypeRequired?: string
}

export const FileUpload: FC<FileUploadProps> = ({
isUploading,
onUpload,
onRemove,
file,
removeLabel,
title,
description,
extension,
fileTypeRequired,
}) => {
const styles = useStyles()
const inputRef = useRef<HTMLInputElement>(null)
const tarDrop = useFileDrop(onUpload, fileTypeRequired)
const clickable = useClickable(() => {
if (inputRef.current) {
inputRef.current.click()
}
})

if (!isUploading && file) {
return (
<Stack
className={styles.file}
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Stack direction="row" alignItems="center">
<FileIcon />
<span>{file.name}</span>
</Stack>

<IconButton title={removeLabel} size="small" onClick={onRemove}>
<RemoveIcon />
</IconButton>
</Stack>
)
}

return (
<>
<div
className={combineClasses({
[styles.root]: true,
[styles.disabled]: isUploading,
})}
{...clickable}
{...tarDrop}
>
<Stack alignItems="center" spacing={1}>
{isUploading ? (
<CircularProgress size={32} />
) : (
<UploadIcon className={styles.icon} />
)}

<Stack alignItems="center" spacing={0.5}>
<span className={styles.title}>{title}</span>
<span className={styles.description}>{description}</span>
</Stack>
</Stack>
</div>

<input
type="file"
ref={inputRef}
className={styles.input}
accept={extension}
onChange={(event) => {
const file = event.currentTarget.files?.[0]
if (file) {
onUpload(file)
}
}}
/>
</>
)
}

const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: theme.shape.borderRadius,
border: `2px dashed ${theme.palette.divider}`,
padding: theme.spacing(6),
cursor: "pointer",

"&:hover": {
backgroundColor: theme.palette.background.paper,
},
},

disabled: {
pointerEvents: "none",
opacity: 0.75,
},

icon: {
fontSize: theme.spacing(8),
},

title: {
fontSize: theme.spacing(2),
},

description: {
color: theme.palette.text.secondary,
textAlign: "center",
maxWidth: theme.spacing(50),
},

input: {
display: "none",
},

file: {
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(2),
background: theme.palette.background.paper,
},
}))
Loading