Skip to content

Commit 7cb90aa

Browse files
Zaimwa9pre-commit-ci[bot]flagsmith-engineering[bot]
authored
feat: cohort segment detail view with CSV re-synchronisation (#8387)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: flagsmith-engineering[bot] <flagsmith-engineering[bot]@users.noreply.github.com>
1 parent e252a36 commit 7cb90aa

17 files changed

Lines changed: 862 additions & 174 deletions

File tree

frontend/common/services/useCohort.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export const cohortService = service
3232
url: `environments/${query.environmentApiKey}/cohorts/${query.cohortId}/`,
3333
}),
3434
}),
35+
getCohort: builder.query<Res['cohort'], Req['getCohort']>({
36+
providesTags: (res, e, arg) => [{ id: arg.cohortId, type: 'Cohort' }],
37+
query: (query) => ({
38+
url: `environments/${query.environmentApiKey}/cohorts/${query.cohortId}/`,
39+
}),
40+
}),
3541
syncCohortCsv: builder.mutation<
3642
Res['cohortCsvSync'],
3743
Req['syncCohortCsv']
@@ -55,6 +61,24 @@ export const cohortService = service
5561
}
5662
},
5763
}),
64+
updateCohort: builder.mutation<Res['cohort'], Req['updateCohort']>({
65+
invalidatesTags: (q, e, arg) => [
66+
{ id: arg.cohortId, type: 'Cohort' },
67+
{ id: arg.segmentId, type: 'Segment' },
68+
{ id: `LIST${arg.projectId}`, type: 'Segment' },
69+
],
70+
query: ({
71+
cohortId,
72+
environmentApiKey,
73+
projectId: _projectId,
74+
segmentId: _segmentId,
75+
...body
76+
}) => ({
77+
body,
78+
method: 'PATCH',
79+
url: `environments/${environmentApiKey}/cohorts/${cohortId}/`,
80+
}),
81+
}),
5882
// END OF ENDPOINTS
5983
}),
6084
})
@@ -72,7 +96,9 @@ export async function deleteCohort(
7296
export const {
7397
useCreateCohortMutation,
7498
useDeleteCohortMutation,
99+
useGetCohortQuery,
75100
useSyncCohortCsvMutation,
101+
useUpdateCohortMutation,
76102
// END OF EXPORTS
77103
} = cohortService
78104

frontend/common/types/requests.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,19 @@ export type Req = {
186186
cohortId: number
187187
projectId: number
188188
}
189+
getCohort: {
190+
environmentApiKey: string
191+
cohortId: number
192+
}
193+
updateCohort: {
194+
environmentApiKey: string
195+
cohortId: number
196+
projectId: number
197+
segmentId: number
198+
name?: string
199+
description?: string
200+
metadata?: Metadata[]
201+
}
189202
syncCohortCsv: {
190203
environmentApiKey: string
191204
cohortId: number

frontend/common/types/responses.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,14 @@ export type SegmentMembersResponse = PagedResponse<SegmentMember> & {
174174
// Pass as `cursor` to fetch the next page; null when there are no more rows.
175175
next_cursor: string | null
176176
}
177+
export type CohortSourceType = 'csv' | 'amplitude' | 'mixpanel'
178+
177179
export type SegmentCohort = {
178180
id: number
179181
environment: number
180182
environment_api_key: string
181183
environment_name: string
182-
source_type: 'csv'
184+
source_type: CohortSourceType
183185
version: number
184186
deletion_requested_at: string | null
185187
}
@@ -1016,15 +1018,23 @@ export type Metadata = {
10161018
field_value: string
10171019
}
10181020

1021+
export type CohortMembershipCounts = {
1022+
applied: number
1023+
pending_add: number
1024+
pending_remove: number
1025+
}
1026+
10191027
export type Cohort = {
10201028
id: number
10211029
uuid: string
10221030
name: string
10231031
description: string | null
10241032
segment: number
1025-
source_type: 'csv'
1033+
source_type: CohortSourceType
10261034
version: number
10271035
created_at: string
1036+
last_synced_at: string | null
1037+
membership_counts: CohortMembershipCounts
10281038
}
10291039

10301040
export type CohortCsvSyncResult = {

frontend/common/utils/csv.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export type ExtractedIdentifiers = {
1414
// DynamoDB sort keys, capped at 1024 bytes).
1515
export const MAX_IDENTIFIER_BYTES = 1024
1616

17+
// Mirrors the API's COHORT_CSV_MAX_FILE_SIZE_BYTES.
18+
export const MAX_CSV_FILE_SIZE_BYTES = 10 * 1024 * 1024
19+
1720
export function parseCsvText(text: string): string[][] {
1821
const rows: string[][] = []
1922
let row: string[] = []
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.csv-preview {
2+
&__section-label {
3+
font-size: 12px;
4+
letter-spacing: 0.05em;
5+
}
6+
7+
&__table {
8+
border: 1px solid var(--color-border-default);
9+
10+
table {
11+
border-collapse: collapse;
12+
}
13+
14+
th,
15+
td {
16+
padding: 12px 16px;
17+
text-align: left;
18+
white-space: nowrap;
19+
}
20+
21+
tbody tr {
22+
border-top: 1px solid var(--color-border-default);
23+
color: var(--color-text-secondary);
24+
}
25+
}
26+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { FC } from 'react'
2+
import classNames from 'classnames'
3+
import './CsvPreview.scss'
4+
5+
const DEFAULT_ROW_COUNT = 5
6+
7+
export type CsvPreviewType = {
8+
columns: string[]
9+
rows: string[][]
10+
selectedColumn: number | null
11+
rowCount?: number
12+
}
13+
14+
const CsvPreview: FC<CsvPreviewType> = ({
15+
columns,
16+
rowCount = DEFAULT_ROW_COUNT,
17+
rows,
18+
selectedColumn,
19+
}) => (
20+
<div>
21+
<div className='csv-preview__section-label fw-semibold text-secondary text-uppercase mb-2'>
22+
Preview (first {rowCount} rows)
23+
</div>
24+
<div className='csv-preview__table rounded-lg overflow-auto'>
25+
<table className='mb-0 w-100 fs-small'>
26+
<thead>
27+
<tr>
28+
{columns.map((column, i) => (
29+
<th
30+
key={i}
31+
className={classNames(
32+
'fw-semibold',
33+
i === selectedColumn
34+
? 'bg-surface-action-tint'
35+
: 'bg-surface-muted',
36+
)}
37+
>
38+
{column}
39+
</th>
40+
))}
41+
</tr>
42+
</thead>
43+
<tbody>
44+
{rows.slice(0, rowCount).map((row, i) => (
45+
<tr key={i}>
46+
{columns.map((_, j) => (
47+
<td
48+
key={j}
49+
className={classNames({
50+
'bg-surface-action-tint': j === selectedColumn,
51+
})}
52+
>
53+
{row[j]}
54+
</td>
55+
))}
56+
</tr>
57+
))}
58+
</tbody>
59+
</table>
60+
</div>
61+
</div>
62+
)
63+
64+
export default CsvPreview
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './CsvPreview'

frontend/web/components/CsvUpload/CsvUpload.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FC, useCallback, useState } from 'react'
2+
import classNames from 'classnames'
23
import { useDropzone } from 'react-dropzone'
34
import { colorIconAction } from 'common/theme/tokens'
45
import DropIcon from 'components/icons/DropIcon'
@@ -11,6 +12,7 @@ export type CsvUploadType = {
1112
value: File | null
1213
maxSizeBytes?: number
1314
rowCount?: number
15+
disabled?: boolean
1416
onChange: (file: File, text: string) => void
1517
}
1618

@@ -22,6 +24,7 @@ const formatFileSize = (bytes: number) => {
2224
}
2325

2426
const CsvUpload: FC<CsvUploadType> = ({
27+
disabled,
2528
maxSizeBytes,
2629
onChange,
2730
rowCount,
@@ -52,6 +55,7 @@ const CsvUpload: FC<CsvUploadType> = ({
5255
accept: {
5356
'text/csv': ['.csv'],
5457
},
58+
disabled,
5559
maxSize: maxSizeBytes,
5660
multiple: false,
5761
noClick: true,
@@ -90,15 +94,24 @@ const CsvUpload: FC<CsvUploadType> = ({
9094
</Button>
9195
</div>
9296
) : (
93-
<div className='csv-upload__droparea text-center rounded-lg'>
97+
<div
98+
className={classNames(
99+
'csv-upload__droparea text-center rounded-lg',
100+
{
101+
'opacity-50 pe-none': disabled,
102+
},
103+
)}
104+
>
94105
<DropIcon />
95106
<div className='mt-2 mb-2'>
96107
<strong>Drag and drop your CSV here</strong>
97108
</div>
98109
<div className='text-secondary fs-small mb-3'>
99110
or browse it from your computer
100111
</div>
101-
<Button onClick={open}>Select file</Button>
112+
<Button disabled={disabled} onClick={open}>
113+
Select file
114+
</Button>
102115
</div>
103116
)}
104117
</div>

frontend/web/components/SegmentSelect.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Req } from 'common/types/requests'
66
import { components } from 'react-select'
77
import Utils from 'common/utils/utils'
88
import Button from './base/forms/Button'
9+
import Chip from './base/Chip'
910

1011
type SegmentSelectType = {
1112
disabled: boolean
@@ -31,12 +32,19 @@ const SegmentSelect: FC<SegmentSelectType> = ({
3132

3233
let filteredResults: Res['segments']['results'] = []
3334
if (data) {
34-
filteredResults = filter
35-
? (data.results.filter(filter) as Res['segments']['results'])
36-
: data.results
35+
// A cohort awaiting deletion is already gone from the user's point of view.
36+
filteredResults = data.results.filter(
37+
(segment) => !segment.cohort?.deletion_requested_at,
38+
)
39+
if (filter) {
40+
filteredResults = filteredResults.filter(
41+
filter,
42+
) as Res['segments']['results']
43+
}
3744
}
3845
const options = filteredResults.map(
39-
({ feature, id: value, name: label }) => ({
46+
({ cohort, feature, id: value, name: label }) => ({
47+
cohort,
4048
feature,
4149
label,
4250
value,
@@ -84,6 +92,11 @@ const SegmentSelect: FC<SegmentSelectType> = ({
8492
{!!data.feature && (
8593
<div className='unread ml-2 px-2'>Feature-Specific</div>
8694
)}
95+
{!!data.cohort && (
96+
<Chip className='ml-2' size='xs' variant='accent'>
97+
{data.cohort.source_type.toUpperCase()}
98+
</Chip>
99+
)}
87100
</div>
88101
),
89102
}}

0 commit comments

Comments
 (0)