Skip to content

Commit 13c6c6f

Browse files
feat: WriteData and Execute Query page for homepage experience (#4036)
1 parent 889b2ca commit 13c6c6f

File tree

7 files changed

+409
-0
lines changed

7 files changed

+409
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Libraries
2+
import React, {PureComponent} from 'react'
3+
4+
// Decorator
5+
import {ErrorHandling} from 'src/shared/decorators/errors'
6+
7+
export enum LoadingState {
8+
NotStarted = 'NotStarted',
9+
Loading = 'Loading',
10+
Done = 'Done',
11+
NotFound = 'NotFound',
12+
Error = 'Error',
13+
}
14+
15+
export interface Props {
16+
loading: LoadingState
17+
bucket: string
18+
countDownSeconds: number
19+
}
20+
21+
@ErrorHandling
22+
class ConnectionInformation extends PureComponent<Props> {
23+
public render() {
24+
return (
25+
<div data-testid="connection-information">
26+
<h4 className={`wizard-step--text-state ${this.className}`}>
27+
{this.header}
28+
</h4>
29+
<p>{this.additionalText}</p>
30+
</div>
31+
)
32+
}
33+
34+
private get className(): string {
35+
switch (this.props.loading) {
36+
case LoadingState.Loading:
37+
return 'loading'
38+
case LoadingState.Done:
39+
return 'success'
40+
case LoadingState.NotFound:
41+
case LoadingState.Error:
42+
return 'error'
43+
}
44+
}
45+
46+
private get header(): string {
47+
switch (this.props.loading) {
48+
case LoadingState.Loading:
49+
return 'Awaiting Connection...'
50+
case LoadingState.Done:
51+
return 'Connection Found!'
52+
case LoadingState.NotFound:
53+
return 'Data Not Found'
54+
case LoadingState.Error:
55+
return 'Error Listening for Data'
56+
}
57+
}
58+
59+
private get additionalText(): any {
60+
const docs = (
61+
<span> Make sure the correct bucket is selected and then retry. </span>
62+
)
63+
switch (this.props.loading) {
64+
case LoadingState.Loading:
65+
return `Timeout in ${this.props.countDownSeconds} seconds`
66+
case LoadingState.Done:
67+
return `${this.props.bucket} is receiving data loud and clear!`
68+
case LoadingState.NotFound:
69+
case LoadingState.Error:
70+
return docs
71+
}
72+
}
73+
}
74+
75+
export default ConnectionInformation
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Libraries
2+
import React, {PureComponent} from 'react'
3+
import {RouteComponentProps, withRouter} from 'react-router-dom'
4+
5+
// Apis
6+
import {runQuery} from 'src/shared/apis/query'
7+
8+
// Components
9+
import {ErrorHandling} from 'src/shared/decorators/errors'
10+
import ConnectionInformation, {LoadingState} from './ConnectionInformation'
11+
import {Button} from '@influxdata/clockface'
12+
13+
interface OwnProps {
14+
bucket: string
15+
}
16+
17+
interface State {
18+
loading: LoadingState
19+
timePassedInSeconds: number
20+
secondsLeft: number
21+
previousBucket: string
22+
retry: boolean
23+
}
24+
25+
const MINUTE = 60000
26+
const FETCH_WAIT = 5000
27+
const SECONDS = 60
28+
const TIMER_WAIT = 1000
29+
30+
type Props = RouteComponentProps<{orgID: string}> & OwnProps
31+
32+
@ErrorHandling
33+
class DataListening extends PureComponent<Props, State> {
34+
private intervalID: ReturnType<typeof setInterval>
35+
private startTime: number
36+
private timer: ReturnType<typeof setInterval>
37+
38+
constructor(props) {
39+
super(props)
40+
41+
this.state = {
42+
loading: LoadingState.NotStarted,
43+
timePassedInSeconds: 0,
44+
secondsLeft: SECONDS,
45+
previousBucket: null,
46+
retry: false,
47+
}
48+
}
49+
50+
public componentWillUnmount() {
51+
clearInterval(this.intervalID)
52+
clearInterval(this.timer)
53+
this.setState({
54+
timePassedInSeconds: 0,
55+
secondsLeft: SECONDS,
56+
})
57+
}
58+
59+
componentDidUpdate() {
60+
const {bucket} = this.props
61+
if (
62+
(bucket !== '<BUCKET>' && this.state.previousBucket !== bucket) ||
63+
this.state.retry
64+
) {
65+
// clear timer when bucket changes
66+
clearInterval(this.intervalID)
67+
clearInterval(this.timer)
68+
this.setState({
69+
timePassedInSeconds: 0,
70+
secondsLeft: SECONDS,
71+
})
72+
this.startListeningForData()
73+
this.setState({previousBucket: bucket, retry: false})
74+
}
75+
}
76+
77+
private handleRetry = () => {
78+
this.setState({retry: true})
79+
}
80+
81+
public render() {
82+
return (
83+
<div className="wizard-step--body-streaming" data-testid="streaming">
84+
{this.connectionInfo}
85+
{this.state.loading === LoadingState.NotFound && (
86+
<Button onClick={this.handleRetry} text="Retry" />
87+
)}
88+
</div>
89+
)
90+
}
91+
92+
private get connectionInfo(): JSX.Element {
93+
const {loading} = this.state
94+
95+
if (loading === LoadingState.NotStarted) {
96+
return
97+
}
98+
99+
return (
100+
<ConnectionInformation
101+
loading={this.state.loading}
102+
bucket={this.props.bucket}
103+
countDownSeconds={this.state.secondsLeft}
104+
/>
105+
)
106+
}
107+
108+
private startListeningForData = (): void => {
109+
this.startTimer()
110+
this.setState({loading: LoadingState.Loading}, () => {
111+
this.startTime = Number(new Date())
112+
this.checkForData()
113+
})
114+
}
115+
116+
private checkForData = async (): Promise<void> => {
117+
const {
118+
bucket,
119+
match: {
120+
params: {orgID},
121+
},
122+
} = this.props
123+
const {secondsLeft} = this.state
124+
const script = `from(bucket: "${bucket}")
125+
|> range(start: -1m)`
126+
127+
let responseLength: number
128+
let timePassed: number
129+
130+
try {
131+
const result = await runQuery(orgID, script).promise
132+
133+
if (result.type !== 'SUCCESS') {
134+
throw new Error(result.message)
135+
}
136+
137+
// if the bucket is empty, the CSV returned is '\r\n' which has a length of 2
138+
// so instead, we check for the trimmed version.
139+
responseLength = result.csv.trim().length
140+
timePassed = Number(new Date()) - this.startTime
141+
} catch (err) {
142+
this.setState({loading: LoadingState.Error})
143+
return
144+
}
145+
146+
if (responseLength > 1) {
147+
this.setState({loading: LoadingState.Done})
148+
return
149+
}
150+
151+
if (timePassed >= MINUTE || secondsLeft <= 0) {
152+
this.setState({loading: LoadingState.NotFound})
153+
return
154+
}
155+
156+
this.intervalID = setTimeout(() => {
157+
this.checkForData()
158+
}, FETCH_WAIT)
159+
}
160+
161+
private startTimer() {
162+
this.setState({timePassedInSeconds: 0, secondsLeft: SECONDS})
163+
164+
this.timer = setInterval(this.countDown, TIMER_WAIT)
165+
}
166+
167+
private countDown = () => {
168+
const {secondsLeft} = this.state
169+
const secs = secondsLeft - 1
170+
this.setState({
171+
timePassedInSeconds: SECONDS - secs,
172+
secondsLeft: secs,
173+
})
174+
175+
if (secs === 0) {
176+
clearInterval(this.timer)
177+
}
178+
}
179+
}
180+
181+
export default withRouter(DataListening)

src/homepageExperience/components/HomepageExperience.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,14 @@ $pool-fill: #00a3ff;
145145
font-weight: normal;
146146
}
147147
}
148+
149+
// Write data helper CSS override for homepage experience
150+
151+
.write-data--details-widget-title {
152+
margin-bottom: $cf-space-2xs;
153+
padding-left: $cf-space-2xs + $cf-space-3xs;
154+
display: flex;
155+
justify-content: space-between;
156+
align-items: center;
157+
text-transform: uppercase;
158+
}

src/homepageExperience/components/HomepageIcons.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,18 @@ export const PythonIcon = (
169169
</defs>
170170
</svg>
171171
)
172+
173+
export const ListeningDataHeartbeatIcon = (
174+
<svg
175+
width="22"
176+
height="20"
177+
viewBox="0 0 22 20"
178+
fill="none"
179+
xmlns="http://www.w3.org/2000/svg"
180+
>
181+
<path
182+
d="M8 5.53894L14 19.5389L17.659 10.9999H22V8.99994H16.341L14 14.4609L8 0.460938L4.341 8.99994H0V10.9999H5.659L8 5.53894Z"
183+
fill="#B7B8FF"
184+
/>
185+
</svg>
186+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react'
2+
import CodeSnippet from 'src/shared/components/CodeSnippet'
3+
4+
export const ExecuteQuery = () => {
5+
const query = `query_api = client.query_api()
6+
7+
query = "from(bucket: \\"bucket1\\") |> range(start: -10m) |> mean()"
8+
tables = query_api.query(query, org=org)
9+
10+
for table in tables:
11+
for record in table.records:
12+
print(record)`
13+
14+
return (
15+
<>
16+
<h1>Execute a Flux Query</h1>
17+
<p>
18+
Now let’s query the database for the data points we just wrote. We will
19+
use a short Flux query embedded in our Python. Paste the following code
20+
after the prompt (>>> ) and press Enter
21+
</p>
22+
<CodeSnippet text={query} />
23+
</>
24+
)
25+
}

0 commit comments

Comments
 (0)