-
Notifications
You must be signed in to change notification settings - Fork 22
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(cc-session-tokens): init #1364
base: master
Are you sure you want to change the base?
Conversation
4644f1c
to
fc38ae6
Compare
🔎 A preview has been automatically published : https://clever-components-preview.cellar-c2.services.clever-cloud.com/cc-session-tokens/init/index.html. This preview will be deleted once this PR is closed. |
fc38ae6
to
2cad92a
Compare
2cad92a
to
431c3e0
Compare
*/ | ||
|
||
/** @type {ExpirationWarningThresholds} */ | ||
const DEFAULT_THRESHOLDS = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I should put this inside the isExpirationClose
helper instead? 🤷
I placed it here because I wanted to use it as the function param default value but we can easily refactor to fallback to this if no thresholds are provided 👍
In terms of design, my only comment, which we forgot to mention during our synchro sessions, concerns the display order. To be able to harmonise with the 3 screens, I recommend displaying by creation date (last creation on top). But there's the question of the session screen with the ‘current session’ item: we expect to see it at the top even if it's not the most recent creation date. I'm continuing to think about this and checking some use cases 🤔 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well done @florian-sanders-cc . The design is very nice.
About component name
Maybe we could rename it with the list
suffix like for emails and ssh-keys components.
About expiration warning threshold
I understand why you do that and I'm ok with that even if I find it a bit overkill.
I think that the utils.js
file should remain for things very general. I propose to move the logic into a lib/tokens.js
file, the types in a lib/tokens.types.d.ts
file, and the tests in test/tokens.test.js
file.
Anyway, this code should be moved to the client so that the clever-tools can benefit from it.
tabindex=${ifDefined(tabIndex)} | ||
${isCurrentSession ? ref(this._currentSessionCardRef) : ''} | ||
> | ||
${isCurrentSession || isCleverTeam || isExpirationClose |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: you could make this if inside the this._renderCardHeader
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather keep very explicit render & subrender. Moving these inside the subrender would read as "there's always a card header" and you would have to go within the subrender to discover that in reality the header in only here in specific cases.
It's very subjective of course but I find the verbosity okay here and it feels easy to read / maintain as it is.
|
||
/** @param {CcSessionTokensPropertyValues} changedProperties */ | ||
willUpdate(changedProperties) { | ||
if (changedProperties.has('state') && (this.state.type === 'loaded' || this.state.type === 'revoking-all')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: computing the tokens order and expiration here is a kind of optimization. But in that case, it is maybe overkill since there will never be a huge amount of tokens to handle. Maybe, you want to simplify the code by sorting at every render and get rid of the _sortedAndFormattedTokens
private class field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes you're right, I'll move that to a const
within the render
method 👍
}; | ||
|
||
// FIXME: remove when clever-client exposes types | ||
export type RawSessionTokenData = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: this could be an interface
creationDate: number | string; // timestamp as number or string with ISO format with timezone information | ||
expirationDate: number | string; // timestamp as number or string with ISO format with timezone information | ||
lastUsedDate: number | string; // timestamp as number or string with ISO format with timezone information |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: is it really necessary to provide two ways of specifying dates? I mean, the smart could do the job of providing that in the right format. I would have permit only ISO string since this is what the API aims at doing for every dates (even if not fully the case right now).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have a point, I don't know to be honest 🤷
I'm okay with only accepting Date and make the smart do the work 👍 (that's what you are suggesting right?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is what I mean
export interface SessionTokensStateLoaded { | ||
type: 'loaded'; | ||
tokens: Array<SessionTokenState>; | ||
} | ||
|
||
export interface SessionTokensStateRevokingAllTokens { | ||
type: 'revoking-all'; | ||
tokens: Array<SessionTokenStateRevoking | SessionTokenStateCurrent>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: We could ensure that there is always one and only one current session token with this design:
export interface SessionTokensStateLoaded {
type: 'loaded';
currentToken: SessionToken;
otherTokens: Array<SessionTokenStateIdle|SessionTokenStateRevoking>;
}
export interface SessionTokensStateRevokingAllTokens {
type: 'revoking-all';
currentToken: SessionToken;
otherTokens: Array<SessionToken>;
}
- No state is necessary for the
currentToken
as it is always inidle
state. - When revoking all, no state is necessary on
otherTokens
as they are all considered inrevoking
state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this idea, I'll wait for other opinions but I think I'll go with that, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like @pdesoyres-cc idea 👍
I wanted to propose tokens
and currentTokenId
. This makes the smart easier as you can just pass the token list as is and just set the current token ID you already have from the apiConfig
. Donwside is you have to find the current token in the list and in theory it the find could fail (from the UI component perspective). Pierre's solution does not have this problem, it seems better 👍 💪
function updateOneToken(tokenId, callback) { | ||
updateComponent('state', (state) => { | ||
if (!('tokens' in state)) { | ||
return; | ||
} | ||
|
||
const sessionTokenToUpdate = state.tokens.find((token) => token.id === tokenId); | ||
|
||
if (sessionTokenToUpdate != null) { | ||
callback(sessionTokenToUpdate); | ||
} | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: you could rely on state.type
instead of the presence of the tokens
property, this is more the philosophy of our algebraic types. And anyway, when this function is called, the sate is always in loaded
state, so you could just type the function param :
function updateOneToken(tokenId, callback) { | |
updateComponent('state', (state) => { | |
if (!('tokens' in state)) { | |
return; | |
} | |
const sessionTokenToUpdate = state.tokens.find((token) => token.id === tokenId); | |
if (sessionTokenToUpdate != null) { | |
callback(sessionTokenToUpdate); | |
} | |
}); | |
} | |
function updateOneToken(tokenId, callback) { | |
updateComponent('state', | |
/** @param {SessionTokensStateLoaded} state */ | |
(state) => { | |
const sessionTokenToUpdate = state.tokens.find((token) => token.id === tokenId); | |
if (sessionTokenToUpdate != null) { | |
callback(sessionTokenToUpdate); | |
} | |
}); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, dunno why I went this way to be honest 👍
'cc-session-tokens.card.label.creation': `Creation: `, | ||
'cc-session-tokens.card.label.expiration': `Expiration: `, | ||
'cc-session-tokens.card.label.last-used': `Last used: `, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worry about this trailing space, I wonder if once integrated inside the console it will be dropped by the build process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They might be, but I use flex + gap anyway.
These are mostly here for screen reader users with braille output, it's better if they do retrieve the spaces (but if they don't, it's not a big deal).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah ok, this is good to know
export function isExpirationClose({ creationDate, expirationDate }, thresholds = DEFAULT_THRESHOLDS) { | ||
const formattedExpirationDate = new Date(expirationDate); | ||
const now = new Date(); | ||
const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix: can be move outside the function
* @param {Date|string|number} tokenInfo.creationDate - The creation date of the token | ||
* @param {Date|string|number} tokenInfo.expirationDate - The expiration date of the token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix: I'm not sure we have to handle multiple format. Date
is enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good point, I'll change to Date
only 👍
@pdesoyres-cc thanks for your review! 😍
Great idea, it would also remove the confusion between
Very good points, I agree with you. I'll move to a separate file within the component project first but moving to the client seems like a good idea? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following @marion-izd research & feedback, let's go with date creation ordering and no special treatment for the current session (TODO @florian-sanders-cc)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! Looking forward seeing it in the console.
I only have minor feedback.
import { ifDefined } from 'lit/directives/if-defined.js'; | ||
import { createRef, ref } from 'lit/directives/ref.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤓 nitpick: import order
@@ -273,6 +273,13 @@ export interface NotificationOptions { | |||
closeable?: boolean; | |||
} | |||
|
|||
export type ExpirationWarningThresholds = Array<{ | |||
/** Number of days representing the maximum lifetime for which the given threshold is applicable */ | |||
maxApplicableTokenLifetimeInDays: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 suggestion: maybe remove the mention to token, to make the type a little more generic & less coupled to this use case.
maxApplicableTokenLifetimeInDays: number; | |
maxApplicableLifetimeInDays: number; |
const hasTokens = | ||
(this.state.type === 'loaded' || this.state.type === 'revoking-all') && this.state.tokens.length > 1; | ||
|
||
if (this.state.type === 'error') { | ||
return html`<cc-notice intent="warning" message="${i18n('cc-session-tokens.error')}"></cc-notice>`; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤓 nitpick: invert code blocks
], | ||
}); | ||
|
||
export const dataLoadedWithCleverTeamAsCurrentSession = makeStory(conf, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 suggestion: show cases where:
isExpirationClose
is true andisCleverTeam: true
isExpirationClose
is true andisCleverTeam: true
andtype: 'current'
* @typedef {import('./cc-session-tokens.types.js').SessionTokensStateRevokingAllTokens} SessionTokensStateRevokingAllTokens | ||
* @typedef {import('./cc-session-tokens.types.js').SessionTokenStateRevoking} SessionTokensStateRevoking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💣 chore: unused types
* @param {SessionTokenState[]} allTokens - An array of all session tokens | ||
* @returns {Promise<{ remainingTokens: SessionTokenState[], revokedTokens: string[], errors: any[] }>} A promise that resolves when all tokens are revoked | ||
*/ | ||
revokeAllSessionTokens(allTokens) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 suggestion:
revokeAllSessionTokens(allTokens) { | |
async revokeAllSessionTokens(allTokens) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, nice work Florian! 💪
Nothing much to say apart from what my colleagues already said.
Also, I noticed that the spacing between the top of the block and the title is not the same between the loading and loaded state.
|
||
/** @param {CcSessionTokensPropertyValues} changedProperties */ | ||
willUpdate(changedProperties) { | ||
if (changedProperties.has('state') && (this.state.type === 'loaded' || this.state.type === 'revoking-all')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: if you're in revoking-all
, why would you need to do all of the below?
row-gap: 0.5em; | ||
} | ||
|
||
@supports (grid-template-columns: subgrid) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: that's cool to be able to do this
Fixes #1360
What does this PR do?
cc-session-tokens
componentHow to review?
Important
isExpirationClose
helper.TODO Before merging